From f8f959144b527148f3b586088ae9dd6fcf1158cf Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Fri, 22 Sep 2023 14:27:31 +0300 Subject: [PATCH] fix(jans-auth-server): redirect when session does not exist but client_id parameter is present (#6104) https://github.com/JanssenProject/jans/issues/5942 Signed-off-by: YuriyZ --- .../ws/rs/EndSessionRestWebServiceImpl.java | 69 +++++++++++-------- .../rs/EndSessionRestWebServiceImplTest.java | 18 ++--- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImpl.java b/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImpl.java index 46a348c3cb9..711057b221f 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImpl.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImpl.java @@ -127,17 +127,17 @@ public Response requestEndSession(String idTokenHint, String postLogoutRedirectU errorResponseFactory.validateFeatureEnabled(FeatureFlagType.END_SESSION); - final SessionId sidSession = validateSidRequestParameter(sid, postLogoutRedirectUri, state); - Jwt validatedIdToken = validateIdTokenHint(idTokenHint, sidSession, postLogoutRedirectUri, state); + final SessionId sidSession = validateSidRequestParameter(sid, postLogoutRedirectUri, state, clientId); + Jwt validatedIdToken = validateIdTokenHint(idTokenHint, sidSession, postLogoutRedirectUri, state, clientId); final Pair pair = getPair(idTokenHint, validatedIdToken, sid, httpRequest); if (pair.getFirst() == null) { final String reason = "Failed to identify session by session_id query parameter or by session_id cookie."; - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state, clientId)); } postLogoutRedirectUri = validatePostLogoutRedirectUri(postLogoutRedirectUri, pair, state, clientId); - validateSid(postLogoutRedirectUri, validatedIdToken, pair.getFirst(), state); + validateSid(postLogoutRedirectUri, validatedIdToken, pair.getFirst(), state, clientId); endSession(pair, httpRequest, httpResponse); auditLogging(httpRequest, pair); @@ -222,14 +222,14 @@ private String addStateInPostLogoutRedirectUri(String postLogoutRedirectUri, Str .toString(); } - private void validateSid(String postLogoutRedirectUri, Jwt idToken, SessionId session, String state) { + private void validateSid(String postLogoutRedirectUri, Jwt idToken, SessionId session, String state, String clientId) { if (idToken == null) { return; } final String sid = idToken.getClaims().getClaimAsString("sid"); if (StringUtils.isNotBlank(sid) && !sid.equals(session.getOutsideSid())) { log.error("sid in id_token_hint does not match sid of the session. id_token_hint sid: {}, session sid: {}", sid, session.getOutsideSid()); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, "sid in id_token_hint does not match sid of the session", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, "sid in id_token_hint does not match sid of the session", state, clientId)); } } @@ -269,10 +269,10 @@ private void backChannel(Map backchannelUris, AuthorizationGrant } - private Response createErrorResponse(String postLogoutRedirectUri, EndSessionErrorResponseType error, String reason, String state) { - log.debug(reason); + private Response createErrorResponse(String postLogoutRedirectUri, EndSessionErrorResponseType error, String reason, String state, String clientId) { + log.debug("Creating error response, reason: {}, error: {}", reason, error); try { - if (allowPostLogoutRedirect(postLogoutRedirectUri)) { + if (allowPostLogoutRedirect(postLogoutRedirectUri, clientId)) { if (ErrorHandlingMethod.REMOTE == appConfiguration.getErrorHandlingMethod()) { String separator = postLogoutRedirectUri.contains("?") ? "&" : "?"; postLogoutRedirectUri = postLogoutRedirectUri + separator + errorResponseFactory.getErrorAsQueryString(error, "", reason); @@ -284,6 +284,8 @@ private Response createErrorResponse(String postLogoutRedirectUri, EndSessionErr } catch (URISyntaxException e) { log.error("Can't perform redirect", e); } + + log.trace("Return 400 - error {}, reason {}", error, reason); return Response.status(Response.Status.BAD_REQUEST).entity(errorResponseFactory.errorAsJson(error, reason)).build(); } @@ -291,38 +293,51 @@ private Response createErrorResponse(String postLogoutRedirectUri, EndSessionErr * Allow post logout redirect without validation only if: * allowPostLogoutRedirectWithoutValidation = true and post_logout_redirect_uri is white listed */ - private boolean allowPostLogoutRedirect(String postLogoutRedirectUri) { + private boolean allowPostLogoutRedirect(String postLogoutRedirectUri, String clientId) { if (StringUtils.isBlank(postLogoutRedirectUri)) { + log.trace("Post logout redirect is blank."); return false; } final Boolean allowPostLogoutRedirectWithoutValidation = appConfiguration.getAllowPostLogoutRedirectWithoutValidation(); - return allowPostLogoutRedirectWithoutValidation != null && + final boolean isOk = allowPostLogoutRedirectWithoutValidation != null && allowPostLogoutRedirectWithoutValidation && endSessionService.isUrlWhiteListed(postLogoutRedirectUri); + if (isOk) { + log.trace("Post logout redirect allowed by 'clientWhiteList' {}", appConfiguration.getClientWhiteList()); + return true; + } + + if (StringUtils.isNotBlank(clientId) && StringUtils.isNotBlank(redirectionUriService.validatePostLogoutRedirectUri(clientId, postLogoutRedirectUri))) { + log.trace("Post logout redirect allowed by client_id {}", clientId); + return true; + } + + log.trace("Post logout redirect is denied."); + return false; } - private SessionId validateSidRequestParameter(String sid, String postLogoutRedirectUri, String state) { + private SessionId validateSidRequestParameter(String sid, String postLogoutRedirectUri, String state, String clientId) { // sid is not required but if it is present then we must validate it #831 if (StringUtils.isNotBlank(sid)) { SessionId sessionIdObject = sessionIdService.getSessionBySid(sid); if (sessionIdObject == null) { final String reason = "sid parameter in request is not valid. Logout is rejected. sid parameter in request can be skipped or otherwise valid value must be provided."; log.error(reason); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state, clientId)); } return sessionIdObject; } return null; } - public Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, String postLogoutRedirectUri, String state) { + public Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, String postLogoutRedirectUri, String state, String clientId) { final boolean isIdTokenHintRequired = isTrue(appConfiguration.getForceIdTokenHintPrecense()); if (isIdTokenHintRequired && StringUtils.isBlank(idTokenHint)) { // must be present for logout tests #1279 final String reason = "id_token_hint is not set"; log.trace(reason); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, reason, state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_REQUEST, reason, state, clientId)); } if (StringUtils.isBlank(idTokenHint) && !isIdTokenHintRequired) { @@ -337,40 +352,40 @@ public Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, String if (tokenHintGrant == null && isRejectEndSessionIfIdTokenExpired) { final String reason = "id_token_hint is not valid. Logout is rejected. id_token_hint can be skipped or otherwise valid value must be provided."; log.trace(reason); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason, state, clientId)); } - return validateIdTokenJwt(tokenHintGrant, idTokenHint, sidSession, postLogoutRedirectUri, state); + return validateIdTokenJwt(tokenHintGrant, idTokenHint, sidSession, postLogoutRedirectUri, state, clientId); } return null; } - private Jwt validateIdTokenJwt(AuthorizationGrant tokenHintGrant, String idTokenHint, SessionId sidSession, String postLogoutRedirectUri, String state) { + private Jwt validateIdTokenJwt(AuthorizationGrant tokenHintGrant, String idTokenHint, SessionId sidSession, String postLogoutRedirectUri, String state, String clientId) { try { final Jwt jwt = Jwt.parse(idTokenHint); if (tokenHintGrant != null) { // id_token is in db log.debug("Found id_token in db."); return jwt; } - validateIdTokenSignature(sidSession, jwt, postLogoutRedirectUri, state); + validateIdTokenSignature(sidSession, jwt, postLogoutRedirectUri, state, clientId); log.debug("id_token is validated successfully."); return jwt; } catch (InvalidJwtException e) { log.error("Unable to parse id_token_hint as JWT.", e); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to parse id_token_hint as JWT.", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to parse id_token_hint as JWT.", state, clientId)); } catch (WebApplicationException e) { throw e; } catch (Exception e) { log.error("Unable to validate id_token_hint as JWT.", e); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to validate id_token_hint as JWT.", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "Unable to validate id_token_hint as JWT.", state, clientId)); } } - private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String postLogoutRedirectUri, String state) throws InvalidJwtException, CryptoProviderException { + private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String postLogoutRedirectUri, String state, String clientId) throws InvalidJwtException, CryptoProviderException { // verify jwt signature if we can't find it in db if (!cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), null, null, jwt.getHeader().getSignatureAlgorithm())) { log.error("id_token signature verification failed."); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "id_token signature verification failed.", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "id_token signature verification failed.", state, clientId)); } if (isTrue(appConfiguration.getAllowEndSessionWithUnmatchedSid())) { @@ -382,7 +397,7 @@ private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String post return; } log.error("sid claim from id_token does not match to any valid session on AS."); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "sid claim from id_token does not match to any valid session on AS.", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, "sid claim from id_token does not match to any valid session on AS.", state, clientId)); } protected AuthorizationGrant getTokenHintGrant(String idTokenHint) { @@ -433,18 +448,18 @@ protected String validatePostLogoutRedirectUri(String postLogoutRedirectUri, Pai if (StringUtils.isBlank(result)) { log.trace("Failed to validate post_logout_redirect_uri."); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state, clientId)); } if (StringUtils.isNotBlank(result)) { return result; } log.trace("Unable to validate post_logout_redirect_uri."); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state, clientId)); } catch (WebApplicationException e) { if (pair.getFirst() != null) { log.error(e.getMessage(), e); - throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state)); + throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.POST_LOGOUT_URI_NOT_ASSOCIATED_WITH_CLIENT, "", state, clientId)); } else { throw e; } diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImplTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImplTest.java index d0fa54fbc23..600b49f9594 100644 --- a/jans-auth-server/server/src/test/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImplTest.java +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImplTest.java @@ -111,14 +111,14 @@ public void validatePostLogoutRedirectUri_whenValidClientIdIsPassed_shouldValida @Test public void validateIdTokenHint_whenIdTokenHintIsBlank_shouldGetNoError() { - assertNull(endSessionRestWebService.validateIdTokenHint("", null, "", "http://postlogout.com")); + assertNull(endSessionRestWebService.validateIdTokenHint("", null, "", "http://postlogout.com", "")); } @Test(expectedExceptions = WebApplicationException.class) public void validateIdTokenHint_whenIdTokenHintIsBlankButRequired_shouldGetError() { when(appConfiguration.getForceIdTokenHintPrecense()).thenReturn(true); - endSessionRestWebService.validateIdTokenHint("", null, "", "http://postlogout.com"); + endSessionRestWebService.validateIdTokenHint("", null, "", "http://postlogout.com", ""); } @Test(expectedExceptions = WebApplicationException.class) @@ -126,7 +126,7 @@ public void validateIdTokenHint_whenIdTokenIsNotInDbAndExpiredIsNotAllowed_shoul when(appConfiguration.getRejectEndSessionIfIdTokenExpired()).thenReturn(true); when(endSessionRestWebService.getTokenHintGrant("test")).thenReturn(null); - endSessionRestWebService.validateIdTokenHint("testToken", null, "", "http://postlogout.com"); + endSessionRestWebService.validateIdTokenHint("testToken", null, "", "http://postlogout.com", ""); } @Test(expectedExceptions = WebApplicationException.class) @@ -134,7 +134,7 @@ public void validateIdTokenHint_whenIdTokenIsNotValidJwt_shouldGetError() { when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(true); when(endSessionRestWebService.getTokenHintGrant("notValidJwt")).thenReturn(GRANT); - endSessionRestWebService.validateIdTokenHint("notValidJwt", null, "", "http://postlogout.com"); + endSessionRestWebService.validateIdTokenHint("notValidJwt", null, "", "http://postlogout.com", ""); } @Test @@ -142,7 +142,7 @@ public void validateIdTokenHint_whenIdTokenIsValidJwt_shouldGetValidJwt() { when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(true); when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(GRANT); - final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "", "http://postlogout.com"); + final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "", "http://postlogout.com", ""); assertNotNull(jwt); } @@ -153,7 +153,7 @@ public void validateIdTokenHint_whenIdTokenSignatureIsBad_shouldGetError() throw when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null); when(cryptoProvider.verifySignature(anyString(), anyString(), anyString(), isNull(), isNull(), any())).thenReturn(false); - assertNull(endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "", "http://postlogout.com")); + assertNull(endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "", "http://postlogout.com", "")); } @Test @@ -163,7 +163,7 @@ public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsNotRequired_sho when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null); when(cryptoProvider.verifySignature(anyString(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(true); - final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "", "http://postlogout.com"); + final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "", "http://postlogout.com", ""); assertNotNull(jwt); } @@ -177,7 +177,7 @@ public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsRequired_should SessionId sidSession = new SessionId(); sidSession.setOutsideSid("1234"); // sid encoded into DUMMY_JWT - final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "", "http://postlogout.com"); + final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "", "http://postlogout.com", ""); assertNotNull(jwt); } @@ -191,7 +191,7 @@ public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsRequiredButSess SessionId sidSession = new SessionId(); sidSession.setOutsideSid("12345"); // sid encoded into DUMMY_JWT - final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "", "http://postlogout.com"); + final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "", "http://postlogout.com", ""); assertNotNull(jwt); } } \ No newline at end of file