SecureRedirectUrisEnforcerExecutor fixes (#27369)
closes #27344 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
5719d71dd4
commit
ae0a0ea30b
2 changed files with 132 additions and 14 deletions
|
@ -17,20 +17,26 @@
|
|||
|
||||
package org.keycloak.services.clientpolicy.executor;
|
||||
|
||||
import java.net.*;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.UnknownHostException;
|
||||
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.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
import org.keycloak.representations.idm.ClientPolicyExecutorConfigurationRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyContext;
|
||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||
import org.keycloak.services.clientpolicy.context.AdminClientRegisterContext;
|
||||
|
@ -167,22 +173,26 @@ public class SecureRedirectUrisEnforcerExecutor implements ClientPolicyExecutorP
|
|||
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) {
|
||||
ClientRepresentation client = ((ClientCRUDContext)context).getProposedClientRepresentation();
|
||||
List<String> redirectUris = client.getRedirectUris();
|
||||
if (redirectUris == null || redirectUris.isEmpty()) {
|
||||
throw invalidRedirectUri(ERR_GENERAL);
|
||||
}
|
||||
verifyRedirectUris(((ClientCRUDContext)context).getAuthenticatedClient().getRootUrl(), redirectUris);
|
||||
verifyRedirectUris(client.getRootUrl(), redirectUris);
|
||||
verifyPostLogoutRedirectUriUpdate(client);
|
||||
} 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) {
|
||||
ClientRepresentation client = ((ClientCRUDContext)context).getProposedClientRepresentation();
|
||||
List<String> redirectUris = client.getRedirectUris();
|
||||
if (redirectUris == null || redirectUris.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
verifyRedirectUris(((ClientCRUDContext)context).getAuthenticatedClient().getRootUrl(), redirectUris);
|
||||
verifyRedirectUris(client.getRootUrl(), redirectUris);
|
||||
verifyPostLogoutRedirectUriUpdate(client);
|
||||
} else {
|
||||
throw invalidRedirectUri(ERR_GENERAL);
|
||||
}
|
||||
|
@ -201,6 +211,15 @@ public class SecureRedirectUrisEnforcerExecutor implements ClientPolicyExecutorP
|
|||
}
|
||||
}
|
||||
|
||||
private void verifyPostLogoutRedirectUriUpdate(ClientRepresentation client) throws ClientPolicyException {
|
||||
List<String> postLogoutRedirectUris = OIDCAdvancedConfigWrapper.fromClientRepresentation(client).getPostLogoutRedirectUris();
|
||||
if (postLogoutRedirectUris == null || postLogoutRedirectUris.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
logger.tracef("Verifying post-logout redirect uris. Target client: %s, Effective post-logout uris: %s", client.getClientId(), postLogoutRedirectUris);
|
||||
verifyRedirectUris(client.getRootUrl(), postLogoutRedirectUris);
|
||||
}
|
||||
|
||||
void verifyRedirectUris(String rootUri, List<String> redirectUris) throws ClientPolicyException {
|
||||
// open redirect allows any value for redirect uri
|
||||
if (configuration.isAllowOpenRedirect()) {
|
||||
|
@ -294,8 +313,8 @@ public class SecureRedirectUrisEnforcerExecutor implements ClientPolicyExecutorP
|
|||
|
||||
InetAddress addr;
|
||||
try {
|
||||
addr = InetAddresses.forUriString(uri.getHost());
|
||||
} catch (IllegalArgumentException e) {
|
||||
addr = InetAddress.getByName(uri.getHost());
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,6 @@ 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;
|
||||
|
@ -162,8 +161,11 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
String json = (new ClientPoliciesUtil.ClientProfilesBuilder()).addProfile(
|
||||
(new ClientPoliciesUtil.ClientProfileBuilder()).createProfile(PROFILE_NAME, "Le Premier Profil")
|
||||
.addExecutor(SecureRedirectUrisEnforcerExecutorFactory.PROVIDER_ID,
|
||||
createSecureRedirectUrisEnforcerExecutorConfig(it->
|
||||
it.setAllowIPv6LoopbackAddress(true)))
|
||||
createSecureRedirectUrisEnforcerExecutorConfig(it-> {
|
||||
it.setOAuth2_1Compliant(true);
|
||||
it.setAllowIPv6LoopbackAddress(true);
|
||||
})
|
||||
)
|
||||
.toRepresentation()
|
||||
).toString();
|
||||
updateProfiles(json);
|
||||
|
@ -180,6 +182,10 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
|
||||
// register - fail
|
||||
// IPv4 loopback address not allowed
|
||||
testSecureRedirectUrisEnforcerExecutor_failRegisterDynamically(Arrays.asList("https://[::1]/", "https://127.0.0.1/auth/admin"));
|
||||
|
||||
// register - fail
|
||||
// IPv4 loopback address not allowed (even when "localhost" is used)
|
||||
testSecureRedirectUrisEnforcerExecutor_failRegisterDynamically(Arrays.asList("https://[::1]/", "https://localhost/auth/admin"));
|
||||
|
||||
// register - success
|
||||
|
@ -189,7 +195,7 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
String alphaCid = registerResultList.get(1);
|
||||
|
||||
// update - fail
|
||||
// representation of IPv6 loopback address [0:0:0:0:0:0:0:1] not allowed
|
||||
// representation of IPv6 loopback address [0:0:0:0:0:0:0:1] not allowed with OAuth 2.1 mode enabled
|
||||
testSecureRedirectUrisEnforcerExecutor_failUpdateByAdmin(alphaCid, List.of("https://[0:0:0:0:0:0:0:1]/oauth2redirect/example-provider"));
|
||||
|
||||
// update - success
|
||||
|
@ -375,6 +381,27 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
testSecureRedirectUrisEnforcerExecutor_failUpdateDynamically(alphaClientId,
|
||||
Arrays.asList("http://dev.oauth.redirect/*", "http://test-example.com/redirect"));
|
||||
|
||||
// update using rootUrl - fail
|
||||
try {
|
||||
updateClientByAdmin(alphaCid, (ClientRepresentation clientRep) -> {
|
||||
clientRep.setAttributes(new HashMap<>());
|
||||
clientRep.setRootUrl("http://incorrect.org");
|
||||
clientRep.setRedirectUris(List.of("/redirect"));
|
||||
});
|
||||
fail("Expected to fail when updating clientId " + alphaCid + " with redirectUris: '/redirect'");
|
||||
} catch (ClientPolicyException cpe) {
|
||||
assertEquals(Errors.INVALID_REQUEST, cpe.getError());
|
||||
}
|
||||
|
||||
// update using rootUrl - success
|
||||
updateClientByAdmin(alphaCid, (ClientRepresentation clientRep) -> {
|
||||
clientRep.setAttributes(new HashMap<>());
|
||||
clientRep.setRootUrl("http://dev-example.org");
|
||||
clientRep.setRedirectUris(List.of("/redirect"));
|
||||
});
|
||||
ClientRepresentation cRep = getClientByAdmin(alphaCid);
|
||||
assertEquals(List.of("/redirect"), cRep.getRedirectUris());
|
||||
|
||||
// update - success
|
||||
testSecureRedirectUrisEnforcerExecutor_successUpdateByAdmin(alphaCid,
|
||||
Arrays.asList("http://oauth.redirect/*", "http://dev-example.org/redirect", ServerURLs.getAuthServerContextRoot() + "/*"));
|
||||
|
@ -464,6 +491,63 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(List.of(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSecureRedirectUrisEnforcerExecutor_postLogoutRedirectUris() 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.setAllowPermittedDomains(List.of("oauth.redirect"));
|
||||
})
|
||||
)
|
||||
.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);
|
||||
|
||||
// Success - register without post-logout redirect uris
|
||||
String clientId = testSecureRedirectUrisEnforcerExecutor_successRegisterDynamically(List.of("https://oauth.redirect/something"));
|
||||
|
||||
// Success - update with post-logout redirect uris as "+"
|
||||
updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) -> {
|
||||
clientRep.setRedirectUris(List.of("https://oauth.redirect/some"));
|
||||
clientRep.setPostLogoutRedirectUris(List.of("+"));
|
||||
});
|
||||
OIDCClientRepresentation clientRepp = reg.oidc().get(clientId);
|
||||
Assert.assertEquals(List.of("https://oauth.redirect/some"), clientRepp.getRedirectUris());
|
||||
Assert.assertEquals(List.of("https://oauth.redirect/some"), clientRepp.getPostLogoutRedirectUris());
|
||||
|
||||
// Fail - incorrect domain for post-logout redirect uri
|
||||
try {
|
||||
updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) -> {
|
||||
clientRep.setRedirectUris(List.of("https://oauth.redirect/some"));
|
||||
clientRep.setPostLogoutRedirectUris(List.of("https://incorrect.domain/some"));
|
||||
});
|
||||
fail();
|
||||
} catch (ClientRegistrationException e) {
|
||||
assertEquals(ERR_MSG_CLIENT_REG_FAIL, e.getMessage());
|
||||
}
|
||||
|
||||
// Success - update with post-logout redirect uris as "+"
|
||||
updateClientDynamically(clientId, (OIDCClientRepresentation clientRep) -> {
|
||||
clientRep.setRedirectUris(List.of("https://oauth.redirect/some"));
|
||||
clientRep.setPostLogoutRedirectUris(List.of("https://oauth.redirect/some-post-logout"));
|
||||
});
|
||||
clientRepp = reg.oidc().get(clientId);
|
||||
Assert.assertEquals(List.of("https://oauth.redirect/some"), clientRepp.getRedirectUris());
|
||||
Assert.assertEquals(List.of("https://oauth.redirect/some-post-logout"), clientRepp.getPostLogoutRedirectUris());
|
||||
}
|
||||
|
||||
|
||||
private void testSecureRedirectUrisEnforcerExecutor_failRegisterByAdmin(List<String> redirectUrisList) {
|
||||
try {
|
||||
|
@ -481,11 +565,13 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
try {
|
||||
createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) ->
|
||||
clientRep.setRedirectUris(redirectUrisList));
|
||||
fail("Expected to fail with redirectUris: " +redirectUrisList);
|
||||
} catch (ClientRegistrationException cre) {
|
||||
assertEquals(ERR_MSG_CLIENT_REG_FAIL, cre.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// First item in the list is clientId. Second item is clientUUID (DB entity ID)
|
||||
private List<String> testSecureRedirectUrisEnforcerExecutor_successRegisterByAdmin(List<String> redirectUrisList) {
|
||||
String alphaClientId = null;
|
||||
String alphaCid = null;
|
||||
|
@ -503,12 +589,25 @@ public class SecureRedirectUrisEnforcerExecutorTest extends AbstractClientPolici
|
|||
return Arrays.asList(alphaClientId, alphaCid);
|
||||
}
|
||||
|
||||
// Return clientId (not DB UUID)
|
||||
private String testSecureRedirectUrisEnforcerExecutor_successRegisterDynamically(List<String> redirectUrisList) {
|
||||
try {
|
||||
return createClientDynamically(generateSuffixedName(CLIENT_NAME), (OIDCClientRepresentation clientRep) ->
|
||||
clientRep.setRedirectUris(redirectUrisList));
|
||||
} catch (ClientRegistrationException cre) {
|
||||
fail("Did not expected to fail when dynamically registering client with redirectUris: " + redirectUrisList);
|
||||
// Should not be here
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void testSecureRedirectUrisEnforcerExecutor_failUpdateByAdmin(String cId, List<String> redirectUrisList) {
|
||||
try {
|
||||
updateClientByAdmin(cId, (ClientRepresentation clientRep) -> {
|
||||
clientRep.setAttributes(new HashMap<>());
|
||||
clientRep.setRedirectUris(redirectUrisList);
|
||||
});
|
||||
fail("Expected to failwhen updating clientId " + cId + " with redirectUris: " +redirectUrisList);
|
||||
} catch (ClientPolicyException cpe) {
|
||||
assertEquals(Errors.INVALID_REQUEST, cpe.getError());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue