Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(jans-auth-server): redirect when session does not exist but client_id parameter is present #6104

Merged
merged 1 commit into from Sep 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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<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 @@ -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));
}
}

Expand Down Expand Up @@ -269,10 +269,10 @@ private void backChannel(Map<String, Client> 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);
Expand All @@ -284,45 +284,60 @@ 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 &&
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) {
Expand All @@ -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())) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down
Expand Up @@ -111,38 +111,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 @@ -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
Expand All @@ -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);
}

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

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