From 0808247721cc64e856cce495d73adc8c3c69d0b4 Mon Sep 17 00:00:00 2001 From: YuriyZ Date: Wed, 21 Sep 2022 13:41:52 +0300 Subject: [PATCH] feat(jans-auth-server): allow end session with expired id_token_hint (by checking signature and sid) #2430 docs: no docs (swagger is updated) https://github.com/JanssenProject/jans/issues/2372 --- .../model/configuration/AppConfiguration.java | 18 ++ .../ws/rs/EndSessionRestWebServiceImpl.java | 176 +++++++++++------- .../rs/EndSessionRestWebServiceImplTest.java | 176 ++++++++++++++++++ .../server/src/test/resources/testng.xml | 1 + .../docs/jans-config-api-swagger-auto.yaml | 4 + .../docs/jans-config-api-swagger.yaml | 6 + .../properties/openid/config/config.json | 2 + 7 files changed, 319 insertions(+), 64 deletions(-) create mode 100644 jans-auth-server/server/src/test/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImplTest.java diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java index c0a223346dd..a200ee40913 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java @@ -262,6 +262,8 @@ public class AppConfiguration implements Configuration { private Boolean useLocalCache = false; private Boolean fapiCompatibility = false; private Boolean forceIdTokenHintPrecense = false; + private Boolean rejectEndSessionIfIdTokenExpired = false; + private Boolean allowEndSessionWithUnmatchedSid = false; private Boolean forceOfflineAccessScopeToEnableRefreshToken = true; private Boolean errorReasonEnabled = false; private Boolean removeRefreshTokensForClientOnLogout = true; @@ -727,6 +729,22 @@ public void setForceIdTokenHintPrecense(Boolean forceIdTokenHintPrecense) { this.forceIdTokenHintPrecense = forceIdTokenHintPrecense; } + public Boolean getRejectEndSessionIfIdTokenExpired() { + return rejectEndSessionIfIdTokenExpired; + } + + public void setRejectEndSessionIfIdTokenExpired(Boolean rejectEndSessionIfIdTokenExpired) { + this.rejectEndSessionIfIdTokenExpired = rejectEndSessionIfIdTokenExpired; + } + + public Boolean getAllowEndSessionWithUnmatchedSid() { + return allowEndSessionWithUnmatchedSid; + } + + public void setAllowEndSessionWithUnmatchedSid(Boolean allowEndSessionWithUnmatchedSid) { + this.allowEndSessionWithUnmatchedSid = allowEndSessionWithUnmatchedSid; + } + public Boolean getRemoveRefreshTokensForClientOnLogout() { if (removeRefreshTokensForClientOnLogout == null) removeRefreshTokensForClientOnLogout = true; return removeRefreshTokensForClientOnLogout; 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 5c1660fef2c..4933c27079f 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 @@ -10,11 +10,14 @@ import com.google.common.collect.Sets; import io.jans.as.common.model.common.User; import io.jans.as.common.model.registration.Client; +import io.jans.as.common.model.session.SessionId; import io.jans.as.model.authorize.AuthorizeRequestParam; import io.jans.as.model.common.FeatureFlagType; import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.crypto.AbstractCryptoProvider; import io.jans.as.model.error.ErrorHandlingMethod; import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.model.exception.CryptoProviderException; import io.jans.as.model.exception.InvalidJwtException; import io.jans.as.model.gluu.GluuErrorResponseType; import io.jans.as.model.jwt.Jwt; @@ -28,13 +31,8 @@ import io.jans.as.server.model.audit.OAuth2AuditLog; import io.jans.as.server.model.common.AuthorizationGrant; import io.jans.as.server.model.common.AuthorizationGrantList; -import io.jans.as.common.model.session.SessionId; import io.jans.as.server.model.config.Constants; -import io.jans.as.server.service.ClientService; -import io.jans.as.server.service.CookieService; -import io.jans.as.server.service.GrantService; -import io.jans.as.server.service.RedirectionUriService; -import io.jans.as.server.service.SessionIdService; +import io.jans.as.server.service.*; import io.jans.as.server.service.external.ExternalApplicationSessionService; import io.jans.as.server.service.external.ExternalEndSessionService; import io.jans.as.server.service.external.context.EndSessionContext; @@ -42,9 +40,6 @@ import io.jans.model.security.Identity; import io.jans.util.Pair; import io.jans.util.StringHelper; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; - import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -54,6 +49,9 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriBuilder; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; + import java.net.URI; import java.net.URISyntaxException; import java.util.Map; @@ -61,6 +59,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; +import static org.apache.commons.lang.BooleanUtils.isTrue; + /** * @author Javier Rojas Blum * @author Yuriy Movchan @@ -112,6 +112,9 @@ public class EndSessionRestWebServiceImpl implements EndSessionRestWebService { @Inject private LogoutTokenFactory logoutTokenFactory; + @Inject + private AbstractCryptoProvider cryptoProvider; + @Override public Response requestEndSession(String idTokenHint, String postLogoutRedirectUri, String state, String sid, HttpServletRequest httpRequest, HttpServletResponse httpResponse, SecurityContext sec) { @@ -121,8 +124,8 @@ public Response requestEndSession(String idTokenHint, String postLogoutRedirectU errorResponseFactory.validateFeatureEnabled(FeatureFlagType.END_SESSION); - Jwt idToken = validateIdTokenHint(idTokenHint, postLogoutRedirectUri); - validateSidRequestParameter(sid, postLogoutRedirectUri); + final SessionId sidSession = validateSidRequestParameter(sid, postLogoutRedirectUri); + Jwt idToken = validateIdTokenHint(idTokenHint, sidSession, postLogoutRedirectUri); final Pair pair = getPair(idTokenHint, sid, httpRequest); if (pair.getFirst() == null) { @@ -140,42 +143,13 @@ public Response requestEndSession(String idTokenHint, String postLogoutRedirectU Set frontchannelUris = Sets.newHashSet(); Map backchannelUris = Maps.newHashMap(); - for (Client client : clients) { - boolean hasBackchannel = false; - for (String logoutUri : client.getAttributes().getBackchannelLogoutUri()) { - if (Util.isNullOrEmpty(logoutUri)) { - continue; // skip if logout_uri is blank - } - backchannelUris.put(logoutUri, client); - hasBackchannel = true; - } - - if (hasBackchannel) { // client has backchannel_logout_uri - continue; - } - if (StringUtils.isNotBlank(client.getFrontChannelLogoutUri())) { - String logoutUri = client.getFrontChannelLogoutUri(); - if (client.getFrontChannelLogoutSessionRequired()) { - logoutUri = EndSessionUtils.appendSid(logoutUri, pair.getFirst().getOutsideSid(), appConfiguration.getIssuer()); - } - frontchannelUris.add(logoutUri); - } - } + collectFrontAndBackChannelsUris(pair, clients, frontchannelUris, backchannelUris); backChannel(backchannelUris, pair.getSecond(), pair.getFirst()); postLogoutRedirectUri = addStateInPostLogoutRedirectUri(postLogoutRedirectUri, state); if (frontchannelUris.isEmpty() && StringUtils.isNotBlank(postLogoutRedirectUri)) { // no front-channel - log.trace("No frontchannel_redirect_uri's found in clients involved in SSO."); - - try { - log.trace("Redirect to postlogout_redirect_uri: " + postLogoutRedirectUri); - return Response.status(Response.Status.FOUND).location(new URI(postLogoutRedirectUri)).build(); - } catch (URISyntaxException e) { - final String message = "Failed to create URI for " + postLogoutRedirectUri + " postlogout_redirect_uri."; - log.error(message); - return Response.status(Response.Status.BAD_REQUEST).entity(errorResponseFactory.errorAsJson(EndSessionErrorResponseType.INVALID_REQUEST, message)).build(); - } + return noFrontChannelRedirectUrisResponse(postLogoutRedirectUri); } return httpBased(frontchannelUris, postLogoutRedirectUri, state, pair, httpRequest); @@ -194,6 +168,43 @@ public Response requestEndSession(String idTokenHint, String postLogoutRedirectU } } + private void collectFrontAndBackChannelsUris(Pair pair, Set clients, Set frontchannelUris, Map backchannelUris) { + for (Client client : clients) { + boolean hasBackchannel = false; + for (String logoutUri : client.getAttributes().getBackchannelLogoutUri()) { + if (Util.isNullOrEmpty(logoutUri)) { + continue; // skip if logout_uri is blank + } + backchannelUris.put(logoutUri, client); + hasBackchannel = true; + } + + if (hasBackchannel) { // client has backchannel_logout_uri + continue; + } + if (StringUtils.isNotBlank(client.getFrontChannelLogoutUri())) { + String logoutUri = client.getFrontChannelLogoutUri(); + if (isTrue(client.getFrontChannelLogoutSessionRequired())) { + logoutUri = EndSessionUtils.appendSid(logoutUri, pair.getFirst().getOutsideSid(), appConfiguration.getIssuer()); + } + frontchannelUris.add(logoutUri); + } + } + } + + private Response noFrontChannelRedirectUrisResponse(String postLogoutRedirectUri) { + log.trace("No frontchannel_redirect_uri's found in clients involved in SSO."); + + try { + log.trace("Redirect to postlogout_redirect_uri: {}", postLogoutRedirectUri); + return Response.status(Response.Status.FOUND).location(new URI(postLogoutRedirectUri)).build(); + } catch (URISyntaxException e) { + final String message = "Failed to create URI for " + postLogoutRedirectUri + " postlogout_redirect_uri."; + log.error(message); + return Response.status(Response.Status.BAD_REQUEST).entity(errorResponseFactory.errorAsJson(EndSessionErrorResponseType.INVALID_REQUEST, message)).build(); + } + } + /** * Adds state param in the post_logout_redirect_uri whether it exists. */ @@ -224,7 +235,7 @@ private void backChannel(Map backchannelUris, AuthorizationGrant return; } - log.trace("backchannel_redirect_uri's: " + backchannelUris); + log.trace("backchannel_redirect_uri's: {}", backchannelUris); User user = grant != null ? grant.getUser() : null; if (user == null) { @@ -235,7 +246,7 @@ private void backChannel(Map backchannelUris, AuthorizationGrant for (final Map.Entry entry : backchannelUris.entrySet()) { final JsonWebResponse logoutToken = logoutTokenFactory.createLogoutToken(entry.getValue(), session.getOutsideSid(), user); if (logoutToken == null) { - log.error("Failed to create logout_token for client: " + entry.getValue().getClientId()); + log.error("Failed to create logout_token for client: {}", entry.getValue().getClientId()); return; } executorService.execute(() -> EndSessionUtils.callRpWithBackchannelUri(entry.getKey(), logoutToken.toString())); @@ -286,7 +297,7 @@ private boolean allowPostLogoutRedirect(String postLogoutRedirectUri) { new URLPatternList(appConfiguration.getClientWhiteList()).isUrlListed(postLogoutRedirectUri); } - private void validateSidRequestParameter(String sid, String postLogoutRedirectUri) { + private SessionId validateSidRequestParameter(String sid, String postLogoutRedirectUri) { // sid is not required but if it is present then we must validate it #831 if (StringUtils.isNotBlank(sid)) { SessionId sessionIdObject = sessionIdService.getSessionBySid(sid); @@ -295,40 +306,77 @@ private void validateSidRequestParameter(String sid, String postLogoutRedirectUr log.error(reason); throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason)); } + return sessionIdObject; } + return null; } - private Jwt validateIdTokenHint(String idTokenHint, String postLogoutRedirectUri) { - if (appConfiguration.getForceIdTokenHintPrecense() && StringUtils.isBlank(idTokenHint)) { // must be present for logout tests #1279 + protected Jwt validateIdTokenHint(String idTokenHint, SessionId sidSession, String postLogoutRedirectUri) { + 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)); } - final AuthorizationGrant tokenHintGrant = getTokenHintGrant(idTokenHint); - if (appConfiguration.getForceIdTokenHintPrecense() && tokenHintGrant == null) { // 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)); + if (StringUtils.isBlank(idTokenHint) && !isIdTokenHintRequired) { + return null; } // id_token_hint is not required but if it is present then we must validate it #831 - if (StringUtils.isNotBlank(idTokenHint)) { - if (tokenHintGrant == null) { + if (StringUtils.isNotBlank(idTokenHint) || isIdTokenHintRequired) { + final boolean isRejectEndSessionIfIdTokenExpired = appConfiguration.getRejectEndSessionIfIdTokenExpired(); + final AuthorizationGrant tokenHintGrant = getTokenHintGrant(idTokenHint); + + 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."; throw new WebApplicationException(createErrorResponse(postLogoutRedirectUri, EndSessionErrorResponseType.INVALID_GRANT_AND_SESSION, reason)); } - try { - return Jwt.parse(idTokenHint); - } 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.")); - } + return validateIdTokenJwt(tokenHintGrant, idTokenHint, sidSession, postLogoutRedirectUri); } return null; } - private AuthorizationGrant getTokenHintGrant(String idTokenHint) { + private Jwt validateIdTokenJwt(AuthorizationGrant tokenHintGrant, String idTokenHint, SessionId sidSession, String postLogoutRedirectUri) { + try { + final Jwt jwt = Jwt.parse(idTokenHint); + if (tokenHintGrant != null) { // id_token is in db + return jwt; + } + validateIdTokenSignature(sidSession, jwt, postLogoutRedirectUri); + 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.")); + } 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.")); + } + } + + private void validateIdTokenSignature(SessionId sidSession, Jwt jwt, String postLogoutRedirectUri) 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.")); + } + + if (isTrue(appConfiguration.getAllowEndSessionWithUnmatchedSid())) { + return; + } + final String sidClaim = jwt.getClaims().getClaimAsString("sid"); + if (sidSession != null && StringUtils.equals(sidSession.getOutsideSid(), sidClaim)) { + 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.")); + } + + protected AuthorizationGrant getTokenHintGrant(String idTokenHint) { if (StringUtils.isBlank(idTokenHint)) { return null; } @@ -351,7 +399,7 @@ private String validatePostLogoutRedirectUri(String postLogoutRedirectUri, Pair< if (StringUtils.isBlank(postLogoutRedirectUri)) { return ""; } - if (appConfiguration.getAllowPostLogoutRedirectWithoutValidation()) { + if (isTrue(appConfiguration.getAllowPostLogoutRedirectWithoutValidation())) { log.trace("Skipped post_logout_redirect_uri validation (because allowPostLogoutRedirectWithoutValidation=true)"); return postLogoutRedirectUri; } @@ -388,7 +436,7 @@ private Response httpBased(Set frontchannelUris, String postLogoutRedire final EndSessionContext context = new EndSessionContext(httpRequest, frontchannelUris, postLogoutRedirectUri, pair.getFirst()); final String htmlFromScript = externalEndSessionService.getFrontchannelHtml(context); if (StringUtils.isNotBlank(htmlFromScript)) { - log.debug("HTML from `getFrontchannelHtml` external script: " + htmlFromScript); + log.debug("HTML from `getFrontchannelHtml` external script: {}", htmlFromScript); return okResponse(htmlFromScript); } } catch (Exception e) { @@ -397,7 +445,7 @@ private Response httpBased(Set frontchannelUris, String postLogoutRedire // default handling final String html = EndSessionUtils.createFronthannelHtml(frontchannelUris, postLogoutRedirectUri, state); - log.debug("Constructed html logout page: " + html); + log.debug("Constructed html logout page: {}", html); return okResponse(html); } 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 new file mode 100644 index 00000000000..92fc393f72a --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImplTest.java @@ -0,0 +1,176 @@ +package io.jans.as.server.session.ws.rs; + +import io.jans.as.common.model.session.SessionId; +import io.jans.as.model.common.GrantType; +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.crypto.AbstractCryptoProvider; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.model.jwt.Jwt; +import io.jans.as.server.audit.ApplicationAuditLogger; +import io.jans.as.server.model.common.AuthorizationGrant; +import io.jans.as.server.model.common.AuthorizationGrantList; +import io.jans.as.server.service.*; +import io.jans.as.server.service.external.ExternalApplicationSessionService; +import io.jans.as.server.service.external.ExternalEndSessionService; +import io.jans.model.security.Identity; +import jakarta.ws.rs.WebApplicationException; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertNull; +import static org.testng.AssertJUnit.assertNotNull; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class EndSessionRestWebServiceImplTest { + + private static final String DUMMY_JWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjM2NzcxODUsImV4cCI6MTY5NTIxMzE4NSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJzaWQiOiIxMjM0IiwiUm9sZSI6IlByb2plY3QgQWRtaW5pc3RyYXRvciJ9.pmJ5kTvxyfOUGOXTzYA1DMjbF96lfCF1dVSn_70nf2Q"; + private static final AuthorizationGrant GRANT = new AuthorizationGrant() { + @Override + public GrantType getGrantType() { + return GrantType.AUTHORIZATION_CODE; + } + }; + + @InjectMocks + private EndSessionRestWebServiceImpl endSessionRestWebService; + + @Mock + private Logger log; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Mock + private RedirectionUriService redirectionUriService; + + @Mock + private AuthorizationGrantList authorizationGrantList; + + @Mock + private ExternalApplicationSessionService externalApplicationSessionService; + + @Mock + private ExternalEndSessionService externalEndSessionService; + + @Mock + private SessionIdService sessionIdService; + + @Mock + private CookieService cookieService; + + @Mock + private ClientService clientService; + + @Mock + private GrantService grantService; + + @Mock + private Identity identity; + + @Mock + private ApplicationAuditLogger applicationAuditLogger; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private LogoutTokenFactory logoutTokenFactory; + + @Mock + private AbstractCryptoProvider cryptoProvider; + + @Test + public void validateIdTokenHint_whenIdTokenHintIsBlank_shouldGetNoError() { + 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"); + } + + @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"); + } + + @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"); + } + + @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"); + assertNotNull(jwt); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateIdTokenHint_whenIdTokenSignatureIsBad_shouldGetError() throws Exception { + when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false); + when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(true); + 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")); + } + + @Test + public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsNotRequired_shouldGetValidJwt() throws Exception { + when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false); + when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(true); + 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"); + assertNotNull(jwt); + } + + @Test + public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsRequired_shouldGetValidJwt() throws Exception { + when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false); + when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(false); + when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null); + when(cryptoProvider.verifySignature(anyString(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(true); + + SessionId sidSession = new SessionId(); + sidSession.setOutsideSid("1234"); // sid encoded into DUMMY_JWT + + final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "http://postlogout.com"); + assertNotNull(jwt); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsRequiredButSessionHasAnotherSid_shouldGetError() throws Exception { + when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false); + when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(false); + when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null); + when(cryptoProvider.verifySignature(anyString(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(true); + + SessionId sidSession = new SessionId(); + sidSession.setOutsideSid("12345"); // sid encoded into DUMMY_JWT + + final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "http://postlogout.com"); + assertNotNull(jwt); + } +} \ No newline at end of file diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index 9583a84c6a9..07641bb8a79 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -22,6 +22,7 @@ + diff --git a/jans-config-api/docs/jans-config-api-swagger-auto.yaml b/jans-config-api/docs/jans-config-api-swagger-auto.yaml index 4fd0a2dfba3..950940dd23a 100644 --- a/jans-config-api/docs/jans-config-api-swagger-auto.yaml +++ b/jans-config-api/docs/jans-config-api-swagger-auto.yaml @@ -4329,6 +4329,10 @@ components: type: boolean forceIdTokenHintPrecense: type: boolean + rejectEndSessionIfIdTokenExpired: + type: boolean + allowEndSessionWithUnmatchedSid: + type: boolean forceOfflineAccessScopeToEnableRefreshToken: type: boolean errorReasonEnabled: diff --git a/jans-config-api/docs/jans-config-api-swagger.yaml b/jans-config-api/docs/jans-config-api-swagger.yaml index 9153beccb13..ba887566ebb 100644 --- a/jans-config-api/docs/jans-config-api-swagger.yaml +++ b/jans-config-api/docs/jans-config-api-swagger.yaml @@ -5288,6 +5288,12 @@ components: forceIdTokenHintPrecense: type: boolean description: Boolean value specifying whether force id_token_hint parameter presence. + rejectEndSessionIfIdTokenExpired: + type: boolean + description: default value false. If true and id_token is not found in db, request is rejected. + allowEndSessionWithUnmatchedSid: + type: boolean + description: default value false. If true, sid check will be skipped. forceOfflineAccessScopeToEnableRefreshToken: type: boolean description: Boolean value specifying whether force offline_access scope to enable refresh_token grant type. diff --git a/jans-config-api/server/src/test/resources/feature/config/properties/openid/config/config.json b/jans-config-api/server/src/test/resources/feature/config/properties/openid/config/config.json index 5fa073ff7f6..98804774ef5 100644 --- a/jans-config-api/server/src/test/resources/feature/config/properties/openid/config/config.json +++ b/jans-config-api/server/src/test/resources/feature/config/properties/openid/config/config.json @@ -44,6 +44,8 @@ "frontChannelLogoutSessionSupported": true, "spontaneousScopeLifetime": 86400, "forceIdTokenHintPrecense": false, + "rejectEndSessionIfIdTokenExpired": false, + "allowEndSessionWithUnmatchedSid": false, "claimsParameterSupported": false, "claimTypesSupported": [ "normal"