8000 Support redirect URI client policy executor by lexcao · Pull Request #14659 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"))) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!"dev".equals(System.getProperty("kc.profile"))) {
if(!Environment.isDevProfile()){

Copy link
Copy Markdown
Contributor Author

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.isDevProfile is from keycloak-quarkus-server module which is not included in the keycloak-services module.

I think it better keep as it is and wait for Keycloak team to check.

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;
}
}
}
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ org.keycloak.services.clientpolicy.executor.SuppressRefreshTokenRotationExecutor
org.keycloak.services.clientpolicy.executor.RegistrationAccessTokenRotationDisabledExecutorFactory
org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory
org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory
org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory
org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory
org.keycloak.services.clientpolicy.executor.SecureRedirectUrisExecutorFactory
Loading
0