Skip to content

Commit

Permalink
fix(oxauth): corrected redirect when session does not exist but clien…
Browse files Browse the repository at this point in the history
…t_id parameter is present (4.5.2)

#1862
  • Loading branch information
yuriyz committed Sep 22, 2023
1 parent b7962b4 commit 5bf1bc5
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,17 @@ public Response requestEndSession(String idTokenHint, String postLogoutRedirectU
if (StringUtils.isBlank(sid) && StringUtils.isNotBlank(sessionId))
sid = sessionId; // backward compatibility. WIll be removed in next major release.

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<SessionId, AuthorizationGrant> 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);
Expand Down Expand Up @@ -199,14 +199,14 @@ public Response requestEndSession(String idTokenHint, String postLogoutRedirectU
}
}

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));
}
}

Expand Down Expand Up @@ -236,10 +236,10 @@ private void backChannel(Map<String, Client> backchannelUris, AuthorizationGrant
log.trace("Finished backchannel calls.");
}

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: {}", reason);
try {
if (allowPostLogoutRedirect(postLogoutRedirectUri)) {
if (allowPostLogoutRedirect(postLogoutRedirectUri, clientId)) {
if (ErrorHandlingMethod.REMOTE == appConfiguration.getErrorHandlingMethod()) {
String separator = postLogoutRedirectUri.contains("?") ? "&" : "?";
postLogoutRedirectUri = postLogoutRedirectUri + separator + errorResponseFactory.getErrorAsQueryString(error, "", reason);
Expand All @@ -251,22 +251,39 @@ 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();
}

/**
* 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 &&
boolean isOk = allowPostLogoutRedirectWithoutValidation != null &&
allowPostLogoutRedirectWithoutValidation &&
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;

}

public boolean isUrlWhiteListed(String url) {
Expand All @@ -275,32 +292,32 @@ public boolean isUrlWhiteListed(String url) {
return result;
}

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;
}

protected Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, String postLogoutRedirectUri, String state) {
protected 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 (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) {
Expand All @@ -315,40 +332,40 @@ protected Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, Stri
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));
}
try {
final Jwt jwt = Jwt.parse(idTokenHint);
if (jwt == null) {
log.error("Unable to parse id_token_hint as JWT: {}", idTokenHint);
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));
}
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));
}
}
return null;
}

private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String postLogoutRedirectUri, String state) throws Exception {
private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String postLogoutRedirectUri, String state, String clientId) throws Exception {
// 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())) {
Expand All @@ -359,7 +376,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) {
Expand Down Expand Up @@ -409,18 +426,18 @@ public String validatePostLogoutRedirectUri(String postLogoutRedirectUri, Pair<S

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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,38 +109,38 @@ 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)
public void validateIdTokenHint_whenIdTokenIsNotInDbAndExpiredIsNotAllowed_shouldGetError() {
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)
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
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);
}

Expand All @@ -151,7 +151,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
Expand All @@ -161,7 +161,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);
}

Expand All @@ -175,7 +175,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);
}

Expand All @@ -189,7 +189,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);
}
}

0 comments on commit 5bf1bc5

Please sign in to comment.