KEYCLOAK-17938 Not possible to create client in the admin console when client policy with "secure-redirecturi-enforce-executor" condition is used

This commit is contained in:
Takashi Norimatsu 2021-05-06 07:10:05 +09:00 committed by Marek Posolda
parent b38b1eb782
commit 624d300a55
2 changed files with 218 additions and 24 deletions

View file

@ -18,11 +18,16 @@
package org.keycloak.services.clientpolicy.executor;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.jboss.logging.Logger;
import org.keycloak.OAuthErrorException;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
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;
@ -55,37 +60,88 @@ public class SecureRedirectUriEnforceExecutor implements ClientPolicyExecutorPro
switch (context.getEvent()) {
case REGISTER:
if (context instanceof AdminClientRegisterContext || context instanceof DynamicClientRegisterContext) {
confirmSecureRedirectUris(((ClientCRUDContext)context).getProposedClientRepresentation().getRedirectUris());
confirmSecureUris(((ClientCRUDContext)context).getProposedClientRepresentation());
} else {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not allowed input format.");
}
return;
case UPDATE:
if (context instanceof AdminClientUpdateContext || context instanceof DynamicClientUpdateContext) {
confirmSecureRedirectUris(((ClientCRUDContext)context).getProposedClientRepresentation().getRedirectUris());
confirmSecureUris(((ClientCRUDContext)context).getProposedClientRepresentation());
} else {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "not allowed input format.");
}
return;
case AUTHORIZATION_REQUEST:
confirmSecureRedirectUris(Arrays.asList(((AuthorizationRequestContext)context).getRedirectUri()));
confirmSecureRedirectUri(((AuthorizationRequestContext)context).getRedirectUri());
return;
default:
return;
}
}
private void confirmSecureRedirectUris(List<String> redirectUris) throws ClientPolicyException {
if (redirectUris == null || redirectUris.isEmpty()) {
throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT_METADATA, "Invalid client metadata: redirect_uris");
private void confirmSecureUris(ClientRepresentation clientRep) throws ClientPolicyException {
// rootUrl
String rootUrl = clientRep.getRootUrl();
if (rootUrl != null) confirmSecureUris(Arrays.asList(rootUrl), "rootUrl");
// adminUrl
String adminUrl = clientRep.getAdminUrl();
if (adminUrl != null) confirmSecureUris(Arrays.asList(adminUrl), "adminUrl");
// baseUrl
String baseUrl = clientRep.getBaseUrl();
if (baseUrl != null) confirmSecureUris(Arrays.asList(baseUrl), "baseUrl");
// web origins
List<String> webOrigins = clientRep.getWebOrigins();
if (webOrigins != null) confirmSecureUris(webOrigins, "webOrigins");
// backchannel logout URL
String logoutUrl = Optional.ofNullable(clientRep.getAttributes()).orElse(Collections.emptyMap()).get(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL);
if (logoutUrl != null) confirmSecureUris(Arrays.asList(logoutUrl), "logoutUrl");
// OAuth2 : redirectUris
List<String> redirectUris = clientRep.getRedirectUris();
if (redirectUris != null) confirmSecureUris(redirectUris, "redirectUris");
// OAuth2 : jwks_uri
String jwksUri = Optional.ofNullable(clientRep.getAttributes()).orElse(Collections.emptyMap()).get(OIDCConfigAttributes.JWKS_URL);
if (jwksUri != null) confirmSecureUris(Arrays.asList(jwksUri), "jwksUri");
// OIDD : requestUris
List<String> requestUris = getAttributeMultivalued(clientRep, OIDCConfigAttributes.REQUEST_URIS);
if (requestUris != null) confirmSecureUris(requestUris, "requestUris");
}
private List<String> getAttributeMultivalued(ClientRepresentation clientRep, String attrKey) {
String attrValue = Optional.ofNullable(clientRep.getAttributes()).orElse(Collections.emptyMap()).get(attrKey);
if (attrValue == null) return Collections.emptyList();
return Arrays.asList(Constants.CFG_DELIMITER_PATTERN.split(attrValue));
}
private void confirmSecureUris(List<String> uris, String uriType) throws ClientPolicyException {
if (uris == null || uris.isEmpty()) {
return;
}
for(String redirectUri : redirectUris) {
logger.tracev("Redirect URI = {0}", redirectUri);
if (redirectUri.startsWith("http://") || redirectUri.contains("*")) {
throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT_METADATA, "Invalid client metadata: redirect_uris");
for (String uri : uris) {
logger.tracev("{0} = {1}", uriType, uri);
if (!uri.startsWith("https://") || uri.contains("*")) {
throw new ClientPolicyException(OAuthErrorException.INVALID_CLIENT_METADATA, "Invalid " + uriType);
}
}
}
private void confirmSecureRedirectUri(String redirectUri) throws ClientPolicyException {
if (redirectUri == null || redirectUri.isEmpty()) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "no redirect_uri specified.");
}
logger.tracev("Redirect URI = {0}", redirectUri);
if (!redirectUri.startsWith("https://") || redirectUri.contains("*")) {
throw new ClientPolicyException(OAuthErrorException.INVALID_REQUEST, "Invalid redirect_uri");
}
}
}

View file

@ -32,7 +32,8 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.Map;
import java.util.Optional;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.CloseableHttpResponse;
@ -1310,23 +1311,160 @@ public class ClientPoliciesTest extends AbstractClientPoliciesTest {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
// update policies
json = (new ClientPoliciesBuilder()).addPolicy(
(new ClientPolicyBuilder()).createPolicy(POLICY_NAME, "Paivitetyn Ensimmaisen Politiikka", Boolean.FALSE, Boolean.TRUE, null, null)
.addCondition(ClientUpdateContextConditionFactory.PROVIDER_ID,
createClientUpdateContextConditionConfig(Arrays.asList(
ClientUpdateContextConditionFactory.BY_AUTHENTICATED_USER,
ClientUpdateContextConditionFactory.BY_REGISTRATION_ACCESS_TOKEN)))
.addProfile(PROFILE_NAME)
.toRepresentation()
).toString();
updatePolicies(json);
String cid = null;
String clientId = generateSuffixedName(CLIENT_NAME);
try {
createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) -> {});
cid = createClientByAdmin(clientId, (ClientRepresentation clientRep) -> {
clientRep.setServiceAccountsEnabled(Boolean.TRUE);
clientRep.setRedirectUris(null);
});
} catch (Exception e) {
fail();
}
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
clientRep.setRedirectUris(null);
clientRep.setServiceAccountsEnabled(Boolean.FALSE);
});
assertEquals(false, getClientByAdmin(cid).isServiceAccountsEnabled());
try {
updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) -> {
clientRep.setRedirectUris(Collections.singletonList("https://newredirect/*"));
});
fail();
} catch (ClientRegistrationException e) {
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// rootUrl
clientRep.setRootUrl("https://client.example.com/");
// adminUrl
clientRep.setAdminUrl("https://client.example.com/admin/");
// baseUrl
clientRep.setBaseUrl("https://client.example.com/base/");
// web origins
clientRep.setWebOrigins(Arrays.asList("https://valid.other.client.example.com/", "https://valid.another.client.example.com/"));
// backchannel logout URL
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, "https://client.example.com/logout/");
clientRep.setAttributes(attributes);
// OAuth2 : redirectUris
clientRep.setRedirectUris(Arrays.asList("https://client.example.com/redirect/", "https://client.example.com/callback/"));
// OAuth2 : jwks_uri
attributes.put(OIDCConfigAttributes.JWKS_URL, "https://client.example.com/jwks/");
clientRep.setAttributes(attributes);
// OIDD : requestUris
setAttributeMultivalued(clientRep, OIDCConfigAttributes.REQUEST_URIS, Arrays.asList("https://client.example.com/request/", "https://client.example.com/reqobj/"));
});
} catch (Exception e) {
fail();
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// rootUrl
clientRep.setRootUrl("http://client.example.com/*/");
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid rootUrl", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// adminUrl
clientRep.setAdminUrl("http://client.example.com/admin/");
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid adminUrl", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// baseUrl
clientRep.setBaseUrl("https://client.example.com/base/*");
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid baseUrl", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// web origins
clientRep.setWebOrigins(Arrays.asList("http://valid.another.client.example.com/"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid webOrigins", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// backchannel logout URL
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.put(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, "httpss://client.example.com/logout/");
clientRep.setAttributes(attributes);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid logoutUrl", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// OAuth2 : redirectUris
clientRep.setRedirectUris(Arrays.asList("https://client.example.com/redirect/", "ftp://client.example.com/callback/"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid redirectUris", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// OAuth2 : jwks_uri
Map<String, String> attributes = Optional.ofNullable(clientRep.getAttributes()).orElse(new HashMap<>());
attributes.put(OIDCConfigAttributes.JWKS_URL, "http s://client.example.com/jwks/");
clientRep.setAttributes(attributes);
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid jwksUri", e.getErrorDetail());
}
try {
updateClientByAdmin(cid, (ClientRepresentation clientRep) -> {
// OIDD : requestUris
setAttributeMultivalued(clientRep, OIDCConfigAttributes.REQUEST_URIS, Arrays.asList("https://client.example.com/request/*", "https://client.example.com/reqobj/"));
});
fail();
} catch (ClientPolicyException e) {
assertEquals(OAuthErrorException.INVALID_CLIENT_METADATA, e.getError());
assertEquals("Invalid requestUris", e.getErrorDetail());
}
}
private List<String> getAttributeMultivalued(ClientRepresentation clientRep, String attrKey) {
String attrValue = Optional.ofNullable(clientRep.getAttributes()).orElse(Collections.emptyMap()).get(attrKey);
if (attrValue == null) return Collections.emptyList();
return Arrays.asList(Constants.CFG_DELIMITER_PATTERN.split(attrValue));
}
private void setAttributeMultivalued(ClientRepresentation clientRep, String attrKey, List<String> attrValues) {
String attrValueFull = String.join(Constants.CFG_DELIMITER, attrValues);
clientRep.getAttributes().put(attrKey, attrValueFull);
}
@Test