Skip to content

Commit 1b46b44

Browse files
authored
feat(jans-auth-server): allow end session with expired id_token_hint (by checking signature and sid) #2430 (#2431)
docs: no docs (swagger is updated) #2430
1 parent 110cb14 commit 1b46b44

File tree

7 files changed

+319
-64
lines changed

7 files changed

+319
-64
lines changed

jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java

+18
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ public class AppConfiguration implements Configuration {
262262
private Boolean useLocalCache = false;
263263
private Boolean fapiCompatibility = false;
264264
private Boolean forceIdTokenHintPrecense = false;
265+
private Boolean rejectEndSessionIfIdTokenExpired = false;
266+
private Boolean allowEndSessionWithUnmatchedSid = false;
265267
private Boolean forceOfflineAccessScopeToEnableRefreshToken = true;
266268
private Boolean errorReasonEnabled = false;
267269
private Boolean removeRefreshTokensForClientOnLogout = true;
@@ -727,6 +729,22 @@ public void setForceIdTokenHintPrecense(Boolean forceIdTokenHintPrecense) {
727729
this.forceIdTokenHintPrecense = forceIdTokenHintPrecense;
728730
}
729731

732+
public Boolean getRejectEndSessionIfIdTokenExpired() {
733+
return rejectEndSessionIfIdTokenExpired;
734+
}
735+
736+
public void setRejectEndSessionIfIdTokenExpired(Boolean rejectEndSessionIfIdTokenExpired) {
737+
this.rejectEndSessionIfIdTokenExpired = rejectEndSessionIfIdTokenExpired;
738+
}
739+
740+
public Boolean getAllowEndSessionWithUnmatchedSid() {
741+
return allowEndSessionWithUnmatchedSid;
742+
}
743+
744+
public void setAllowEndSessionWithUnmatchedSid(Boolean allowEndSessionWithUnmatchedSid) {
745+
this.allowEndSessionWithUnmatchedSid = allowEndSessionWithUnmatchedSid;
746+
}
747+
730748
public Boolean getRemoveRefreshTokensForClientOnLogout() {
731749
if (removeRefreshTokensForClientOnLogout == null) removeRefreshTokensForClientOnLogout = true;
732750
return removeRefreshTokensForClientOnLogout;

jans-auth-server/server/src/main/java/io/jans/as/server/session/ws/rs/EndSessionRestWebServiceImpl.java

+112-64
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package io.jans.as.server.session.ws.rs;
2+
3+
import io.jans.as.common.model.session.SessionId;
4+
import io.jans.as.model.common.GrantType;
5+
import io.jans.as.model.configuration.AppConfiguration;
6+
import io.jans.as.model.crypto.AbstractCryptoProvider;
7+
import io.jans.as.model.error.ErrorResponseFactory;
8+
import io.jans.as.model.jwt.Jwt;
9+
import io.jans.as.server.audit.ApplicationAuditLogger;
10+
import io.jans.as.server.model.common.AuthorizationGrant;
11+
import io.jans.as.server.model.common.AuthorizationGrantList;
12+
import io.jans.as.server.service.*;
13+
import io.jans.as.server.service.external.ExternalApplicationSessionService;
14+
import io.jans.as.server.service.external.ExternalEndSessionService;
15+
import io.jans.model.security.Identity;
16+
import jakarta.ws.rs.WebApplicationException;
17+
import org.mockito.InjectMocks;
18+
import org.mockito.Mock;
19+
import org.mockito.testng.MockitoTestNGListener;
20+
import org.slf4j.Logger;
21+
import org.testng.annotations.Listeners;
22+
import org.testng.annotations.Test;
23+
24+
import static org.mockito.ArgumentMatchers.*;
25+
import static org.mockito.Mockito.when;
26+
import static org.testng.Assert.assertNull;
27+
import static org.testng.AssertJUnit.assertNotNull;
28+
29+
/**
30+
* @author Yuriy Z
31+
*/
32+
@Listeners(MockitoTestNGListener.class)
33+
public class EndSessionRestWebServiceImplTest {
34+
35+
private static final String DUMMY_JWT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NjM2NzcxODUsImV4cCI6MTY5NTIxMzE4NSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIkdpdmVuTmFtZSI6IkpvaG5ueSIsIlN1cm5hbWUiOiJSb2NrZXQiLCJFbWFpbCI6Impyb2NrZXRAZXhhbXBsZS5jb20iLCJzaWQiOiIxMjM0IiwiUm9sZSI6IlByb2plY3QgQWRtaW5pc3RyYXRvciJ9.pmJ5kTvxyfOUGOXTzYA1DMjbF96lfCF1dVSn_70nf2Q";
36+
private static final AuthorizationGrant GRANT = new AuthorizationGrant() {
37+
@Override
38+
public GrantType getGrantType() {
39+
return GrantType.AUTHORIZATION_CODE;
40+
}
41+
};
42+
43+
@InjectMocks
44+
private EndSessionRestWebServiceImpl endSessionRestWebService;
45+
46+
@Mock
47+
private Logger log;
48+
49+
@Mock
50+
private ErrorResponseFactory errorResponseFactory;
51+
52+
@Mock
53+
private RedirectionUriService redirectionUriService;
54+
55+
@Mock
56+
private AuthorizationGrantList authorizationGrantList;
57+
58+
@Mock
59+
private ExternalApplicationSessionService externalApplicationSessionService;
60+
61+
@Mock
62+
private ExternalEndSessionService externalEndSessionService;
63+
64+
@Mock
65+
private SessionIdService sessionIdService;
66+
67+
@Mock
68+
private CookieService cookieService;
69+
70+
@Mock
71+
private ClientService clientService;
72+
73+
@Mock
74+
private GrantService grantService;
75+
76+
@Mock
77+
private Identity identity;
78+
79+
@Mock
80+
private ApplicationAuditLogger applicationAuditLogger;
81+
82+
@Mock
83+
private AppConfiguration appConfiguration;
84+
85+
@Mock
86+
private LogoutTokenFactory logoutTokenFactory;
87+
88+
@Mock
89+
private AbstractCryptoProvider cryptoProvider;
90+
91+
@Test
92+
public void validateIdTokenHint_whenIdTokenHintIsBlank_shouldGetNoError() {
93+
assertNull(endSessionRestWebService.validateIdTokenHint("", null, "http://postlogout.com"));
94+
}
95+
96+
@Test(expectedExceptions = WebApplicationException.class)
97+
public void validateIdTokenHint_whenIdTokenHintIsBlankButRequired_shouldGetError() {
98+
when(appConfiguration.getForceIdTokenHintPrecense()).thenReturn(true);
99+
100+
endSessionRestWebService.validateIdTokenHint("", null, "http://postlogout.com");
101+
}
102+
103+
@Test(expectedExceptions = WebApplicationException.class)
104+
public void validateIdTokenHint_whenIdTokenIsNotInDbAndExpiredIsNotAllowed_shouldGetError() {
105+
when(appConfiguration.getRejectEndSessionIfIdTokenExpired()).thenReturn(true);
106+
when(endSessionRestWebService.getTokenHintGrant("test")).thenReturn(null);
107+
108+
endSessionRestWebService.validateIdTokenHint("testToken", null, "http://postlogout.com");
109+
}
110+
111+
@Test(expectedExceptions = WebApplicationException.class)
112+
public void validateIdTokenHint_whenIdTokenIsNotValidJwt_shouldGetError() {
113+
when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(true);
114+
when(endSessionRestWebService.getTokenHintGrant("notValidJwt")).thenReturn(GRANT);
115+
116+
endSessionRestWebService.validateIdTokenHint("notValidJwt", null, "http://postlogout.com");
117+
}
118+
119+
@Test
120+
public void validateIdTokenHint_whenIdTokenIsValidJwt_shouldGetValidJwt() {
121+
when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(true);
122+
when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(GRANT);
123+
124+
final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "http://postlogout.com");
125+
assertNotNull(jwt);
126+
}
127+
128+
@Test(expectedExceptions = WebApplicationException.class)
129+
public void validateIdTokenHint_whenIdTokenSignatureIsBad_shouldGetError() throws Exception {
130+
when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false);
131+
when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(true);
132+
when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null);
133+
when(cryptoProvider.verifySignature(anyString(), anyString(), anyString(), isNull(), isNull(), any())).thenReturn(false);
134+
135+
assertNull(endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "http://postlogout.com"));
136+
}
137+
138+
@Test
139+
public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsNotRequired_shouldGetValidJwt() throws Exception {
140+
when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false);
141+
when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(true);
142+
when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null);
143+
when(cryptoProvider.verifySignature(anyString(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(true);
144+
145+
final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, null, "http://postlogout.com");
146+
assertNotNull(jwt);
147+
}
148+
149+
@Test
150+
public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsRequired_shouldGetValidJwt() throws Exception {
151+
when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false);
152+
when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(false);
153+
when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null);
154+
when(cryptoProvider.verifySignature(anyString(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(true);
155+
156+
SessionId sidSession = new SessionId();
157+
sidSession.setOutsideSid("1234"); // sid encoded into DUMMY_JWT
158+
159+
final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "http://postlogout.com");
160+
assertNotNull(jwt);
161+
}
162+
163+
@Test(expectedExceptions = WebApplicationException.class)
164+
public void validateIdTokenHint_whenIdTokenIsExpiredAndSidCheckIsRequiredButSessionHasAnotherSid_shouldGetError() throws Exception {
165+
when(appConfiguration.getEndSessionWithAccessToken()).thenReturn(false);
166+
when(appConfiguration.getAllowEndSessionWithUnmatchedSid()).thenReturn(false);
167+
when(endSessionRestWebService.getTokenHintGrant(DUMMY_JWT)).thenReturn(null);
168+
when(cryptoProvider.verifySignature(anyString(), anyString(), isNull(), isNull(), isNull(), any())).thenReturn(true);
169+
170+
SessionId sidSession = new SessionId();
171+
sidSession.setOutsideSid("12345"); // sid encoded into DUMMY_JWT
172+
173+
final Jwt jwt = endSessionRestWebService.validateIdTokenHint(DUMMY_JWT, sidSession, "http://postlogout.com");
174+
assertNotNull(jwt);
175+
}
176+
}

