mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-28 04:13:22 -04:00
Verify resource indicator syntax in authz and token endpoint (#47438)
Closes #47116, closes #47119 Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
parent
02be20e9fa
commit
af942df712
9 changed files with 139 additions and 16 deletions
|
|
@ -187,6 +187,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
checker.checkInvalidRequestMessage();
|
||||
checker.checkOIDCRequest();
|
||||
checker.checkValidScope();
|
||||
checker.checkValidResource();
|
||||
checker.checkOIDCParams();
|
||||
checker.checkPKCEParams();
|
||||
} catch (AuthorizationEndpointChecker.AuthorizationCheckException ex) {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import org.keycloak.protocol.oidc.TokenManager;
|
|||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
|
||||
import org.keycloak.protocol.oidc.endpoints.request.RequestUriType;
|
||||
import org.keycloak.protocol.oidc.resourceindicators.ResourceIndicatorConstants;
|
||||
import org.keycloak.protocol.oidc.resourceindicators.ResourceIndicatorValidation;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
|
|
@ -242,6 +244,16 @@ public class AuthorizationEndpointChecker {
|
|||
}
|
||||
}
|
||||
|
||||
public void checkValidResource() throws AuthorizationCheckException {
|
||||
if (!ResourceIndicatorValidation.isValidResourceIndicator(request.getResource())) {
|
||||
ServicesLogger.LOGGER.invalidParameter(OIDCLoginProtocol.SCOPE_PARAM);
|
||||
String errorMessage = "Invalid resource: " + request.getResource();
|
||||
event.detail(Details.REASON, errorMessage);
|
||||
event.error(Errors.INVALID_REQUEST);
|
||||
throw new AuthorizationCheckException(Response.Status.BAD_REQUEST, OAuthErrorException.INVALID_TARGET, ResourceIndicatorConstants.ERROR_INVALID_RESOURCE);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkOIDCParams() throws AuthorizationCheckException {
|
||||
// If request is not OIDC request, but pure OAuth2 request and response_type is just 'token', then 'nonce' is not mandatory
|
||||
boolean isOIDCRequest = TokenUtil.isOIDCRequest(request.getScope());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package org.keycloak.protocol.oidc.resourceindicators;
|
||||
|
||||
public interface ResourceIndicatorConstants {
|
||||
String ERROR_NOT_MATCHING = "The requested resource is not matching the original request.";
|
||||
String ERROR_INVALID_RESOURCE = "The requested resource is invalid, missing, unknown, or malformed.";
|
||||
String URN_CLIENT_PREFIX = "urn:client:";
|
||||
String CLIENT_RESOURCE_URL_ATTRIBUTE = "resource_url";
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package org.keycloak.protocol.oidc.resourceindicators;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ResourceIndicatorValidation {
|
||||
|
||||
private static final Pattern URN_REGEX = Pattern.compile("^urn:[a-z0-9][a-z0-9-]{0,31}:([a-z0-9()+,-.:=@;$_!*']|%[0-9a-f]{2})++$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private ResourceIndicatorValidation() {
|
||||
}
|
||||
|
||||
public static boolean isValidResourceIndicator(String resourceIndicator) {
|
||||
if (resourceIndicator == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = new URI(resourceIndicator);
|
||||
if ("urn".equalsIgnoreCase(uri.getScheme())) {
|
||||
return URN_REGEX.matcher(resourceIndicator).matches();
|
||||
} else {
|
||||
if (!uri.isAbsolute()) {
|
||||
return false;
|
||||
} else if (uri.getFragment() != null) {
|
||||
return false;
|
||||
} else if (uri.getQuery() != null) {
|
||||
return false;
|
||||
} else if (uri.getPath() == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
package org.keycloak.protocol.oidc.token;
|
||||
package org.keycloak.protocol.oidc.resourceindicators;
|
||||
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.token.TokenInterceptorException;
|
||||
import org.keycloak.protocol.oidc.token.TokenPostProcessor;
|
||||
import org.keycloak.protocol.oidc.token.TokenPostProcessorContext;
|
||||
|
||||
public class ResourceIndicatorsPostProcessor implements TokenPostProcessor {
|
||||
|
||||
public static final String ERROR_NOT_MATCHING = "The requested resource is not matching the original request.";
|
||||
public static final String ERROR_INVALID_RESOURCE = "The requested resource is invalid, missing, unknown, or malformed.";
|
||||
public static final String URN_CLIENT_PREFIX = "urn:client:";
|
||||
public static final String CLIENT_RESOURCE_URL_ATTRIBUTE = "resource_url";
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public ResourceIndicatorsPostProcessor(KeycloakSession session) {
|
||||
|
|
@ -22,6 +20,10 @@ public class ResourceIndicatorsPostProcessor implements TokenPostProcessor {
|
|||
@Override
|
||||
public void process(TokenPostProcessorContext context) {
|
||||
String requestedResource = context.clientSessionCtx().getAttribute(OAuth2Constants.RESOURCE, String.class);
|
||||
if (requestedResource != null && !ResourceIndicatorValidation.isValidResourceIndicator(requestedResource)) {
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ResourceIndicatorConstants.ERROR_INVALID_RESOURCE);
|
||||
}
|
||||
|
||||
String grantType = context.clientSessionCtx().getAttribute(Constants.GRANT_TYPE, String.class);
|
||||
|
||||
boolean originalResourceParamRequired = false;
|
||||
|
|
@ -40,13 +42,13 @@ public class ResourceIndicatorsPostProcessor implements TokenPostProcessor {
|
|||
|
||||
if (originalResourceParamRequired) {
|
||||
if (originalResourceParam == null) {
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ERROR_NOT_MATCHING);
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ResourceIndicatorConstants.ERROR_NOT_MATCHING);
|
||||
}
|
||||
|
||||
if (requestedResource == null) {
|
||||
requestedResource = originalResourceParam;
|
||||
} else if (!requestedResource.equals(originalResourceParam)){
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ERROR_NOT_MATCHING);
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ResourceIndicatorConstants.ERROR_NOT_MATCHING);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -58,7 +60,7 @@ public class ResourceIndicatorsPostProcessor implements TokenPostProcessor {
|
|||
}
|
||||
|
||||
if (audienceToSet == null) {
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ERROR_INVALID_RESOURCE);
|
||||
throw new TokenInterceptorException(OAuthErrorException.INVALID_TARGET, ResourceIndicatorConstants.ERROR_INVALID_RESOURCE);
|
||||
}
|
||||
|
||||
context.refreshToken().getOtherClaims().put(OAuth2Constants.RESOURCE, requestedResource);
|
||||
|
|
@ -66,11 +68,11 @@ public class ResourceIndicatorsPostProcessor implements TokenPostProcessor {
|
|||
}
|
||||
|
||||
private boolean isClientUrn(String resource) {
|
||||
return resource.startsWith(URN_CLIENT_PREFIX);
|
||||
return resource.startsWith(ResourceIndicatorConstants.URN_CLIENT_PREFIX);
|
||||
}
|
||||
|
||||
private String findAudienceByClientUrn(String resource, String[] audience) {
|
||||
String requestedClientId = resource.substring(URN_CLIENT_PREFIX.length());
|
||||
String requestedClientId = resource.substring(ResourceIndicatorConstants.URN_CLIENT_PREFIX.length());
|
||||
return find(requestedClientId, audience);
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +80,7 @@ public class ResourceIndicatorsPostProcessor implements TokenPostProcessor {
|
|||
for (String a : audience) {
|
||||
ClientModel client = session.clients().getClientByClientId(session.getContext().getRealm(), a);
|
||||
if (client != null) {
|
||||
String clientResourceUrl = client.getAttribute(CLIENT_RESOURCE_URL_ATTRIBUTE);
|
||||
String clientResourceUrl = client.getAttribute(ResourceIndicatorConstants.CLIENT_RESOURCE_URL_ATTRIBUTE);
|
||||
if (resource.equals(clientResourceUrl)) {
|
||||
return resource;
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
package org.keycloak.protocol.oidc.token;
|
||||
package org.keycloak.protocol.oidc.resourceindicators;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.token.TokenPostProcessor;
|
||||
import org.keycloak.protocol.oidc.token.TokenPostProcessorFactory;
|
||||
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||
|
||||
public class ResourceIndicatorsPostProcessorFactory implements TokenPostProcessorFactory, EnvironmentDependentProviderFactory {
|
||||
|
|
@ -1 +1 @@
|
|||
org.keycloak.protocol.oidc.token.ResourceIndicatorsPostProcessorFactory
|
||||
org.keycloak.protocol.oidc.resourceindicators.ResourceIndicatorsPostProcessorFactory
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package org.keycloak.protocol.oidc.resourceindicators;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class ResourceIndicatorValidationTest {
|
||||
|
||||
@Test
|
||||
public void testValidResourceIndicatorUrns() {
|
||||
assertValid("urn:client:something");
|
||||
assertValid("urn:client:asdfasdfs23_asdfasefr43_asdf34f43-asdf34avdrvdr");
|
||||
assertValid("urn:something:something");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidResourceIndicatorUrls() {
|
||||
assertValid("https://something");
|
||||
assertValid("https://something:8080");
|
||||
assertValid("https://something:8080/something");
|
||||
assertValid("https://something/something");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidResourceIndicatorUrns() {
|
||||
assertInvalid("urn:client:something#something");
|
||||
assertInvalid("urn:client:something?foo=bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidResourceIndicatorUrls() {
|
||||
assertInvalid("https://something#something");
|
||||
assertInvalid("https://something?something");
|
||||
assertInvalid("/something");
|
||||
}
|
||||
|
||||
private void assertValid(String str) {
|
||||
Assert.assertTrue(ResourceIndicatorValidation.isValidResourceIndicator(str));
|
||||
}
|
||||
|
||||
private void assertInvalid(String str) {
|
||||
Assert.assertFalse(ResourceIndicatorValidation.isValidResourceIndicator(str));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,8 +23,8 @@ import org.junit.jupiter.api.Assertions;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.OAuthErrorException.INVALID_TARGET;
|
||||
import static org.keycloak.protocol.oidc.token.ResourceIndicatorsPostProcessor.ERROR_INVALID_RESOURCE;
|
||||
import static org.keycloak.protocol.oidc.token.ResourceIndicatorsPostProcessor.ERROR_NOT_MATCHING;
|
||||
import static org.keycloak.protocol.oidc.resourceindicators.ResourceIndicatorConstants.ERROR_INVALID_RESOURCE;
|
||||
import static org.keycloak.protocol.oidc.resourceindicators.ResourceIndicatorConstants.ERROR_NOT_MATCHING;
|
||||
|
||||
@KeycloakIntegrationTest(config = ResourceIndicatorsTest.ResourceIndicatorServerConfig.class)
|
||||
public class ResourceIndicatorsTest {
|
||||
|
|
@ -65,6 +65,20 @@ public class ResourceIndicatorsTest {
|
|||
assertErrorResponse(tokenResponse, INVALID_TARGET, ERROR_INVALID_RESOURCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidResourceSyntax() {
|
||||
AccessTokenResponse tokenResponse = oauth.passwordGrantRequest("user", "pass").resource("/theservice2").send();
|
||||
assertErrorResponse(tokenResponse, INVALID_TARGET, ERROR_INVALID_RESOURCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthzInvalidResourceParam() {
|
||||
AuthorizationEndpointResponse authorizationEndpointResponse = oauth.loginForm().resource("/invalid").doLoginWithCookie();
|
||||
Assertions.assertTrue(authorizationEndpointResponse.isRedirected());
|
||||
Assertions.assertEquals(authorizationEndpointResponse.getError(), INVALID_TARGET);
|
||||
Assertions.assertEquals(authorizationEndpointResponse.getErrorDescription(), ERROR_INVALID_RESOURCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAuthzResourceInTokenRequest() {
|
||||
AuthorizationEndpointResponse authorizationEndpointResponse = oauth.loginForm().resource("urn:client:theservice").doLoginWithCookie();
|
||||
|
|
|
|||
Loading…
Reference in a new issue