Skip to content

feat(jans-auth-server): allow end session with expired id_token_hint(by checking signature and sid) #2430 #2431

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

Merged
merged 1 commit into from
Sep 21, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions jans-auth-server/server/src/test/resources/testng.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<class name="io.jans.as.server.token.ws.rs.TokenRestWebServiceValidatorTest" />
<class name="io.jans.as.server.ws.rs.stat.MonthsTest" />
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceValidatorTest" />
<class name="io.jans.as.server.session.ws.rs.EndSessionRestWebServiceImplTest" />
</classes>
</test>

Expand Down
4 changes: 4 additions & 0 deletions jans-config-api/docs/jans-config-api-swagger-auto.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4329,6 +4329,10 @@ components:
type: boolean
forceIdTokenHintPrecense:
type: boolean
rejectEndSessionIfIdTokenExpired:
type: boolean
allowEndSessionWithUnmatchedSid:
type: boolean
forceOfflineAccessScopeToEnableRefreshToken:
type: boolean
errorReasonEnabled:
Expand Down
6 changes: 6 additions & 0 deletions jans-config-api/docs/jans-config-api-swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
"frontChannelLogoutSessionSupported": true,
"spontaneousScopeLifetime": 86400,
"forceIdTokenHintPrecense": false,
"rejectEndSessionIfIdTokenExpired": false,
"allowEndSessionWithUnmatchedSid": false,
"claimsParameterSupported": false,
"claimTypesSupported": [
"normal"
Expand Down