jans-auth-server/server/src/test/resources/testng.xml

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<class name="io.jans.as.server.token.ws.rs.TokenRestWebServiceValidatorTest" />
2323
<class name="io.jans.as.server.ws.rs.stat.MonthsTest" />
2424
<class name="io.jans.as.server.authorize.ws.rs.AuthorizeRestWebServiceValidatorTest" />
25+
<class name="io.jans.as.server.session.ws.rs.EndSessionRestWebServiceImplTest" />
2526
</classes>
2627
</test>
2728

jans-config-api/docs/jans-config-api-swagger-auto.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -4329,6 +4329,10 @@ components:
43294329
type: boolean
43304330
forceIdTokenHintPrecense:
43314331
type: boolean
4332+
rejectEndSessionIfIdTokenExpired:
4333+
type: boolean
4334+
allowEndSessionWithUnmatchedSid:
4335+
type: boolean
43324336
forceOfflineAccessScopeToEnableRefreshToken:
43334337
type: boolean
43344338
errorReasonEnabled:

jans-config-api/docs/jans-config-api-swagger.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -5288,6 +5288,12 @@ components:
52885288
forceIdTokenHintPrecense:
52895289
type: boolean
52905290
description: Boolean value specifying whether force id_token_hint parameter presence.
5291+
rejectEndSessionIfIdTokenExpired:
5292+
type: boolean
5293+
description: default value false. If true and id_token is not found in db, request is rejected.
5294+
allowEndSessionWithUnmatchedSid:
5295+
type: boolean
5296+
description: default value false. If true, sid check will be skipped.
52915297
forceOfflineAccessScopeToEnableRefreshToken:
52925298
type: boolean
52935299
description: Boolean value specifying whether force offline_access scope to enable refresh_token grant type.

jans-config-api/server/src/test/resources/feature/config/properties/openid/config/config.json

+2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
"frontChannelLogoutSessionSupported": true,
4545
"spontaneousScopeLifetime": 86400,
4646
"forceIdTokenHintPrecense": false,
47+
"rejectEndSessionIfIdTokenExpired": false,
48+
"allowEndSessionWithUnmatchedSid": false,
4749
"claimsParameterSupported": false,
4850
"claimTypesSupported": [
4951
"normal"

0 commit comments

Comments
 (0)