Client policies: executor for validate and match a redirect URI
closes #25637 Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
parent
870befa422
commit
1bdbaa2ca5
9 changed files with 1689 additions and 1 deletions
|
@ -131,6 +131,7 @@ One of several purposes for this executor is to realize the security requirement
|
||||||
* Enforce <<_dpop-bound-tokens,DPoP-binding tokens>> is used (available when `dpop` feature is enabled)
|
* Enforce <<_dpop-bound-tokens,DPoP-binding tokens>> is used (available when `dpop` feature is enabled)
|
||||||
* Enforce <<_using_lightweight_access_token, using lightweight access token>>
|
* Enforce <<_using_lightweight_access_token, using lightweight access token>>
|
||||||
* Enforce that <<_refresh_token_rotation,refresh token rotation>> is skipped and there is no refresh token returned from the refresh token response
|
* Enforce that <<_refresh_token_rotation,refresh token rotation>> is skipped and there is no refresh token returned from the refresh token response
|
||||||
|
* Enforce a valid redirect URI that the OAuth 2.1 specification requires
|
||||||
|
|
||||||
[[_client_policy_profile]]
|
[[_client_policy_profile]]
|
||||||
=== Profile
|
=== Profile
|
||||||
|
|
|
@ -6,3 +6,5 @@ An open redirector is an endpoint using a parameter to automatically redirect a
|
||||||
{project_name} requires that all registered applications and clients register at least one redirection URI pattern. When a client requests that {project_name} performs a redirect, {project_name} checks the redirect URI against the list of valid registered URI patterns. Clients and applications must register as specific a URI pattern as possible to mitigate open redirector attacks.
|
{project_name} requires that all registered applications and clients register at least one redirection URI pattern. When a client requests that {project_name} performs a redirect, {project_name} checks the redirect URI against the list of valid registered URI patterns. Clients and applications must register as specific a URI pattern as possible to mitigate open redirector attacks.
|
||||||
|
|
||||||
If an application requires a non http(s) custom scheme, it should be an explicit part of the validation pattern (for example `custom:/app/\*`). For security reasons a general pattern like `*` does not cover non http(s) schemes.
|
If an application requires a non http(s) custom scheme, it should be an explicit part of the validation pattern (for example `custom:/app/\*`). For security reasons a general pattern like `*` does not cover non http(s) schemes.
|
||||||
|
|
||||||
|
By using <<_client_policies, Client Policies>>, an administrator can make sure that clients cannot register open redirect URLs such as `*`.
|
||||||
|
|
|
@ -3,3 +3,5 @@
|
||||||
=== Unspecific redirect URIs
|
=== Unspecific redirect URIs
|
||||||
|
|
||||||
Make your registered redirect URIs as specific as feasible. Registering vague redirect URIs for xref:con-oidc-auth-flows_{context}[Authorization Code Flows] can allow malicious clients to impersonate another client with broader access. Impersonation can happen if two clients live under the same domain, for example.
|
Make your registered redirect URIs as specific as feasible. Registering vague redirect URIs for xref:con-oidc-auth-flows_{context}[Authorization Code Flows] can allow malicious clients to impersonate another client with broader access. Impersonation can happen if two clients live under the same domain, for example.
|
||||||
|
|
||||||
|
You can use secure redirect uris enforcer executor for your realm. The result makes sure that client administrators are able to register only clients with specific redirect-uris matching various requirements such as requiring that a URL cannot have wildcards in the context path or can be limited to specified permitted domains. See <<_client_policies, Client Policies>> for details about how to configure client policies with a specific executor.
|
|
@ -0,0 +1,512 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientpolicy.executor;
|
||||||
|
|
||||||
|
import java.net.*;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import com.google.common.net.InetAddresses;
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
|
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||||
|
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 org.keycloak.services.clientpolicy.context.ClientCRUDContext;
|
||||||
|
import org.keycloak.services.clientpolicy.context.DynamicClientRegisterContext;
|
||||||
|
import org.keycloak.services.clientpolicy.context.DynamicClientUpdateContext;
|
||||||
|
import org.keycloak.services.clientpolicy.context.PreAuthorizationRequestContext;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.UriType;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public class SecureRedirectUrisEnforcerExecutor implements ClientPolicyExecutorProvider<SecureRedirectUrisEnforcerExecutor.Configuration> {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(SecureRedirectUrisEnforcerExecutor.class);
|
||||||
|
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private Configuration configuration;
|
||||||
|
|
||||||
|
public static final String ERR_GENERAL = "Invalid Redirect Uri: invalid uri";
|
||||||
|
|
||||||
|
public static final String ERR_LOOPBACK = "Invalid Redirect Uri: invalid loopback address";
|
||||||
|
public static final String ERR_PRIVATESCHEME = "Invalid Redirect Uri: invalid private use scheme";
|
||||||
|
public static final String ERR_NORMALURI = "Invalid Redirect Uri: invalid uri";
|
||||||
|
|
||||||
|
public SecureRedirectUrisEnforcerExecutor(KeycloakSession session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProviderId() {
|
||||||
|
return SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setupConfiguration(Configuration config) {
|
||||||
|
this.configuration = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<Configuration> getExecutorConfigurationClass() {
|
||||||
|
return Configuration.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Configuration extends ClientPolicyExecutorConfigurationRepresentation {
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_PRIVATE_USE_URI_SCHEME)
|
||||||
|
protected boolean allowPrivateUseUriScheme;
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_IPV4_LOOPBACK_ADDRESS)
|
||||||
|
protected boolean allowIPv4LoopbackAddress;
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_IPV6_LOOPBACK_ADDRESS)
|
||||||
|
protected boolean allowIPv6LoopbackAddress;
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_HTTP_SCHEME)
|
||||||
|
protected boolean allowHttpScheme;
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_WILDCARD_CONTEXT_PATH)
|
||||||
|
protected boolean allowWildcardContextPath;
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_PERMITTED_DOMAINS)
|
||||||
|
protected List<String> allowPermittedDomains = Collections.emptyList();
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.OAUTH_2_1_COMPLIANT)
|
||||||
|
protected boolean oauth2_1complient;
|
||||||
|
@JsonProperty(SecureRedirectUrisEnforcerExecutorFactory.ALLOW_OPEN_REDIRECT)
|
||||||
|
protected boolean allowOpenRedirect;
|
||||||
|
|
||||||
|
public boolean isAllowPrivateUseUriScheme() {
|
||||||
|
return allowPrivateUseUriScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowPrivateUseUriScheme(boolean allowPrivateUseUriScheme) {
|
||||||
|
this.allowPrivateUseUriScheme = allowPrivateUseUriScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowIPv4LoopbackAddress() {
|
||||||
|
return allowIPv4LoopbackAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowIPv4LoopbackAddress(boolean allowIPv4LoopbackAddress) {
|
||||||
|
this.allowIPv4LoopbackAddress = allowIPv4LoopbackAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowIPv6LoopbackAddress() {
|
||||||
|
return allowIPv6LoopbackAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowIPv6LoopbackAddress(boolean allowIPv6LoopbackAddress) {
|
||||||
|
this.allowIPv6LoopbackAddress = allowIPv6LoopbackAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowHttpScheme() {
|
||||||
|
return allowHttpScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowHttpScheme(boolean allowHttpScheme) {
|
||||||
|
this.allowHttpScheme = allowHttpScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowWildcardContextPath() {
|
||||||
|
return allowWildcardContextPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowWildcardContextPath(boolean allowWildcardContextPath) {
|
||||||
|
this.allowWildcardContextPath = allowWildcardContextPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getAllowPermittedDomains() {
|
||||||
|
return allowPermittedDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowPermittedDomains(List<String> permittedDomains) {
|
||||||
|
this.allowPermittedDomains = permittedDomains;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOAuth2_1Compliant() {
|
||||||
|
return oauth2_1complient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOAuth2_1Compliant(boolean oauth21complient) {
|
||||||
|
this.oauth2_1complient = oauth21complient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowOpenRedirect() {
|
||||||
|
return allowOpenRedirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAllowOpenRedirect(boolean allowOpenRedirect) {
|
||||||
|
this.allowOpenRedirect = allowOpenRedirect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Configuration getConfiguration() {
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void executeOnEvent(ClientPolicyContext context) throws ClientPolicyException {
|
||||||
|
switch (context.getEvent()) {
|
||||||
|
case REGISTER:
|
||||||
|
if (context instanceof AdminClientRegisterContext || context instanceof DynamicClientRegisterContext) {
|
||||||
|
List<String> redirectUris = ((ClientCRUDContext)context).getProposedClientRepresentation().getRedirectUris();
|
||||||
|
if (redirectUris == null || redirectUris.isEmpty() || ((ClientCRUDContext)context).getAuthenticatedClient() == null) {
|
||||||
|
throw invalidRedirectUri(ERR_GENERAL);
|
||||||
|
}
|
||||||
|
verifyRedirectUris(((ClientCRUDContext)context).getAuthenticatedClient().getRootUrl(), redirectUris);
|
||||||
|
} else {
|
||||||
|
throw invalidRedirectUri(ERR_GENERAL);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case UPDATE:
|
||||||
|
if (context instanceof AdminClientUpdateContext || context instanceof DynamicClientUpdateContext) {
|
||||||
|
List<String> redirectUris = ((ClientCRUDContext)context).getProposedClientRepresentation().getRedirectUris();
|
||||||
|
if (redirectUris == null || redirectUris.isEmpty() || ((ClientCRUDContext)context).getAuthenticatedClient() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
verifyRedirectUris(((ClientCRUDContext)context).getAuthenticatedClient().getRootUrl(), redirectUris);
|
||||||
|
} else {
|
||||||
|
throw invalidRedirectUri(ERR_GENERAL);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case PRE_AUTHORIZATION_REQUEST:
|
||||||
|
String redirectUriParam = ((PreAuthorizationRequestContext)context).getRequestParameters()
|
||||||
|
.getFirst(OAuth2Constants.REDIRECT_URI);
|
||||||
|
String clientId = ((PreAuthorizationRequestContext)context).getClientId();
|
||||||
|
ClientModel client = session.getContext().getRealm().getClientByClientId(clientId);
|
||||||
|
if (client == null) {
|
||||||
|
throw invalidRedirectUri("Invalid parameter: clientId");
|
||||||
|
}
|
||||||
|
verifyRedirectUri(redirectUriParam, true);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyRedirectUris(String rootUri, List<String> redirectUris) throws ClientPolicyException {
|
||||||
|
// open redirect allows any value for redirect uri
|
||||||
|
if (configuration.isAllowOpenRedirect()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String uri : RedirectUtils.resolveValidRedirects(session, rootUri, new HashSet<>(redirectUris))) {
|
||||||
|
verifyRedirectUri(uri, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void verifyRedirectUri(String redirectUri, boolean isRedirectUriParam) throws ClientPolicyException {
|
||||||
|
UriValidation validation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
validation = new UriValidation(redirectUri, isRedirectUriParam, configuration);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
logger.debugv("URISyntaxException - input = {0}, errMessage = {1], errReason = {2}, redirectUri = {3}", e.getInput(), e.getMessage(), e.getReason(), redirectUri);
|
||||||
|
throw invalidRedirectUri(ERR_GENERAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
validation.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UriValidation {
|
||||||
|
public final URI uri;
|
||||||
|
public final boolean isRedirectUriParameter;
|
||||||
|
public final Configuration config;
|
||||||
|
|
||||||
|
public UriValidation(String uriString, boolean isRedirectUriParameter, Configuration config) throws URISyntaxException {
|
||||||
|
this.uri = new URI(uriString);
|
||||||
|
this.isRedirectUriParameter = isRedirectUriParameter;
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validate() throws ClientPolicyException {
|
||||||
|
switch (identifyUriType()) {
|
||||||
|
case IPV4_LOOPBACK_ADDRESS:
|
||||||
|
if(!config.isAllowIPv4LoopbackAddress() || !isValidIPv4LoopbackAddress()) {
|
||||||
|
throw invalidRedirectUri(ERR_LOOPBACK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IPV6_LOOPBACK_ADDRESS:
|
||||||
|
if(!config.isAllowIPv6LoopbackAddress() || !isValidIPv6LoopbackAddress()) {
|
||||||
|
throw invalidRedirectUri(ERR_LOOPBACK);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PRIVATE_USE_URI_SCHEME:
|
||||||
|
if(!config.isAllowPrivateUseUriScheme() || !isValidPrivateUseUriScheme()) {
|
||||||
|
throw invalidRedirectUri(ERR_PRIVATESCHEME);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NORMAL_URI:
|
||||||
|
if(!isValidNormalUri()) {
|
||||||
|
throw invalidRedirectUri(ERR_NORMALURI);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default :
|
||||||
|
logger.debugv("Invalid URI Type - input = {0}", uri.toString());
|
||||||
|
throw invalidRedirectUri(ERR_GENERAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UriType identifyUriType() {
|
||||||
|
// NOTE: the order of evaluation methods is important.
|
||||||
|
if (isIPv4LoopbackAddress()) {
|
||||||
|
return UriType.IPV4_LOOPBACK_ADDRESS;
|
||||||
|
} else if (isIPv6LoopbackAddress()) {
|
||||||
|
return UriType.IPV6_LOOPBACK_ADDRESS;
|
||||||
|
} else if (isPrivateUseScheme()) {
|
||||||
|
return UriType.PRIVATE_USE_URI_SCHEME;
|
||||||
|
} else if (isNormalUri()) {
|
||||||
|
return UriType.NORMAL_URI;
|
||||||
|
} else {
|
||||||
|
return UriType.INVALID_URI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isIPv4LoopbackAddress() {
|
||||||
|
return isLoopbackAddressRedirectUri(i->i instanceof Inet4Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isIPv6LoopbackAddress() {
|
||||||
|
return isLoopbackAddressRedirectUri(i->i instanceof Inet6Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isLoopbackAddressRedirectUri(Predicate<InetAddress> p) {
|
||||||
|
if (uri.getHost() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InetAddress addr;
|
||||||
|
try {
|
||||||
|
addr = InetAddresses.forUriString(uri.getHost());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!addr.isLoopbackAddress()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p.test(addr)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPrivateUseScheme() {
|
||||||
|
// NOTE: this method assumes that the uri is not loopback address
|
||||||
|
return uri.isAbsolute() && !isHttp() && !isHttps();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isNormalUri() {
|
||||||
|
// NOTE: this method assumes that the uri is not loopback address
|
||||||
|
return isHttp() || isHttps();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isValidIPv4LoopbackAddress() {
|
||||||
|
return isValidLoopbackAddress(i->true);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isValidIPv6LoopbackAddress() {
|
||||||
|
return isValidLoopbackAddress(i->{
|
||||||
|
if (!"[::1]".equals(i.getHost())) { // [::1] is only allowed.
|
||||||
|
logger.debugv("Invalid IPv6LoopbackAddress: unacceptable form - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isValidLoopbackAddress(Predicate<URI> p) {
|
||||||
|
// valid addresses depend on configurations
|
||||||
|
|
||||||
|
if (!config.isAllowHttpScheme() && isHttp()) {
|
||||||
|
logger.debugv("Invalid LoopbackAddress: HTTP not allowed - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isAllowWildcardContextPath()) {
|
||||||
|
if (isIncludeInvalidWildcard()) {
|
||||||
|
logger.debugv("Invalid LoopbackAddress: invalid Wildcard - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isIncludeWildcard()) {
|
||||||
|
logger.debugv("Invalid LoopbackAddress: Wildcard not allowed - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isOAuth2_1Compliant()) {
|
||||||
|
if (isIncludeUriFragment()) { // URL fragment is not allowed
|
||||||
|
logger.debugv("Invalid LoopbackAddress: URI fragment not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIncludeWildcard()) { // wildcard is not allowed
|
||||||
|
logger.debugv("Invalid LoopbackAddress: Wildcard not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("localhost".equalsIgnoreCase(uri.getHost())) { // "localhost" is not allowed.
|
||||||
|
logger.debugv("Invalid LoopbackAddress: localhost not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRedirectUriParameter) {
|
||||||
|
if (uri.getPort() < 0 || uri.getPort() > 65535) { // only 0 to 65535 are allowed. no port number is not allowed.
|
||||||
|
logger.debugv("Invalid LoopbackAddress: invalid port number - OAuth 2.1 compliant - redirect_uri parameter - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (uri.getPort() > -1) { // any port number is not allowed
|
||||||
|
logger.debugv("Invalid LoopbackAddress: port number not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!p.test(uri)) return false; // additional tests for OAuth 2.1 compliant
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isValidPrivateUseUriScheme() {
|
||||||
|
// valid addresses depend on configurations
|
||||||
|
|
||||||
|
if (config.isAllowWildcardContextPath()) {
|
||||||
|
if (isIncludeInvalidWildcard()) {
|
||||||
|
logger.debugv("Invalid PrivateUseUriScheme: invalid Wildcard - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isIncludeWildcard()) {
|
||||||
|
logger.debugv("Invalid PrivateUseUriScheme: Wildcard not allowed - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isOAuth2_1Compliant()) {
|
||||||
|
if (isIncludeUriFragment()) { // URL fragment is not allowed
|
||||||
|
logger.debugv("Invalid PrivateUseUriScheme: URI fragment not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIncludeWildcard()) { // wildcard is not allowed
|
||||||
|
logger.debugv("Invalid PrivateUseUriScheme: Wildcard not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.getScheme() == null || !uri.getScheme().contains(".")) { // a single word scheme name is not allowed.
|
||||||
|
logger.debugv("Invalid PrivateUseUriScheme: a single word scheme name is not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isValidNormalUri() {
|
||||||
|
// valid addresses depend on configurations
|
||||||
|
|
||||||
|
if (!config.isAllowHttpScheme() && isHttp()) {
|
||||||
|
logger.debugv("Invalid NormalUri: HTTP not allowed - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isAllowWildcardContextPath()) {
|
||||||
|
if (isIncludeInvalidWildcard()) {
|
||||||
|
logger.debugv("Invalid NormalUri: invalid Wildcard - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isIncludeWildcard()) {
|
||||||
|
logger.debugv("Invalid NormalUri: Wildcard not allowed - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.getAllowPermittedDomains() != null && !config.getAllowPermittedDomains().isEmpty()) {
|
||||||
|
if (!matchDomains(config.getAllowPermittedDomains())) {
|
||||||
|
logger.debugv("Invalid NormalUri: no permitted domain matched - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isOAuth2_1Compliant()) {
|
||||||
|
if (!isHttps()) { // only https scheme is allowed.
|
||||||
|
logger.debugv("Invalid NormalUri: HTTP not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIncludeUriFragment()) { // URL fragment is not allowed.
|
||||||
|
logger.debugv("Invalid NormalUri: URI fragment not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIncludeWildcard()) { // wildcard is not allowed.
|
||||||
|
logger.debugv("Invalid NormalUri: Wildcard not allowed - OAuth 2.1 compliant - input = {0}", uri.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchDomain(String domainPattern) {
|
||||||
|
return uri.getHost() != null && uri.getHost().matches(domainPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchDomains(List<String> permittedDomains) {
|
||||||
|
return permittedDomains.stream().anyMatch(this::matchDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isHttp() {
|
||||||
|
return "http".equals(uri.getScheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isHttps() {
|
||||||
|
return "https".equals(uri.getScheme());
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isWildcardContextPath() {
|
||||||
|
return uri.getPath() != null && (uri.getPath().startsWith("/*") || uri.getPath().startsWith("*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isIncludeUriFragment() {
|
||||||
|
return uri.toString().contains("#");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isIncludeWildcard() {
|
||||||
|
return uri.toString().contains("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isIncludeInvalidWildcard() {
|
||||||
|
// NOTE: this method assumes that the uri includes at least one wildcard.
|
||||||
|
if (!isWildcardContextPath()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return uri.toString().length() - uri.toString().replace("*", "").length() != 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClientPolicyException invalidRedirectUri(String errorDetail) {
|
||||||
|
return new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, errorDetail);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientpolicy.executor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.Config.Scope;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||||
|
|
||||||
|
public class SecureRedirectUrisEnforcerExecutorFactory implements ClientPolicyExecutorProviderFactory {
|
||||||
|
|
||||||
|
public static final String PROVIDER_ID = "secure-redirect-uris-enforcer";
|
||||||
|
|
||||||
|
public static final String ALLOW_IPV4_LOOPBACK_ADDRESS = "allow-ipv4-loopback-address";
|
||||||
|
public static final String ALLOW_IPV6_LOOPBACK_ADDRESS = "allow-ipv6-loopback-address";
|
||||||
|
public static final String ALLOW_PRIVATE_USE_URI_SCHEME = "allow-private-use-uri-scheme";
|
||||||
|
|
||||||
|
public static final String ALLOW_HTTP_SCHEME = "allow-http-scheme";
|
||||||
|
public static final String ALLOW_WILDCARD_CONTEXT_PATH = "allow-wildcard-context-path";
|
||||||
|
public static final String ALLOW_PERMITTED_DOMAINS = "allow-permitted-domains";
|
||||||
|
public static final String OAUTH_2_1_COMPLIANT = "oauth-2-1-compliant";
|
||||||
|
|
||||||
|
public static final String ALLOW_OPEN_REDIRECT = "allow-open-redirect";
|
||||||
|
|
||||||
|
public enum UriType {
|
||||||
|
NORMAL_URI,
|
||||||
|
IPV4_LOOPBACK_ADDRESS,
|
||||||
|
IPV6_LOOPBACK_ADDRESS,
|
||||||
|
PRIVATE_USE_URI_SCHEME,
|
||||||
|
INVALID_URI
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientPolicyExecutorProvider create(KeycloakSession session) {
|
||||||
|
return new SecureRedirectUrisEnforcerExecutor(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Scope config) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return PROVIDER_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "On registering and updating a client, this executor only allows a valid redirect uri. On receiving an authorization request, this executor checks whether a redirect uri parameter matches registered redirect uris in the way that depends on the executor's setting.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return CONFIG_PROPERTIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES;
|
||||||
|
|
||||||
|
static {
|
||||||
|
CONFIG_PROPERTIES = ProviderConfigurationBuilder.create()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(ALLOW_IPV4_LOOPBACK_ADDRESS)
|
||||||
|
.label("Allow IPv4 loopback address")
|
||||||
|
.helpText("If ON, then the executor allows IPv4 loopback address as a valid redirect uri. " +
|
||||||
|
"For example, 'http://127.0.0.1:{port}/{path}' . ")
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.defaultValue(false)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(ALLOW_IPV6_LOOPBACK_ADDRESS)
|
||||||
|
.label("Allow IPv6 loopback address")
|
||||||
|
.helpText("If ON, then the executor allows IPv6 loopback address as a valid redirect uri. " +
|
||||||
|
"For example, 'http://[::1]:{port}/{path}' . ")
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.defaultValue(false)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(ALLOW_PRIVATE_USE_URI_SCHEME)
|
||||||
|
.label("Allow private use URI scheme")
|
||||||
|
.helpText("If ON, then the executor allows a private-use URI scheme (aka custom URL scheme) as a valid redirect uri. " +
|
||||||
|
"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_HTTP_SCHEME)
|
||||||
|
.label("Allow http scheme")
|
||||||
|
.helpText("If On, then the executor allows http scheme as a valid redirect uri.")
|
||||||
|
.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(false)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(ALLOW_PERMITTED_DOMAINS)
|
||||||
|
.label("Allow permitted domains")
|
||||||
|
.helpText("If some domains are filled, the redirect uri host must match one of registered domains. " +
|
||||||
|
"If not filled, then all domains are possible to use. The domains are checked by using regex. " +
|
||||||
|
"For example use pattern like this '(.*)\\.example\\.org' if you want clients to register redirect-uris only from domain 'example.org'." +
|
||||||
|
"Don't forget to use escaping of special characters like dots as otherwise dot is interpreted as any character in regex!")
|
||||||
|
.type(ProviderConfigProperty.MULTIVALUED_STRING_TYPE)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(OAUTH_2_1_COMPLIANT)
|
||||||
|
.label("OAuth 2.1 Compliant")
|
||||||
|
.helpText("If On, then the executor checks and matches the uri by following OAuth 2.1 specification. This means that for example URL fragments, wildcard redirect uris or URL using 'localhost' are not allowed.")
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.defaultValue(false)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.property()
|
||||||
|
.name(ALLOW_OPEN_REDIRECT)
|
||||||
|
.label("Allow open redirect")
|
||||||
|
.helpText("If ON, then the executor does not verify a redirect uri even if its other setting is ON. " +
|
||||||
|
"WARNING: This is insecure and should be used with care as open redirects are bad practice.")
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.defaultValue(false)
|
||||||
|
.add()
|
||||||
|
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,3 +24,4 @@ org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory
|
org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory
|
org.keycloak.services.clientpolicy.executor.DPoPBindEnforcerExecutorFactory
|
||||||
org.keycloak.services.clientpolicy.executor.UseLightweightAccessTokenExecutorFactory
|
org.keycloak.services.clientpolicy.executor.UseLightweightAccessTokenExecutorFactory
|
||||||
|
org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory
|
|
@ -0,0 +1,442 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.services.clientpolicy.executor;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutor.Configuration;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutor.UriValidation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.BooleanNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_HTTP_SCHEME;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_IPV4_LOOPBACK_ADDRESS;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_IPV6_LOOPBACK_ADDRESS;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_PRIVATE_USE_URI_SCHEME;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_WILDCARD_CONTEXT_PATH;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_OPEN_REDIRECT;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.ALLOW_PERMITTED_DOMAINS;
|
||||||
|
import static org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory.OAUTH_2_1_COMPLIANT;
|
||||||
|
|
||||||
|
public class SecureRedirectUrisEnforcerExecutorTest {
|
||||||
|
|
||||||
|
private static SecureRedirectUrisEnforcerExecutor executor;
|
||||||
|
private ObjectNode configuration;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupAll() {
|
||||||
|
executor = new SecureRedirectUrisEnforcerExecutor(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
configuration = JsonSerialization.createObjectNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void defaultConfiguration() {
|
||||||
|
setupConfiguration(configuration);
|
||||||
|
Configuration configuration = executor.getConfiguration();
|
||||||
|
|
||||||
|
assertFalse(configuration.isAllowIPv4LoopbackAddress());
|
||||||
|
assertFalse(configuration.isAllowIPv6LoopbackAddress());
|
||||||
|
assertFalse(configuration.isAllowPrivateUseUriScheme());
|
||||||
|
assertFalse(configuration.isAllowHttpScheme());
|
||||||
|
assertFalse(configuration.isAllowWildcardContextPath());
|
||||||
|
assertFalse(configuration.isOAuth2_1Compliant());
|
||||||
|
assertFalse(configuration.isAllowOpenRedirect());
|
||||||
|
|
||||||
|
assertTrue(configuration.getAllowPermittedDomains().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failUriSyntax() {
|
||||||
|
checkFail("https://keycloak.org\n" ,false, SecureRedirectUrisEnforcerExecutor.ERR_GENERAL);
|
||||||
|
checkFail("Collins'&1=1;--" ,false, SecureRedirectUrisEnforcerExecutor.ERR_GENERAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failValidatePrivateUseUriScheme() {
|
||||||
|
// default config
|
||||||
|
checkFail("myapp:/oauth.redirect", false, SecureRedirectUrisEnforcerExecutor.ERR_PRIVATESCHEME);
|
||||||
|
|
||||||
|
// allow private use uri scheme
|
||||||
|
enable(ALLOW_PRIVATE_USE_URI_SCHEME);
|
||||||
|
checkFail("myapp.example.com:/*", false, SecureRedirectUrisEnforcerExecutor.ERR_PRIVATESCHEME);
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkFail("myapp.example.com:/*/abc/*/efg", false, SecureRedirectUrisEnforcerExecutor.ERR_PRIVATESCHEME);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
Stream.of(
|
||||||
|
"myapp:/oauth.redirect#pinpoint",
|
||||||
|
"myapp.example.com:/oauth.redirect/*",
|
||||||
|
"myapp:/oauth.redirect"
|
||||||
|
).forEach(i->checkFail(i, false, SecureRedirectUrisEnforcerExecutor.ERR_PRIVATESCHEME));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successValidatePrivateUseUriScheme() {
|
||||||
|
// allow private use uri scheme
|
||||||
|
enable(ALLOW_PRIVATE_USE_URI_SCHEME);
|
||||||
|
Stream.of(
|
||||||
|
"com.example.app:/oauth2redirect/example-provider",
|
||||||
|
"com.example.app:51004/oauth2redirect/example-provider"
|
||||||
|
).forEach(i->checkSuccess(i, false));
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkSuccess("myapp.example.com:/*", false);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
checkSuccess("com.example.app:/oauth2redirect/example-provider", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failValidateIPv4LoopbackAddress() {
|
||||||
|
// default config
|
||||||
|
checkFail("https://127.0.0.1/auth/admin", false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK);
|
||||||
|
|
||||||
|
// allow IPv4 loopback address
|
||||||
|
enable(ALLOW_IPV4_LOOPBACK_ADDRESS);
|
||||||
|
Stream.of(
|
||||||
|
"http://127.0.0.1:8080/auth/admin",
|
||||||
|
"https://127.0.0.1:8080/*"
|
||||||
|
).forEach(i->checkFail(i, false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK));
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkFail("https://127.0.0.1:8080/*/efg/*/abc", false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK);
|
||||||
|
|
||||||
|
// allow http scheme
|
||||||
|
enable(ALLOW_HTTP_SCHEME);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
Stream.of(
|
||||||
|
"http://127.0.0.1:8080/auth/admin",
|
||||||
|
"http://127.0.0.1/*",
|
||||||
|
"http://127.0.0.1/auth/admin#fragment"
|
||||||
|
).forEach(i->checkFail(i, false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK));
|
||||||
|
// authorization code request redirect_uri parameter
|
||||||
|
Stream.of(
|
||||||
|
"http://127.0.0.1:123456/oauth2redirect/example-provider",
|
||||||
|
"http://127.0.0.1/oauth2redirect/example-provider"
|
||||||
|
).forEach(i->checkFail(i, true, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successValiIPv4LoopbackAddress() {
|
||||||
|
// allow IPv4 loopback address
|
||||||
|
enable(ALLOW_IPV4_LOOPBACK_ADDRESS);
|
||||||
|
Stream.of(
|
||||||
|
"https://127.0.0.1:8443",
|
||||||
|
"https://localhost/auth/admin"
|
||||||
|
).forEach(i->checkSuccess(i, false));
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkSuccess("https://localhost/*", false);
|
||||||
|
|
||||||
|
// allow http scheme
|
||||||
|
enable(ALLOW_HTTP_SCHEME);
|
||||||
|
checkSuccess("http://127.0.0.1:8080/oauth2redirect", false);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
checkSuccess("http://127.0.0.1/oauth2redirect/example-provider", false);
|
||||||
|
checkSuccess("http://127.0.0.1:43321/oauth2redirect/example-provider", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failValidateIPv6LoopbackAddress() {
|
||||||
|
// default config
|
||||||
|
checkFail("https://[::1]:9999/oauth2redirect/example-provider", false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK);
|
||||||
|
|
||||||
|
// allow IPv6 loopback address
|
||||||
|
enable(ALLOW_IPV6_LOOPBACK_ADDRESS);
|
||||||
|
checkFail("http://[::1]:9999/oauth2redirect/example-provider", false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK);
|
||||||
|
checkFail("https://[::1]:9999/*", false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK);
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkFail("https://[::1]/*/efg/*/abc", false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK);
|
||||||
|
|
||||||
|
// allow http scheme
|
||||||
|
enable(ALLOW_HTTP_SCHEME);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
Stream.of(
|
||||||
|
"http://[::1]:8080/auth/admin",
|
||||||
|
"http://[::1]/*",
|
||||||
|
"http://[::1]/auth/admin#fragment",
|
||||||
|
"https://[0:0:0:0:0:0:0:1]:8080"
|
||||||
|
).forEach(i->checkFail(i, false, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK));
|
||||||
|
// authorization code request redirect_uri parameter
|
||||||
|
Stream.of(
|
||||||
|
"http://[::1]:123456/oauth2redirect/example-provider",
|
||||||
|
"http://[::1]/oauth2redirect/example-provider"
|
||||||
|
).forEach(i->checkFail(i, true, SecureRedirectUrisEnforcerExecutor.ERR_LOOPBACK));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successValiIPv6LoopbackAddress() {
|
||||||
|
// allow IPv6 loopback address
|
||||||
|
enable(ALLOW_IPV6_LOOPBACK_ADDRESS);
|
||||||
|
for (String i : Arrays.asList(
|
||||||
|
"https://[::1]/oauth2redirect/example-provider",
|
||||||
|
"https://[::1]:8443"
|
||||||
|
)) {
|
||||||
|
checkSuccess(i, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkSuccess("https://[::1]/*", false);
|
||||||
|
|
||||||
|
// allow http scheme
|
||||||
|
enable(ALLOW_HTTP_SCHEME);
|
||||||
|
checkSuccess("http://[::1]:8080/oauth2redirect", false);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
checkSuccess("http://[::1]/oauth2redirect/example-provider", false);
|
||||||
|
checkSuccess("http://[::1]:43321/oauth2redirect/example-provider", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failNormalUri() {
|
||||||
|
// default config
|
||||||
|
checkFail("http://192.168.1.211:9088/oauth2redirect/example-provider/test", false, SecureRedirectUrisEnforcerExecutor.ERR_NORMALURI);
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkFail("https://192.168.1.211/*/efg/*/abc", false, SecureRedirectUrisEnforcerExecutor.ERR_NORMALURI);
|
||||||
|
|
||||||
|
// allow http scheme
|
||||||
|
enable(ALLOW_HTTP_SCHEME);
|
||||||
|
|
||||||
|
// allow permitted domains
|
||||||
|
permittedDomains("oauth.redirect", "((dev|test)-)*example.org");
|
||||||
|
checkFail("http://192.168.1.211/oauth2redirect/example-provider/test", false, SecureRedirectUrisEnforcerExecutor.ERR_NORMALURI);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
Stream.of(
|
||||||
|
"http://dev-example.com/auth/callback",
|
||||||
|
"https://test-example.com:8443/*",
|
||||||
|
"https://oauth.redirect:8443/auth/callback#fragment"
|
||||||
|
).forEach(i->checkFail(i, true, SecureRedirectUrisEnforcerExecutor.ERR_NORMALURI));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successNormalUri() {
|
||||||
|
// default config
|
||||||
|
Stream.of(
|
||||||
|
"https://example.org/realms/master",
|
||||||
|
"https://192.168.1.211:9088/oauth2redirect/example-provider/test",
|
||||||
|
"https://192.168.1.211/"
|
||||||
|
).forEach(i->checkSuccess(i, false));
|
||||||
|
|
||||||
|
// allow wildcard context path
|
||||||
|
enable(ALLOW_WILDCARD_CONTEXT_PATH);
|
||||||
|
checkSuccess("https://example.org/*", false);
|
||||||
|
|
||||||
|
// allow http scheme
|
||||||
|
enable(ALLOW_HTTP_SCHEME);
|
||||||
|
|
||||||
|
// allow permitted domains
|
||||||
|
permittedDomains("oauth.redirect", "((dev|test)-)*example.org");
|
||||||
|
checkSuccess("http://test-example.org:8080/*", false);
|
||||||
|
|
||||||
|
// OAuth 2.1 compliant
|
||||||
|
enable(OAUTH_2_1_COMPLIANT);
|
||||||
|
checkSuccess("https://dev-example.org:8080/redirect", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successDefaultConfiguration() {
|
||||||
|
Stream.of(
|
||||||
|
"https://example.org/realms/master",
|
||||||
|
"https://192.168.1.211:9088/oauth2redirect/example-provider/test",
|
||||||
|
"https://192.168.1.211/"
|
||||||
|
).forEach(i->checkSuccess(i, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successAllConfigurationEnabled() {
|
||||||
|
// except ALLOW_OPEN_REDIRECT
|
||||||
|
enable(
|
||||||
|
ALLOW_WILDCARD_CONTEXT_PATH,
|
||||||
|
ALLOW_IPV4_LOOPBACK_ADDRESS,
|
||||||
|
ALLOW_IPV6_LOOPBACK_ADDRESS,
|
||||||
|
ALLOW_HTTP_SCHEME,
|
||||||
|
ALLOW_PRIVATE_USE_URI_SCHEME
|
||||||
|
);
|
||||||
|
|
||||||
|
permittedDomains("oauth.redirect", "((dev|test)-)*example.org");
|
||||||
|
|
||||||
|
Stream.of(
|
||||||
|
"http://127.0.0.1/*/realms/master",
|
||||||
|
"http://[::1]/*/realms/master",
|
||||||
|
"myapp.example.com://oauth.redirect",
|
||||||
|
"https://test-example.org/*/auth/admin"
|
||||||
|
).forEach(i->checkSuccess(i, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void successAllConfigurationDisabled() {
|
||||||
|
disable(
|
||||||
|
ALLOW_WILDCARD_CONTEXT_PATH,
|
||||||
|
ALLOW_IPV4_LOOPBACK_ADDRESS,
|
||||||
|
ALLOW_IPV6_LOOPBACK_ADDRESS,
|
||||||
|
ALLOW_HTTP_SCHEME,
|
||||||
|
ALLOW_PRIVATE_USE_URI_SCHEME,
|
||||||
|
ALLOW_OPEN_REDIRECT
|
||||||
|
);
|
||||||
|
|
||||||
|
Stream.of(
|
||||||
|
"https://keycloak.org/sso/silent-callback.html",
|
||||||
|
"https://example.org/auth/realms/master/broker/oidc/endpoint",
|
||||||
|
"https://192.168.8.1:12345/auth/realms/master/broker/oidc/endpoint"
|
||||||
|
).forEach(i->checkSuccess(i, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void permittedDomains(String... domains) {
|
||||||
|
ArrayNode arrayNode = JsonSerialization.mapper.createArrayNode();
|
||||||
|
Arrays.stream(domains).forEach(arrayNode::add);
|
||||||
|
configuration.set(ALLOW_PERMITTED_DOMAINS, arrayNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disable(String... config) {
|
||||||
|
Arrays.stream(config).forEach(it -> configuration.set(it, BooleanNode.getFalse()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enable(String... config) {
|
||||||
|
Arrays.stream(config).forEach(it -> configuration.set(it, BooleanNode.getTrue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupConfiguration(JsonNode node) {
|
||||||
|
Configuration configuration = JsonSerialization.mapper.convertValue(node, executor.getExecutorConfigurationClass());
|
||||||
|
executor.setupConfiguration(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkFail(String redirectUri, boolean isRedirectUriParameter, String errorDetail) {
|
||||||
|
try {
|
||||||
|
doValidate(redirectUri, isRedirectUriParameter);
|
||||||
|
fail();
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
assertEquals(OAuthErrorException.INVALID_REQUEST, cpe.getMessage());
|
||||||
|
assertEquals(errorDetail, cpe.getErrorDetail());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkSuccess(String redirectUri, boolean isRedirectUriParameter) {
|
||||||
|
try {
|
||||||
|
doValidate(redirectUri, isRedirectUriParameter);
|
||||||
|
} catch (ClientPolicyException e) {
|
||||||
|
assertNull(e.getErrorDetail(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doValidate(String redirectUri, boolean isRedirectUriParameter) throws ClientPolicyException {
|
||||||
|
setupConfiguration(configuration);
|
||||||
|
executor.verifyRedirectUri(redirectUri, isRedirectUriParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void matchDomains() throws URISyntaxException {
|
||||||
|
SecureRedirectUrisEnforcerExecutor.Configuration config = new SecureRedirectUrisEnforcerExecutor.Configuration();
|
||||||
|
|
||||||
|
// no domains
|
||||||
|
UriValidation validation = new UriValidation("http://localhost:8080/auth/realms/master/account", false, config);
|
||||||
|
boolean matches = validation.matchDomains(Collections.emptyList());
|
||||||
|
assertFalse(matches);
|
||||||
|
|
||||||
|
// 1 domain not match
|
||||||
|
matches = validation.matchDomains(Collections.singletonList("local-\\w+"));
|
||||||
|
assertFalse(matches);
|
||||||
|
|
||||||
|
// 1 domain match
|
||||||
|
matches = validation.matchDomains(Collections.singletonList("localhost"));
|
||||||
|
assertTrue(matches);
|
||||||
|
|
||||||
|
matches = validation.matchDomains(Collections.singletonList("local\\w+"));
|
||||||
|
assertTrue(matches);
|
||||||
|
|
||||||
|
// 2 domains not match
|
||||||
|
matches = validation.matchDomains(Arrays.asList(
|
||||||
|
"local-\\w+",
|
||||||
|
"localhost2"
|
||||||
|
));
|
||||||
|
assertFalse(matches);
|
||||||
|
|
||||||
|
// 2 domain match
|
||||||
|
matches = validation.matchDomains(Arrays.asList(
|
||||||
|
"local\\w+",
|
||||||
|
"localhost"
|
||||||
|
));
|
||||||
|
assertTrue(matches);
|
||||||
|
|
||||||
|
// 3 more cases
|
||||||
|
String givenPattern = "((dev|test)-)*example.org";
|
||||||
|
String[] expectMatches = new String[]{
|
||||||
|
"https://dev-example.org",
|
||||||
|
"https://test-example.org",
|
||||||
|
"https://example.org",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String match : expectMatches) {
|
||||||
|
validation = new UriValidation(match, false, config);
|
||||||
|
assertTrue(match, validation.matchDomain(givenPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] expectNoneMatches = new String[]{
|
||||||
|
"https://prod-example.org",
|
||||||
|
"https://testexample.org"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (String match : expectNoneMatches) {
|
||||||
|
validation = new UriValidation(match, false, config);
|
||||||
|
assertFalse(match, validation.matchDomain(givenPattern));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,556 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.keycloak.testsuite.client.policies;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createAnyClientConditionConfig;
|
||||||
|
import static org.keycloak.testsuite.util.ClientPoliciesUtil.createSecureRedirectUrisEnforcerExecutorConfig;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.client.registration.ClientRegistrationException;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||||
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
|
import org.keycloak.services.clientpolicy.condition.AnyClientConditionFactory;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutor;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutorFactory;
|
||||||
|
import org.keycloak.testsuite.util.ClientPoliciesUtil;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.ServerURLs;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPoliciesTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
|
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
|
||||||
|
testRealms.add(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_normalUri() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->{}))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// no redirect uri is not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterByAdmin(List.of(""));
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// HTTP scheme not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterDynamically(List.of("http://app.example.com:51004/oauth2redirect/example-provider"));
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
Arrays.asList("https://app.example.com:51004/oauth2redirect/example-provider", "https://dev.example.com/redirect"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// IPv4 loopback address not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateByAdmin(alphaCid,
|
||||||
|
Arrays.asList("https://127.0.0.1:8443", "https://app.example.com:51004/oauth2redirect/example-provider"));
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// wildcard context path not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateDynamically(alphaClientId,
|
||||||
|
List.of("https://dev.example.com:8443/*"));
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
Arrays.asList("https://app.example.com:51004/oauth2redirect/example-provider/update", "https://dev.example.com/redirect/update"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri not matched with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "https://app.example.com:51004/oauth2redirect/example-provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_IPv4LoopbackAddress() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->
|
||||||
|
it.setAllowIPv4LoopbackAddress(true)))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// IPv6 loopback address not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterDynamically(Arrays.asList("https://127.0.0.1/oauth2redirect/example-provider",
|
||||||
|
"http://[::1]/oauth2redirect/example-provider"));
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
Arrays.asList("https://app.example.com:51004/oauth2redirect/example-provider",
|
||||||
|
"https://127.0.0.1/oauth2redirect/example-provider"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// private use uri scheme not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateByAdmin(alphaCid, List.of("com.example.app:/oauth2redirect/example-provider"));
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
List.of("/auth/realms/master/app/auth", "https://dev.example.com/redirect/update"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// invalid uri form
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "https://keycloak.org\n");
|
||||||
|
|
||||||
|
// authorization request - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successAuthorizationRequest(alphaClientId, ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_IPv6LoopbackAddress() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->
|
||||||
|
it.setAllowIPv6LoopbackAddress(true)))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// IPv4 loopback address not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterDynamically(Arrays.asList("https://[::1]/", "https://localhost/auth/admin"));
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
Arrays.asList("https://[::1]/oauth2redirect/example-provider", "https://[::1]/"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// representation of IPv6 loopback address [0:0:0:0:0:0:0:1] not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateByAdmin(alphaCid, List.of("https://[0:0:0:0:0:0:0:1]/oauth2redirect/example-provider"));
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
List.of("https://[::1]/oauth2redirect/example-provider/update", "https://dev.example.com/redirect/update"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri parameter not match with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "http://[::1]:65522/oauth2redirect/example-provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_PrivateUseUriScheme() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->
|
||||||
|
it.setAllowPrivateUseUriScheme(true)))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// invalid uri form
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterByAdmin(List.of("com.example:"));
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
List.of("com.example.app:/oauth2redirect/example-provider",
|
||||||
|
"dev.com.example.app:/oauth2redirect/example-provider/dev"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// HTTP scheme not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateDynamically(alphaClientId,
|
||||||
|
Arrays.asList("com.example.app:/oauth2redirect/example-provider", "http://dev.example.com/redirect"));
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
Arrays.asList("com.example.app:/oauth2redirect/example-provider/update", "https://dev.example.com/redirect/update"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri not match with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "com.example.app:/oauth2redirect/example-provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_AllowHttpScheme() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->{
|
||||||
|
it.setAllowIPv4LoopbackAddress(true);
|
||||||
|
it.setAllowIPv6LoopbackAddress(true);
|
||||||
|
it.setAllowHttpScheme(true);}))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
Arrays.asList("http://localhost:8080/redirect", "http://dev.example.com/redirect/update"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
Arrays.asList("http://[::1]:8080/redirect", ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri not match with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "http://[::1]:8080/");
|
||||||
|
|
||||||
|
// authorization request - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successAuthorizationRequest(alphaClientId, ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_AllowWildcardContextPath() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->{
|
||||||
|
it.setAllowPrivateUseUriScheme(true);
|
||||||
|
it.setAllowIPv4LoopbackAddress(true);
|
||||||
|
it.setAllowIPv6LoopbackAddress(true);
|
||||||
|
it.setAllowHttpScheme(true);
|
||||||
|
it.setAllowWildcardContextPath(true);}))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
Arrays.asList("http://localhost:8080/*", "http://dev.example.com/redirect/update"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
Arrays.asList("http://[::1]:8080/*", ServerURLs.getAuthServerContextRoot() + "/*"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri not match with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "com.example.app:/oauth2redirect/example-provider");
|
||||||
|
|
||||||
|
// authorization request - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successAuthorizationRequest(alphaClientId, ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_AllowPermittedDomains() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->{
|
||||||
|
it.setAllowIPv4LoopbackAddress(true);
|
||||||
|
it.setAllowPermittedDomains(Arrays.asList(
|
||||||
|
"oauth.redirect", "((dev|test)-)*example.org", "localhost"));
|
||||||
|
it.setAllowHttpScheme(true);
|
||||||
|
it.setAllowWildcardContextPath(true);}))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// not match permitted domains
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterByAdmin(Arrays.asList("http://oauth.redirect/*", "http://dev.example.org/redirect"));
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(
|
||||||
|
Arrays.asList("http://oauth.redirect/*", "http://dev-example.org/redirect"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// not match permitted domains
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateDynamically(alphaClientId,
|
||||||
|
Arrays.asList("http://dev.oauth.redirect/*", "http://test-example.com/redirect"));
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
Arrays.asList("http://oauth.redirect/*", "http://dev-example.org/redirect", ServerURLs.getAuthServerContextRoot() + "/*"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri not match with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "http://dev-example.org/v2/redirect");
|
||||||
|
|
||||||
|
// authorization request - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successAuthorizationRequest(alphaClientId, ServerURLs.getAuthServerContextRoot() + "/auth/realms/master/app/auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_OAuth2_1Complient() throws Exception {
|
||||||
|
// register profiles
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->{
|
||||||
|
it.setAllowPrivateUseUriScheme(true);
|
||||||
|
it.setAllowIPv4LoopbackAddress(true);
|
||||||
|
it.setAllowIPv6LoopbackAddress(true);
|
||||||
|
it.setAllowHttpScheme(true);
|
||||||
|
it.setOAuth2_1Compliant(true);}))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - fail
|
||||||
|
// IPv4 loopback address with port number not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failRegisterByAdmin(Arrays.asList("http://127.0.0.1/auth/admin", "http://127.0.0.1:8080/auth/admin"));
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
List<String> registerResultList = testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(List.of("https://127.0.0.1/auth/admin"));
|
||||||
|
String alphaClientId = registerResultList.get(0);
|
||||||
|
String alphaCid = registerResultList.get(1);
|
||||||
|
|
||||||
|
// update - fail
|
||||||
|
// HTTP scheme not allowed
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failUpdateDynamically(alphaClientId,
|
||||||
|
List.of("https://127.0.0.1/auth/admin", "http://test-example.com/redirect"));
|
||||||
|
|
||||||
|
// update - success
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||||
|
List.of("https://[::1]/auth/admin", "com.example.app:/oauth2redirect/example-provider"));
|
||||||
|
|
||||||
|
// authorization request - fail
|
||||||
|
// redirect_uri not match with registered redirect uris
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(alphaClientId, "com.example.app:/oauth3redirect/example-provider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSecureRedirectUrisEnforcerExecutor_AllowOpenRedirect() throws Exception {
|
||||||
|
// Allow open redirect
|
||||||
|
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||||
|
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||||
|
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||||
|
createSecureRedirectUrisEnforcerExecutorConfig(it->{
|
||||||
|
it.setAllowOpenRedirect(true);
|
||||||
|
it.setOAuth2_1Compliant(true);}))
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updateProfiles(json);
|
||||||
|
|
||||||
|
// register policies
|
||||||
|
json = (new ClientPoliciesUtil.ClientPoliciesBuilder()).addPolicy(
|
||||||
|
(new ClientPoliciesUtil.ClientPolicyBuilder()).createPolicy(POLICY_NAME, "La Premiere Politique", Boolean.TRUE)
|
||||||
|
.addCondition(AnyClientConditionFactory.PROVIDER_ID,
|
||||||
|
createAnyClientConditionConfig())
|
||||||
|
.addProfile(PROFILE_NAME)
|
||||||
|
.toRepresentation()
|
||||||
|
).toString();
|
||||||
|
updatePolicies(json);
|
||||||
|
|
||||||
|
// register - success
|
||||||
|
// open redirect is allowed in any running mode
|
||||||
|
testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(List.of(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_failRegisterByAdmin(List<String> redirectUrisList) {
|
||||||
|
try {
|
||||||
|
createClientByAdmin(generateSuffixedName(CLIENT_NAME), (ClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setSecret("secret");
|
||||||
|
clientRep.setRedirectUris(redirectUrisList);
|
||||||
|
});
|
||||||
|
fail();
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
assertEquals(OAuthErrorException.INVALID_REQUEST, cpe.getError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_failRegisterDynamically(List<String> redirectUrisList) {
|
||||||
|
try {
|
||||||
|
createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) ->
|
||||||
|
clientRep.setRedirectUris(redirectUrisList));
|
||||||
|
} catch (ClientRegistrationException cre) {
|
||||||
|
assertEquals(ERR_MSG_CLIENT_REG_FAIL, cre.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(List<String> redirectUrisList) {
|
||||||
|
String alphaClientId = null;
|
||||||
|
String alphaCid = null;
|
||||||
|
try {
|
||||||
|
alphaClientId = generateSuffixedName(CLIENT_NAME);
|
||||||
|
alphaCid = createClientByAdmin(alphaClientId, (ClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setSecret("secret");
|
||||||
|
clientRep.setRedirectUris(redirectUrisList);
|
||||||
|
});
|
||||||
|
ClientRepresentation cRep = getClientByAdmin(alphaCid);
|
||||||
|
assertEquals(new HashSet<>(redirectUrisList), new HashSet<>(cRep.getRedirectUris()));
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
return Arrays.asList(alphaClientId, alphaCid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_failUpdateByAdmin(String cId, List<String> redirectUrisList) {
|
||||||
|
try {
|
||||||
|
updateClientByAdmin(cId, (ClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setAttributes(new HashMap<>());
|
||||||
|
clientRep.setRedirectUris(redirectUrisList);
|
||||||
|
});
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
assertEquals(Errors.INVALID_REQUEST, cpe.getError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_failUpdateDynamically(String clientId, List<String> redirectUrisList) {
|
||||||
|
try {
|
||||||
|
updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) ->
|
||||||
|
clientRep.setRedirectUris(redirectUrisList));
|
||||||
|
fail();
|
||||||
|
} catch (ClientRegistrationException e) {
|
||||||
|
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(String cId, List<String> redirectUrisList) {
|
||||||
|
try {
|
||||||
|
updateClientByAdmin(cId, (ClientRepresentation clientRep) -> {
|
||||||
|
clientRep.setAttributes(new HashMap<>());
|
||||||
|
clientRep.setRedirectUris(redirectUrisList);
|
||||||
|
});
|
||||||
|
ClientRepresentation cRep = getClientByAdmin(cId);
|
||||||
|
assertEquals(new HashSet<>(redirectUrisList), new HashSet<>(cRep.getRedirectUris()));
|
||||||
|
} catch (ClientPolicyException cpe) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_failAuthorizationRequest(String clientId, String redirectUri) {
|
||||||
|
oauth.clientId(clientId);
|
||||||
|
oauth.redirectUri(redirectUri);
|
||||||
|
oauth.openLoginForm();
|
||||||
|
assertTrue(errorPage.isCurrent());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSecureRedirectUrisEnforcerExecutor_successAuthorizationRequest(String clientId, String redirectUri) {
|
||||||
|
oauth.clientId(clientId);
|
||||||
|
oauth.redirectUri(redirectUri);
|
||||||
|
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
||||||
|
Assert.assertNotNull(response.getCode());
|
||||||
|
OAuthClient.AccessTokenResponse res = oauth.doAccessTokenRequest(response.getCode(), "secret");
|
||||||
|
assertEquals(200, res.getStatusCode());
|
||||||
|
oauth.doLogout(res.getRefreshToken(), "secret");
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,11 +63,11 @@ import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutor;
|
import org.keycloak.services.clientpolicy.executor.RejectResourceOwnerPasswordCredentialsGrantExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutor;
|
import org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutor;
|
import org.keycloak.services.clientpolicy.executor.SecureClientAuthenticatorExecutor;
|
||||||
|
import org.keycloak.services.clientpolicy.executor.SecureRedirectUrisEnforcerExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
|
import org.keycloak.services.clientpolicy.executor.SecureRequestObjectExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutor;
|
import org.keycloak.services.clientpolicy.executor.SecureResponseTypeExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmExecutor;
|
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmExecutor;
|
||||||
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutor;
|
import org.keycloak.services.clientpolicy.executor.SecureSigningAlgorithmForSignedJwtExecutor;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
|
||||||
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExceptionCondition;
|
import org.keycloak.testsuite.services.clientpolicy.condition.TestRaiseExceptionCondition;
|
||||||
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutor;
|
import org.keycloak.testsuite.services.clientpolicy.executor.TestRaiseExceptionExecutor;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
@ -85,6 +85,7 @@ import java.security.interfaces.RSAPublicKey;
|
||||||
import java.security.spec.ECGenParameterSpec;
|
import java.security.spec.ECGenParameterSpec;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
|
import static org.keycloak.jose.jwk.JWKUtil.toIntegerBytes;
|
||||||
|
@ -271,6 +272,15 @@ public final class ClientPoliciesUtil {
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static SecureRedirectUrisEnforcerExecutor.Configuration createSecureRedirectUrisEnforcerExecutorConfig(
|
||||||
|
Consumer<SecureRedirectUrisEnforcerExecutor.Configuration> apply) {
|
||||||
|
SecureRedirectUrisEnforcerExecutor.Configuration config = new SecureRedirectUrisEnforcerExecutor.Configuration();
|
||||||
|
if (apply != null) {
|
||||||
|
apply.accept(config);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
public static class ClientPoliciesBuilder {
|
public static class ClientPoliciesBuilder {
|
||||||
private final ClientPoliciesRepresentation policiesRep;
|
private final ClientPoliciesRepresentation policiesRep;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue