Repository navigation
Support redirect URI client policy executor #14659
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
lexcao
wants to merge
1
commit into
keycloak:main
from
lexcao:14381/redirect-uri-client-policy-executor
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
207 changes: 207 additions & 0 deletions
207
...src/main/java/org/keycloak/services/clientpolicy/executor/SecureRedirectUrisExecutor.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| package org.keycloak.services.clientpolicy.executor; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonProperty; | ||
| import org.keycloak.OAuthErrorException; | ||
| import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation; | ||
| import org.keycloak.representations.idm.ClientRepresentation; | ||
| import org.keycloak.services.clientpolicy.ClientPolicyContext; | ||
| import org.keycloak.services.clientpolicy.ClientPolicyException; | ||
| import org.keycloak.services.clientpolicy.context.AdminClientRegisterContext; | ||
| import org.keycloak.services.clientpolicy.context.AdminClientUpdateContext; | ||
|
|
||
| import java.net.InetAddress; | ||
| import java.net.URI; | ||
| import java.net.URISyntaxException; | ||
| import java.net.UnknownHostException; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public class SecureRedirectUrisExecutor implements ClientPolicyExecutorProvider<SecureRedirectUrisExecutor.Configuration> { | ||
| private Configuration configuration; | ||
|
|
||
| @Override | ||
| public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException { | ||
| ClientRepresentation proposedClient; | ||
| switch (context.getEvent()) { | ||
| case REGISTER: | ||
| proposedClient = ((AdminClientRegisterContext) context).getProposedClientRepresentation(); | ||
| break; | ||
| case UPDATE: | ||
| proposedClient = ((AdminClientUpdateContext) context).getProposedClientRepresentation(); | ||
| break; | ||
| default: | ||
| return; | ||
| } | ||
|
|
||
| for (String s : proposedClient.getRedirectUris()) { | ||
| validate(s); | ||
| } | ||
| } | ||
|
|
||
| void validate(String redirectUri) throws ClientPolicyException { | ||
| UriValidation validation; | ||
| try { | ||
| validation = new UriValidation(redirectUri); | ||
| } catch (URISyntaxException e) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: invalid uri syntax"); | ||
| } | ||
|
|
||
| if (configuration.allowOpen) { | ||
| if (!"dev".equals(System.getProperty("kc.profile"))) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: allow open redirect uris only in dev mode"); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if (!configuration.allowPrivateUseSchema && validation.isPrivateUseSchema()) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: not allowed private use schema"); | ||
| } | ||
|
|
||
| if (!configuration.allowLoopbackInterface && validation.isLoopbackInterface()) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: not allowed loopback interface"); | ||
| } | ||
|
|
||
| if (!configuration.allowHttp && validation.isHttp()) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: not allowed HTTP"); | ||
| } | ||
|
|
||
| if (!configuration.allowWildcardContextPath && validation.isWildcardContextPath()) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: not allowed wildcard context path"); | ||
| } | ||
|
|
||
| // if we allow loopback interface but the interface is not in permitted domains | ||
| // there should allow loopback interface | ||
| if (!configuration.getPermittedDomains().isEmpty() && | ||
| !validation.matchDomains(configuration.getPermittedDomains()) && | ||
| !validation.isLoopbackInterface()) { | ||
| throw invalidRedirectUri("Invalid Redirect Uri: not allowed domain"); | ||
| } | ||
| } | ||
|
|
||
| static class UriValidation { | ||
| private final URI uri; | ||
|
|
||
| public UriValidation(String uriString) throws URISyntaxException { | ||
| this.uri = new URI(uriString); | ||
| } | ||
|
|
||
| boolean isPrivateUseSchema() { | ||
| return uri.isAbsolute() && !isHttp() && !isHttps(); | ||
| } | ||
|
|
||
| boolean isLoopbackInterface() { | ||
| try { | ||
| InetAddress addr = InetAddress.getByName(uri.getHost()); | ||
| return addr.isLoopbackAddress(); | ||
| } catch (UnknownHostException e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| boolean isHttp() { | ||
| return "http".equals(uri.getScheme()); | ||
| } | ||
|
|
||
| boolean isHttps() { | ||
| return "https".equals(uri.getScheme()); | ||
| } | ||
|
|
||
| boolean matchDomain(String domainPattern) { | ||
| return uri.getHost().matches(domainPattern); | ||
| } | ||
|
|
||
| boolean matchDomains(List<String> permittedDomains) { | ||
| return permittedDomains.stream().anyMatch(this::matchDomain); | ||
| } | ||
|
|
||
| boolean isWildcardContextPath() { | ||
| return uri.getPath().startsWith("/*") || uri.getPath().startsWith("*"); | ||
| } | ||
| } | ||
|
|
||
| private static ClientPolicyException invalidRedirectUri(String errorDetail) { | ||
| return new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, errorDetail); | ||
| } | ||
|
|
||
| public Configuration getConfiguration() { | ||
| return configuration; | ||
| } | ||
|
|
||
| @Override | ||
| public void setupConfiguration(Configuration configuration) { | ||
| this.configuration = configuration; | ||
| } | ||
|
|
||
| @Override | ||
| public Class<Configuration> getExecutorConfigurationClass() { | ||
| return Configuration.class; | ||
| } | ||
|
|
||
| @Override | ||
| public String getProviderId() { | ||
| return SecureRedirectUrisExecutorFactory.PROVIDER_ID; | ||
| } | ||
|
|
||
| public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation { | ||
| @JsonProperty(SecureRedirectUrisExecutorFactory.ALLOW_PRIVATE_USE_SCHEMA) | ||
| private boolean allowPrivateUseSchema; | ||
| @JsonProperty(SecureRedirectUrisExecutorFactory.ALLOW_LOOPBACK_INTERFACE) | ||
| private boolean allowLoopbackInterface = true; | ||
| @JsonProperty(SecureRedirectUrisExecutorFactory.ALLOW_HTTP) | ||
| private boolean allowHttp; | ||
| @JsonProperty(SecureRedirectUrisExecutorFactory.ALLOW_WILDCARD_CONTEXT_PATH) | ||
| private boolean allowWildcardContextPath = true; | ||
| @JsonProperty(SecureRedirectUrisExecutorFactory.ALLOW_OPEN) | ||
| private boolean allowOpen; | ||
| @JsonProperty(SecureRedirectUrisExecutorFactory.PERMITTED_DOMAINS) | ||
| private List<String> permittedDomains = Collections.emptyList(); | ||
|
|
||
| public boolean isAllowPrivateUseSchema() { | ||
| return allowPrivateUseSchema; | ||
| } | ||
|
|
||
| public void setAllowPrivateUseSchema(boolean allowPrivateUseSchema) { | ||
| this.allowPrivateUseSchema = allowPrivateUseSchema; | ||
| } | ||
|
|
||
| public boolean isAllowLoopbackInterface() { | ||
| return allowLoopbackInterface; | ||
| } | ||
|
|
||
| public void setAllowLoopbackInterface(boolean allowLoopbackInterface) { | ||
| this.allowLoopbackInterface = allowLoopbackInterface; | ||
| } | ||
|
|
||
| public boolean isAllowHttp() { | ||
| return allowHttp; | ||
| } | ||
|
|
||
| public void setAllowHttp(boolean allowHttp) { | ||
| this.allowHttp = allowHttp; | ||
| } | ||
|
|
||
| public boolean isAllowWildcardContextPath() { | ||
| return allowWildcardContextPath; | ||
| } | ||
|
|
||
| public void setAllowWildcardContextPath(boolean allowWildcardContextPath) { | ||
| this.allowWildcardContextPath = allowWildcardContextPath; | ||
| } | ||
|
|
||
| public boolean isAllowOpen() { | ||
| return allowOpen; | ||
| } | ||
|
|
||
| public void setAllowOpen(boolean allowOpen) { | ||
| this.allowOpen = allowOpen; | ||
| } | ||
|
|
||
| public List<String> getPermittedDomains() { | ||
| return permittedDomains; | ||
| } | ||
|
|
||
| public void setPermittedDomains(List<String> permittedDomains) { | ||
| this.permittedDomains = permittedDomains; | ||
| } | ||
| } | ||
| } | ||
10BC8
127 changes: 127 additions & 0 deletions
127
...n/java/org/keycloak/services/clientpolicy/executor/SecureRedirectUrisExecutorFactory.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package org.keycloak.services.clientpolicy.executor; | ||
|
|
||
| import org.keycloak.Config; | ||
| import org.keycloak.models.KeycloakSession; | ||
| import org.keycloak.models.KeycloakSessionFactory; | ||
| import org.keycloak.provider.ProviderConfigProperty; | ||
| import org.keycloak.provider.ProviderConfigurationBuilder; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class SecureRedirectUrisExecutorFactory implements ClientPolicyExecutorProviderFactory { | ||
|
|
||
| public static final String PROVIDER_ID = "secure-redirect-uris"; | ||
| public static final String ALLOW_PRIVATE_USE_SCHEMA = "allow-private-use-schema"; | ||
| public static final String ALLOW_LOOPBACK_INTERFACE = "allow-loopback-interface"; | ||
| public static final String ALLOW_HTTP = "allow-http"; | ||
| public static final String ALLOW_WILDCARD_CONTEXT_PATH = "allow-wildcard-context-path"; | ||
| public static final String ALLOW_OPEN = "allow-open"; | ||
| public static final String PERMITTED_DOMAINS = "permitted-domains"; | ||
|
|
||
| @Override | ||
| public String getHelpText() { | ||
| return "Check the configured redirect-uris for a client when created or updated"; | ||
| } | ||
|
|
||
| @Override | ||
| public List<ProviderConfigProperty> getConfigProperties() { | ||
| return CONFIG_PROPERTIES; | ||
| } | ||
|
|
||
| @Override | ||
| public ClientPolicyExecutorProvider create(KeycloakSession session) { | ||
| return new SecureRedirectUrisExecutor(); | ||
| } | ||
|
|
||
| @Override | ||
| public void init(Config.Scope config) { | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public void postInit(KeycloakSessionFactory factory) { | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
|
|
||
| } | ||
|
|
||
| @Override | ||
| public String getId() { | ||
| return PROVIDER_ID; | ||
| } | ||
|
|
||
| private static final List<ProviderConfigProperty> CONFIG_PROPERTIES; | ||
|
|
||
| static { | ||
| CONFIG_PROPERTIES = ProviderConfigurationBuilder.create() | ||
|
|
||
| .property() | ||
| .name(ALLOW_PRIVATE_USE_SCHEMA) | ||
| .label("Allow private-use schema redirection") | ||
| .helpText("If ON, then it will allow private-use URI schemes. For example, " + | ||
| "an app that controls the domain name 'app.example.com' " + | ||
| "can use 'com.example.app' as their scheme.") | ||
| .type(ProviderConfigProperty.BOOLEAN_TYPE) | ||
| .defaultValue(false) | ||
| .add() | ||
|
|
||
| .property() | ||
| .name(ALLOW_LOOPBACK_INTERFACE) | ||
| .label("Allow loopback interface redirection") | ||
| .helpText("If ON, then it will allow loopback network interface. For example, " + | ||
| "'http://127.0.0.1:{port}/{path}' for IPv4, and " + | ||
| "'http://[::1]:{port}/{path}' for IPv6") | ||
| .type(ProviderConfigProperty.BOOLEAN_TYPE) | ||
| .defaultValue(false) | ||
| .add() | ||
|
|
||
| .property() | ||
| .name(ALLOW_PRIVATE_USE_SCHEMA) | ||
| .label("Allow private-use schema") | ||
| .helpText("If ON, then it will allow private-use URI schemes. For example, " + | ||
| "an app that controls the domain name \"app.example.com\" " + | ||
| "can use \"com.example.app\" as their scheme.") | ||
| .type(ProviderConfigProperty.BOOLEAN_TYPE) | ||
| .defaultValue(true) | ||
| .add() | ||
|
|
||
| .property() | ||
| .name(ALLOW_HTTP) | ||
| .label("Allow http schema redirection") | ||
| .helpText("If ON, then it will allow http uris.") | ||
| .type(ProviderConfigProperty.BOOLEAN_TYPE) | ||
| .defaultValue(false) | ||
| .add() | ||
|
|
||
| .property() | ||
| .name(ALLOW_WILDCARD_CONTEXT_PATH) | ||
| .label("Allow wildcard in context-path") | ||
| .helpText("If ON, then it will allow wildcard in context-path uris. For example, " + | ||
| "domain.example.com/*") | ||
| .type(ProviderConfigProperty.BOOLEAN_TYPE) | ||
| .defaultValue(true) | ||
| .add() | ||
|
|
||
| .property() | ||
| .name(ALLOW_OPEN) | ||
| .label("Allow open redirection") | ||
| .helpText("If ON, then it will allow open uris. " + | ||
| "It is only available in developer environments.") | ||
| .type(ProviderConfigProperty.BOOLEAN_TYPE) | ||
| .defaultValue(false) | ||
| .add() | ||
|
|
||
| .property() | ||
| .name(PERMITTED_DOMAINS) | ||
| .label("Allow permitted domains") | ||
| .helpText("If ON, it will allow wildcard in hostname and " + | ||
| "check the uri matches one of these permitted domains") | ||
| .type(ProviderConfigProperty.MULTIVALUED_STRING_TYPE) | ||
| .add() | ||
|
|
||
| .build(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @1-alex98
Thanks, currently
Environment.isDevProfileis fromkeycloak-quarkus-servermodule which is not included in thekeycloak-servicesmodule.I think it better keep as it is and wait for Keycloak team to check.