Skip to content

Commit

Permalink
fix: Evaluate the redirect_uri at each step of the auth flow
Browse files Browse the repository at this point in the history
fixes AM-722
  • Loading branch information
leleueri committed Dec 15, 2023
1 parent 57ab05c commit 5ac7aab
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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;
Expand Down Expand Up @@ -329,6 +330,7 @@ protected void doStart() throws Exception {
Handler<RoutingContext> userActivityHandler = new UserActivityHandler(userActivityService);
Handler<RoutingContext> localeHandler = new LocaleHandler(messageResolver);
Handler<RoutingContext> loginPostWebAuthnHandler = new LoginPostWebAuthnHandler(webAuthnCookieService);
Handler<RoutingContext> redirectUriValidationHandler = new RedirectUriValidationHandler(domain);

// Root policy chain handler
rootRouter.route()
Expand All @@ -344,13 +346,15 @@ 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)
.handler(new IdentifierFirstLoginEndpoint(thymeleafTemplateEngine, domain, botDetectionManager));

rootRouter.post(PATH_IDENTIFIER_FIRST_LOGIN)
.handler(clientRequestParseHandler)
.handler(redirectUriValidationHandler)
.handler(botDetectionHandler)
.handler(new LoginSocialAuthenticationHandler(identityProviderManager, jwtService, certificateManager))
.handler(policyChainHandler.create(ExtensionPoint.POST_LOGIN_IDENTIFIER))
Expand All @@ -361,6 +365,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))
Expand All @@ -369,6 +374,7 @@ protected void doStart() throws Exception {

rootRouter.post(PATH_LOGIN)
.handler(clientRequestParseHandler)
.handler(redirectUriValidationHandler)
.handler(botDetectionHandler)
.handler(loginAttemptHandler)
.handler(new LoginFormHandler(userAuthProvider))
Expand Down Expand Up @@ -421,21 +427,25 @@ protected void doStart() throws Exception {
// MFA route
rootRouter.route(PATH_MFA_ENROLL)
.handler(clientRequestParseHandler)
.handler(redirectUriValidationHandler)
.handler(localeHandler)
.handler(new MFAEnrollEndpoint(factorManager, thymeleafTemplateEngine, userService, domain));
rootRouter.route(PATH_MFA_CHALLENGE)
.handler(clientRequestParseHandler)
.handler(redirectUriValidationHandler)
.handler(rememberDeviceSettingsHandler)
.handler(localeHandler)
.handler(new MFAChallengeEndpoint(factorManager, userService, thymeleafTemplateEngine, deviceService, applicationContext,
domain, credentialService, factorService, rateLimiterService, verifyAttemptService, emailService))
.failureHandler(new MFAChallengeFailureHandler(authenticationFlowContextService));
rootRouter.route(PATH_MFA_CHALLENGE_ALTERNATIVES)
.handler(clientRequestParseHandler)
.handler(redirectUriValidationHandler)
.handler(localeHandler)
.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));

Expand All @@ -444,11 +454,13 @@ protected void doStart() throws Exception {
Handler<RoutingContext> 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)
Expand All @@ -459,12 +471,14 @@ protected void doStart() throws Exception {
.handler(new WebAuthnRegisterCredentialsEndpoint(domain, webAuthn));
rootRouter.get(PATH_WEBAUTHN_LOGIN)
.handler(clientRequestParseHandler)
.handler(redirectUriValidationHandler)
.handler(webAuthnAccessHandler)
.handler(new LoginSocialAuthenticationHandler(identityProviderManager, jwtService, certificateManager))
.handler(localeHandler)
.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(deviceIdentifierHandler)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* 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.InvalidRequestException;
import io.gravitee.am.common.exception.oauth2.RedirectMismatchException;
import io.gravitee.am.common.utils.ConstantKeys;
import io.gravitee.am.model.Domain;
import io.gravitee.am.model.oidc.Client;
import io.vertx.core.Handler;
import io.vertx.reactivex.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 <a href="https://tools.ietf.org/html/rfc6749#section-4.1.1">4.1.1. Authorization Request</a>
*
* 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<RoutingContext> {

private final Domain domain;

public RedirectUriValidationHandler(Domain domain) {
this.domain = domain;
}

@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);
final List<String> 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 void checkMatchingRedirectUri(String requestedRedirect, List<String> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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;
Expand Down Expand Up @@ -253,6 +254,7 @@ private void initRouter() {
xssHandler(oauth2Router);

AuthenticationFlowContextHandler authenticationFlowContextHandler = new AuthenticationFlowContextHandler(authenticationFlowContextService, environment);
RedirectUriValidationHandler redirectUriValidationHandler = new RedirectUriValidationHandler(domain);
Handler<RoutingContext> localeHandler = new LocaleHandler(messageResolver);

// Authorization endpoint
Expand All @@ -268,6 +270,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))
Expand All @@ -283,6 +286,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)
Expand All @@ -293,6 +297,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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
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;
import io.gravitee.am.common.oidc.Parameters;
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;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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<String> 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));
}
Expand All @@ -332,12 +304,4 @@ private boolean containsGrantType(List<String> authorizedGrantTypes) {
.anyMatch(authorizedGrantType -> GrantType.AUTHORIZATION_CODE.equals(authorizedGrantType)
|| GrantType.IMPLICIT.equals(authorizedGrantType));
}

private void checkMatchingRedirectUri(String requestedRedirect, List<String> 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));
}
}
}

0 comments on commit 5ac7aab

Please sign in to comment.