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:
parent
b38b1eb782
commit
624d300a55
2 changed files with 218 additions and 24 deletions
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue