diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/RedirectMismatchException.java b/gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/RedirectMismatchException.java similarity index 89% rename from gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/RedirectMismatchException.java rename to gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/RedirectMismatchException.java index 895da6ac4f0..1369266e430 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/exception/RedirectMismatchException.java +++ b/gravitee-am-common/src/main/java/io/gravitee/am/common/exception/oauth2/RedirectMismatchException.java @@ -13,9 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.gravitee.am.gateway.handler.oauth2.exception; - -import io.gravitee.am.common.exception.oauth2.OAuth2Exception; +package io.gravitee.am.common.exception.oauth2; /** * @author Titouan COMPIEGNE (titouan.compiegne at graviteesource.com) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java index 73dc90f67e1..d2ce57c4ecc 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/RootProvider.java @@ -73,6 +73,7 @@ import io.gravitee.am.gateway.handler.root.resources.handler.LocaleHandler; import io.gravitee.am.gateway.handler.root.resources.handler.botdetection.BotDetectionHandler; import io.gravitee.am.gateway.handler.root.resources.handler.client.ClientRequestParseHandler; +import io.gravitee.am.gateway.handler.root.resources.handler.common.RedirectUriValidationHandler; import io.gravitee.am.gateway.handler.root.resources.handler.consent.DataConsentHandler; import io.gravitee.am.gateway.handler.root.resources.handler.error.ErrorHandler; import io.gravitee.am.gateway.handler.root.resources.handler.geoip.GeoIpHandler; @@ -352,6 +353,7 @@ protected void doStart() throws Exception { Handler localeHandler = new LocaleHandler(messageResolver); Handler loginPostWebAuthnHandler = new LoginPostWebAuthnHandler(webAuthnCookieService); Handler userRememberMeHandler = new UserRememberMeRequestHandler(jwtService, domain, rememberMeCookieName); + Handler redirectUriValidationHandler = new RedirectUriValidationHandler(domain); // Root policy chain handler rootRouter.route() @@ -367,6 +369,7 @@ protected void doStart() throws Exception { // Identifier First Login route rootRouter.get(PATH_IDENTIFIER_FIRST_LOGIN) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(new LoginSocialAuthenticationHandler(identityProviderManager, jwtService, certificateManager)) .handler(policyChainHandler.create(ExtensionPoint.PRE_LOGIN_IDENTIFIER)) .handler(localeHandler) @@ -374,6 +377,7 @@ protected void doStart() throws Exception { rootRouter.post(PATH_IDENTIFIER_FIRST_LOGIN) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(botDetectionHandler) .handler(new LoginSocialAuthenticationHandler(identityProviderManager, jwtService, certificateManager)) .handler(userRememberMeHandler) @@ -385,6 +389,7 @@ protected void doStart() throws Exception { rootRouter.get(PATH_LOGIN) .handler(clientRequestParseHandler) .handler(new LoginSocialAuthenticationHandler(identityProviderManager, jwtService, certificateManager)) + .handler(redirectUriValidationHandler) .handler(policyChainHandler.create(ExtensionPoint.PRE_LOGIN)) .handler(new LoginHideFormHandler(domain)) .handler(new LoginSelectionRuleHandler(false)) @@ -393,6 +398,7 @@ protected void doStart() throws Exception { rootRouter.post(PATH_LOGIN) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(botDetectionHandler) .handler(loginAttemptHandler) .handler(new LoginFormHandler(userAuthProvider)) @@ -454,10 +460,12 @@ protected void doStart() throws Exception { Handler mfaChallengeUserHandler = new MFAChallengeUserHandler(userService); rootRouter.route(PATH_MFA_ENROLL) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(localeHandler) .handler(new MFAEnrollEndpoint(factorManager, thymeleafTemplateEngine, userService, domain, applicationContext)); rootRouter.route(PATH_MFA_CHALLENGE) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(rememberDeviceSettingsHandler) .handler(localeHandler) .handler(mfaChallengeUserHandler) @@ -466,11 +474,13 @@ protected void doStart() throws Exception { .failureHandler(new MFAChallengeFailureHandler(authenticationFlowContextService)); rootRouter.route(PATH_MFA_CHALLENGE_ALTERNATIVES) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(localeHandler) .handler(mfaChallengeUserHandler) .handler(new MFAChallengeAlternativesEndpoint(thymeleafTemplateEngine, factorManager, domain)); rootRouter.route(PATH_MFA_RECOVERY_CODE) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(localeHandler) .handler(new MFARecoveryCodeEndpoint(thymeleafTemplateEngine, domain, userService, factorManager, applicationContext)); @@ -479,11 +489,13 @@ protected void doStart() throws Exception { Handler webAuthnRememberDeviceHandler = new WebAuthnRememberDeviceHandler(webAuthnCookieService, domain); rootRouter.get(PATH_WEBAUTHN_REGISTER) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(webAuthnAccessHandler) .handler(localeHandler) .handler(new WebAuthnRegisterEndpoint(thymeleafTemplateEngine, domain, factorManager)); rootRouter.post(PATH_WEBAUTHN_REGISTER) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(webAuthnAccessHandler) .handler(new WebAuthnRegisterHandler(factorService, factorManager, domain, webAuthn, credentialService)) .handler(webAuthnRememberDeviceHandler) @@ -497,6 +509,7 @@ protected void doStart() throws Exception { .handler(new WebAuthnRegisterSuccessEndpoint(thymeleafTemplateEngine, credentialService, domain)); rootRouter.get(PATH_WEBAUTHN_LOGIN) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(webAuthnAccessHandler) .handler(new LoginSocialAuthenticationHandler(identityProviderManager, jwtService, certificateManager)) .handler(localeHandler) @@ -504,6 +517,7 @@ protected void doStart() throws Exception { .handler(new WebAuthnLoginEndpoint(thymeleafTemplateEngine, domain, deviceIdentifierManager, userActivityService)); rootRouter.post(PATH_WEBAUTHN_LOGIN) .handler(clientRequestParseHandler) + .handler(redirectUriValidationHandler) .handler(webAuthnAccessHandler) .handler(new WebAuthnLoginHandler(factorService, factorManager, domain, webAuthn, credentialService, userAuthenticationManager)) .handler(userRememberMeHandler) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/common/RedirectUriValidationHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/common/RedirectUriValidationHandler.java new file mode 100644 index 00000000000..2d53f560824 --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/resources/handler/common/RedirectUriValidationHandler.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.root.resources.handler.common; + +import io.gravitee.am.common.exception.oauth2.RedirectMismatchException; +import io.gravitee.am.common.utils.ConstantKeys; +import io.gravitee.am.gateway.handler.root.service.RedirectUriValidator; +import io.gravitee.am.model.Domain; +import io.gravitee.am.model.oidc.Client; +import io.vertx.core.Handler; +import io.vertx.rxjava3.ext.web.RoutingContext; + +import java.util.List; + +import static io.gravitee.am.gateway.handler.root.resources.endpoint.ParamUtils.getOAuthParameter; +import static io.gravitee.am.gateway.handler.root.resources.endpoint.ParamUtils.redirectMatches; + +/** + * The authorization server validates the request to ensure that all parameters are valid. + * If the request is valid, the authorization server authenticates the resource owner and obtains + * an authorization decision (by asking the resource owner or by establishing approval via other means). + * + * See 4.1.1. Authorization Request + * + * This specific handler is checking the validity of the redirect_uri + * + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class RedirectUriValidationHandler implements Handler { + + private final Domain domain; + private final RedirectUriValidator redirectUriValidator; + + public RedirectUriValidationHandler(Domain domain) { + this.domain = domain; + this.redirectUriValidator = new RedirectUriValidator(); + } + + @Override + public void handle(RoutingContext context) { + final Client client = context.get(ConstantKeys.CLIENT_CONTEXT_KEY); + + // proceed redirect_uri parameter + parseRedirectUriParameter(context, client); + + context.next(); + } + + private void parseRedirectUriParameter(RoutingContext context, Client client) { + String requestedRedirectUri = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); + redirectUriValidator.validate(client, requestedRedirectUri, this::checkMatchingRedirectUri); + } + + private void checkMatchingRedirectUri(String requestedRedirect, List registeredClientRedirectUris) { + if (registeredClientRedirectUris + .stream() + .noneMatch(registeredClientUri -> redirectMatches(requestedRedirect, registeredClientUri, this.domain.isRedirectUriStrictMatching() || this.domain.usePlainFapiProfile()))) { + throw new RedirectMismatchException(String.format("The redirect_uri [ %s ] MUST match the registered callback URL for this application", requestedRedirect)); + } + } +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/RedirectUriValidator.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/RedirectUriValidator.java new file mode 100644 index 00000000000..b6e8a14a4cd --- /dev/null +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-core/src/main/java/io/gravitee/am/gateway/handler/root/service/RedirectUriValidator.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2015 The Gravitee team (http://gravitee.io) + * + * 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 io.gravitee.am.gateway.handler.root.service; + +import io.gravitee.am.common.exception.oauth2.InvalidRequestException; +import io.gravitee.am.model.oidc.Client; + +import java.util.List; +import java.util.function.BiConsumer; + +/** + * @author Eric LELEU (eric.leleu at graviteesource.com) + * @author GraviteeSource Team + */ +public class RedirectUriValidator { + public void validate(Client client, String requestedRedirectUri, BiConsumer> checkMethod) { + final List registeredClientRedirectUris = client.getRedirectUris(); + final boolean hasRegisteredClientRedirectUris = registeredClientRedirectUris != null && !registeredClientRedirectUris.isEmpty(); + final boolean hasRequestedRedirectUri = requestedRedirectUri != null && !requestedRedirectUri.isEmpty(); + + // if no requested redirect_uri and no registered client redirect_uris + // throw invalid request exception + if (!hasRegisteredClientRedirectUris && !hasRequestedRedirectUri) { + throw new InvalidRequestException("A redirect_uri must be supplied"); + } + + // if no requested redirect_uri and more than one registered client redirect_uris + // throw invalid request exception + if (!hasRequestedRedirectUri && (registeredClientRedirectUris != null && registeredClientRedirectUris.size() > 1)) { + throw new InvalidRequestException("Unable to find suitable redirect_uri, a redirect_uri must be supplied"); + } + + // if requested redirect_uri doesn't match registered client redirect_uris + // throw redirect mismatch exception + if (hasRequestedRedirectUri && hasRegisteredClientRedirectUris) { + checkMethod.accept(requestedRedirectUri, registeredClientRedirectUris); + } + } +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java index 3b8ca881ac9..ab65bce018c 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/OAuth2Provider.java @@ -54,6 +54,7 @@ import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; import io.gravitee.am.gateway.handler.oidc.service.request.RequestObjectService; import io.gravitee.am.gateway.handler.root.resources.handler.LocaleHandler; +import io.gravitee.am.gateway.handler.root.resources.handler.common.RedirectUriValidationHandler; import io.gravitee.am.model.Domain; import io.gravitee.am.service.AuthenticationFlowContextService; import io.gravitee.am.service.DeviceService; @@ -237,6 +238,7 @@ private void initRouter() { xssHandler(oauth2Router); AuthenticationFlowContextHandler authenticationFlowContextHandler = new AuthenticationFlowContextHandler(authenticationFlowContextService, environment); + RedirectUriValidationHandler redirectUriValidationHandler = new RedirectUriValidationHandler(domain); Handler localeHandler = new LocaleHandler(messageResolver); // Authorization endpoint @@ -252,6 +254,7 @@ private void initRouter() { .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService, domain, parService, authenticationFlowContextService)) .handler(new AuthorizationRequestParseIdTokenHintHandler(idTokenService)) .handler(new AuthorizationRequestParseParametersHandler(domain)) + .handler(redirectUriValidationHandler) .handler(new RiskAssessmentHandler(deviceService, userActivityService, vertx.eventBus(), objectMapper)) .handler(authenticationFlowHandler.create()) .handler(new AuthorizationRequestResolveHandler(scopeManager)) @@ -268,6 +271,7 @@ private void initRouter() { .handler(authenticationFlowContextHandler) .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService, domain, parService, authenticationFlowContextService)) .handler(new AuthorizationRequestResolveHandler(scopeManager)) + .handler(redirectUriValidationHandler) .handler(userConsentPrepareContextHandler) .handler(policyChainHandler.create(ExtensionPoint.PRE_CONSENT)) .handler(localeHandler) @@ -278,6 +282,7 @@ private void initRouter() { .handler(authenticationFlowContextHandler) .handler(new AuthorizationRequestParseRequestObjectHandler(requestObjectService, domain, parService, authenticationFlowContextService)) .handler(new AuthorizationRequestResolveHandler(scopeManager)) + .handler(redirectUriValidationHandler) .handler(userConsentPrepareContextHandler) .handler(new UserConsentProcessHandler(userConsentService, domain)) .handler(policyChainHandler.create(ExtensionPoint.POST_CONSENT)) diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java index 4486021899d..1396cb55556 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestFailureHandler.java @@ -17,13 +17,13 @@ import io.gravitee.am.common.exception.oauth2.InvalidRequestObjectException; import io.gravitee.am.common.exception.oauth2.OAuth2Exception; +import io.gravitee.am.common.exception.oauth2.RedirectMismatchException; import io.gravitee.am.common.oauth2.Parameters; import io.gravitee.am.common.web.UriBuilder; import io.gravitee.am.gateway.handler.common.jwt.JWTService; import io.gravitee.am.common.utils.ConstantKeys; import io.gravitee.am.gateway.handler.common.vertx.utils.UriBuilderRequest; import io.gravitee.am.gateway.handler.oauth2.exception.JWTOAuth2Exception; -import io.gravitee.am.gateway.handler.oauth2.exception.RedirectMismatchException; import io.gravitee.am.gateway.handler.oauth2.resources.request.AuthorizationRequestFactory; import io.gravitee.am.gateway.handler.oauth2.service.request.AuthorizationRequest; import io.gravitee.am.gateway.handler.oauth2.service.response.OAuth2ErrorResponse; diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java index 886d299d70e..47528570d46 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/authorization/AuthorizationRequestParseParametersHandler.java @@ -16,6 +16,7 @@ package io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization; import io.gravitee.am.common.exception.oauth2.InvalidRequestException; +import io.gravitee.am.common.exception.oauth2.RedirectMismatchException; import io.gravitee.am.common.oauth2.CodeChallengeMethod; import io.gravitee.am.common.oauth2.GrantType; import io.gravitee.am.common.oauth2.ResponseType; @@ -23,7 +24,6 @@ import io.gravitee.am.common.oidc.idtoken.Claims; import io.gravitee.am.common.utils.ConstantKeys; import io.gravitee.am.gateway.handler.oauth2.exception.LoginRequiredException; -import io.gravitee.am.gateway.handler.oauth2.exception.RedirectMismatchException; import io.gravitee.am.gateway.handler.oauth2.exception.UnauthorizedClientException; import io.gravitee.am.gateway.handler.oauth2.exception.UnsupportedResponseModeException; import io.gravitee.am.gateway.handler.oauth2.service.pkce.PKCEUtils; @@ -98,9 +98,6 @@ public void handle(RoutingContext context) { // proceed response_type parameter parseResponseTypeParameter(context, client); - // proceed redirect_uri parameter - parseRedirectUriParameter(context, client); - context.next(); } @@ -298,31 +295,6 @@ private void parseResponseTypeParameter(RoutingContext context, Client client) { } } - private void parseRedirectUriParameter(RoutingContext context, Client client) { - String requestedRedirectUri = getOAuthParameter(context, io.gravitee.am.common.oauth2.Parameters.REDIRECT_URI); - final List registeredClientRedirectUris = client.getRedirectUris(); - final boolean hasRegisteredClientRedirectUris = registeredClientRedirectUris != null && !registeredClientRedirectUris.isEmpty(); - final boolean hasRequestedRedirectUri = requestedRedirectUri != null && !requestedRedirectUri.isEmpty(); - - // if no requested redirect_uri and no registered client redirect_uris - // throw invalid request exception - if (!hasRegisteredClientRedirectUris && !hasRequestedRedirectUri) { - throw new InvalidRequestException("A redirect_uri must be supplied"); - } - - // if no requested redirect_uri and more than one registered client redirect_uris - // throw invalid request exception - if (!hasRequestedRedirectUri && (registeredClientRedirectUris != null && registeredClientRedirectUris.size() > 1)) { - throw new InvalidRequestException("Unable to find suitable redirect_uri, a redirect_uri must be supplied"); - } - - // if requested redirect_uri doesn't match registered client redirect_uris - // throw redirect mismatch exception - if (hasRequestedRedirectUri && hasRegisteredClientRedirectUris) { - checkMatchingRedirectUri(requestedRedirectUri, registeredClientRedirectUris); - } - } - private boolean returnFromLoginPage(RoutingContext context) { return Boolean.TRUE.equals(context.session().get(ConstantKeys.USER_LOGIN_COMPLETED_KEY)); } @@ -332,12 +304,4 @@ private boolean containsGrantType(List authorizedGrantTypes) { .anyMatch(authorizedGrantType -> GrantType.AUTHORIZATION_CODE.equals(authorizedGrantType) || GrantType.IMPLICIT.equals(authorizedGrantType)); } - - private void checkMatchingRedirectUri(String requestedRedirect, List registeredClientRedirectUris) { - if (registeredClientRedirectUris - .stream() - .noneMatch(registeredClientUri -> redirectMatches(requestedRedirect, registeredClientUri, this.domain.isRedirectUriStrictMatching() || this.domain.usePlainFapiProfile()))) { - throw new RedirectMismatchException(String.format("The redirect_uri [ %s ] MUST match the registered callback URL for this application", requestedRedirect)); - } - } } diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java index e541e6e052e..0f6c95da092 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/main/java/io/gravitee/am/gateway/handler/oauth2/service/par/impl/PushedAuthorizationRequestServiceImpl.java @@ -31,6 +31,7 @@ import io.gravitee.am.gateway.handler.oidc.service.jwe.JWEService; import io.gravitee.am.gateway.handler.oidc.service.jwk.JWKService; import io.gravitee.am.gateway.handler.oidc.service.jws.JWSService; +import io.gravitee.am.gateway.handler.root.service.RedirectUriValidator; import io.gravitee.am.model.Domain; import io.gravitee.am.model.jose.JWK; import io.gravitee.am.model.oidc.Client; @@ -86,6 +87,8 @@ public class PushedAuthorizationRequestServiceImpl implements PushedAuthorizatio @Autowired private JWKService jwkService; + private RedirectUriValidator redirectUriValidator = new RedirectUriValidator(); + @Override public Single readFromURI(String requestUri, Client client, OpenIDProviderMetadata oidcMetadata) { if (requestUri.startsWith(PAR_URN_PREFIX)) { @@ -149,7 +152,7 @@ public Single registerParameters(PushedAutho .map(jwt -> checkRedirectUriParameter(jwt, client)))) .ignoreElement(); } else { - registrationValidation.andThen(Completable.fromAction(() -> checkRedirectUriParameter(par, client))); + registrationValidation = registrationValidation.andThen(Completable.fromAction(() -> checkRedirectUriParameter(par, client))); } return registrationValidation.andThen(Single.defer(() -> parRepository.create(par))).map(parPersisted -> { @@ -252,27 +255,7 @@ private JWT checkRedirectUriParameter(JWT request, Client client) { } private void checkRedirectUri(Client client, String requestedRedirectUri) { - final List registeredClientRedirectUris = client.getRedirectUris(); - final boolean hasRegisteredClientRedirectUris = registeredClientRedirectUris != null && !registeredClientRedirectUris.isEmpty(); - final boolean hasRequestedRedirectUri = requestedRedirectUri != null && !requestedRedirectUri.isEmpty(); - - // if no requested redirect_uri and no registered client redirect_uris - // throw invalid request exception - if (!hasRegisteredClientRedirectUris && !hasRequestedRedirectUri) { - throw new InvalidRequestException("A redirect_uri must be supplied"); - } - - // if no requested redirect_uri and more than one registered client redirect_uris - // throw invalid request exception - if (!hasRequestedRedirectUri && (registeredClientRedirectUris != null && registeredClientRedirectUris.size() > 1)) { - throw new InvalidRequestException("Unable to find suitable redirect_uri, a redirect_uri must be supplied"); - } - - // if requested redirect_uri doesn't match registered client redirect_uris - // throw redirect mismatch exception - if (hasRequestedRedirectUri && hasRegisteredClientRedirectUris) { - checkMatchingRedirectUri(requestedRedirectUri, registeredClientRedirectUris); - } + this.redirectUriValidator.validate(client, requestedRedirectUri, this::checkMatchingRedirectUri); } private void checkMatchingRedirectUri(String requestedRedirect, List registeredClientRedirectUris) { @@ -292,4 +275,4 @@ public Completable deleteRequestUri(String uriIdentifier) { } return parRepository.delete(uriIdentifier); } -} \ No newline at end of file +} diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java index dee38b1a4b1..3bfdb7ec92c 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/endpoint/AuthorizationEndpointTest.java @@ -37,6 +37,7 @@ import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; import io.gravitee.am.gateway.handler.oidc.service.flow.Flow; import io.gravitee.am.gateway.handler.oidc.service.jwe.JWEService; +import io.gravitee.am.gateway.handler.root.resources.handler.common.RedirectUriValidationHandler; import io.gravitee.am.model.Domain; import io.gravitee.am.model.application.ApplicationScopeSettings; import io.gravitee.am.model.oidc.Client; @@ -147,6 +148,7 @@ public void setUp() throws Exception { .handler(new AuthorizationRequestParseRequiredParametersHandler()) .handler(new AuthorizationRequestParseClientHandler(clientSyncService)) .handler(new AuthorizationRequestParseParametersHandler(domain)) + .handler(new RedirectUriValidationHandler(domain)) .handler(new AuthorizationRequestResolveHandler(scopeManager)) .handler(authorizationEndpointHandler); router.route() diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java index 97421602ccc..af037b9fae8 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/resources/handler/AuthorizationRequestParseParametersHandlerTest.java @@ -22,6 +22,7 @@ import io.gravitee.am.gateway.handler.common.vertx.RxWebTestBase; import io.gravitee.am.gateway.handler.oauth2.resources.handler.authorization.AuthorizationRequestParseParametersHandler; import io.gravitee.am.gateway.handler.oidc.service.discovery.OpenIDProviderMetadata; +import io.gravitee.am.gateway.handler.root.resources.handler.common.RedirectUriValidationHandler; import io.gravitee.am.model.Domain; import io.gravitee.am.model.oidc.Client; import io.gravitee.common.http.HttpStatusCode; @@ -52,6 +53,7 @@ public void setUp() throws Exception { super.setUp(); router.route(HttpMethod.GET, "/oauth/authorize") .handler(new AuthorizationRequestParseParametersHandler(domain)) + .handler(new RedirectUriValidationHandler(domain)) .handler(rc -> rc.response().end()) .failureHandler(rc -> rc.response().setStatusCode(400).end()); } @@ -75,6 +77,30 @@ public void shouldRejectRequest_unsupportedAcrValues() throws Exception { HttpStatusCode.BAD_REQUEST_400, "Bad Request", null); } + @Test + public void shouldRejectRequest_uriMismatch() throws Exception { + doReturn(false).when(domain).isRedirectUriStrictMatching(); + OpenIDProviderMetadata openIDProviderMetadata = new OpenIDProviderMetadata(); + openIDProviderMetadata.setAcrValuesSupported(Collections.singletonList(AcrValues.IN_COMMON_SILVER)); + openIDProviderMetadata.setResponseTypesSupported(Arrays.asList(ResponseType.CODE)); + Client client = new Client(); + client.setAuthorizedGrantTypes(Collections.singletonList(GrantType.AUTHORIZATION_CODE)); + client.setResponseTypes(Collections.singletonList(ResponseType.CODE)); + client.setRedirectUris(List.of("https://callback")); + router.route().order(-1).handler(routingContext -> { + routingContext.put(ConstantKeys.CLIENT_CONTEXT_KEY, client); + routingContext.put(ConstantKeys.PROVIDER_METADATA_CONTEXT_KEY, openIDProviderMetadata); + routingContext.next(); + }); + + testRequest( + HttpMethod.GET, + "/oauth/authorize?response_type=code&redirect_uri=https://notCallback&claims={\"id_token\":{\"acr\":{\"value\":\"urn:mace:incommon:iap:silver\",\"essential\":true}}}", + null, + HttpStatusCode.BAD_REQUEST_400, "Bad Request", null); + + } + @Test public void shouldRejectRequest_uriNotFormattedCorrectly() throws Exception { doReturn(false).when(domain).isRedirectUriStrictMatching(); diff --git a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java index 59dc09cb0b9..d410964fd3c 100644 --- a/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java +++ b/gravitee-am-gateway/gravitee-am-gateway-handler/gravitee-am-gateway-handler-oidc/src/test/java/io/gravitee/am/gateway/handler/oauth2/service/par/PushedAuthorizationRequestServiceTest.java @@ -50,10 +50,16 @@ import java.text.ParseException; import java.time.Instant; import java.util.Date; +import java.util.List; import java.util.concurrent.TimeUnit; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyBoolean; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * @author Eric LELEU (eric.leleu at graviteesource.com) @@ -99,14 +105,59 @@ public void shouldNotPersist_ClientIdMismatch() { verify(repository, never()).create(any()); } + @Test + public void shouldNotPersist_RedirectUriMismatch() { + final Client client = new Client(); + client.setClientId("clientid"); + client.setRedirectUris(List.of("https://valid/redirect/uri")); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("scope", "openid"); + parameters.add("response_type", "code"); + parameters.add("client_id", client.getClientId()); + parameters.add("redirect_uri", "https://invalid/redirect/uri"); + par.setParameters(parameters); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitDone(10, TimeUnit.SECONDS); + observer.assertError(InvalidRequestObjectException.class); + verify(repository, never()).create(any()); + } + + @Test + public void shouldNotPersist_ParametersWithoutRedirectUri() { + final Client client = createClient(); + + final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.add("scope", "openid"); + parameters.add("response_type", "code"); + parameters.add("client_id", client.getClientId()); + + final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); + par.setParameters(parameters); + par.setId("parid"); + par.setClient(client.getId()); + + final TestObserver observer = cut.registerParameters(par, client).test(); + + observer.awaitDone(10, TimeUnit.SECONDS); + observer.assertError(InvalidRequestException.class); + + verify(repository, never()).create(any()); + } + @Test public void shouldPersist_ParametersWithoutRequest() { final Client client = createClient(); + client.setRedirectUris(List.of("https://valid/redirect/uri")); final LinkedMultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("scope", "openid"); parameters.add("response_type", "code"); parameters.add("client_id", client.getClientId()); + parameters.add("redirect_uri", "https://valid/redirect/uri"); final PushedAuthorizationRequest par = new PushedAuthorizationRequest(); par.setParameters(parameters);