2121import static java .util .Collections .synchronizedMap ;
2222import static org .wildfly .security .http .oidc .ElytronMessages .log ;
2323
24+ import java .net .MalformedURLException ;
2425import java .net .URISyntaxException ;
26+ import java .net .URL ;
2527import java .util .LinkedHashMap ;
2628import java .util .Map ;
2729
3840 */
3941final class LogoutHandler {
4042
41- public static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri" ;
42- public static final String ID_TOKEN_HINT_PARAM = "id_token_hint" ;
43+ private static final String POST_LOGOUT_REDIRECT_URI_PARAM = "post_logout_redirect_uri" ;
44+ private static final String ID_TOKEN_HINT_PARAM = "id_token_hint" ;
4345 private static final String LOGOUT_TOKEN_PARAM = "logout_token" ;
44- private static final String LOGOUT_TOKEN_TYPE = "Logout" ;
46+ private static final String LOGOUT_JWT_TOKEN_TYPE = "logout+jwt" ;
47+ private static final String KEYCLOCK_LOGOUT_TOKEN_TYPE = "Logout" ;
4548 private static final String CLIENT_ID_SID_SEPARATOR = "-" ;
46- public static final String SID = "sid" ;
47- public static final String ISS = "iss" ;
49+ private static final String SID = "sid" ;
50+ private static final String ISS = "iss" ;
4851
4952 /**
5053 * A bounded map to store sessions marked for invalidation after receiving logout requests through the back-channel
5154 */
5255 private Map <String , OidcClientConfiguration > sessionsMarkedForInvalidation = synchronizedMap (new LinkedHashMap <String , OidcClientConfiguration >(16 , 0.75f , true ) {
5356 @ Override
5457 protected boolean removeEldestEntry (Map .Entry <String , OidcClientConfiguration > eldest ) {
55- boolean remove = sessionsMarkedForInvalidation .size () > eldest .getValue ().getLogoutSessionWaitingLimit ();
58+ boolean remove = sessionsMarkedForInvalidation .size () > eldest .getValue ().getBackChannelLogoutSessionInvalidationLimit ();
5659
5760 if (remove ) {
58- log .debugf ("Limit [%s] reached for sessions waiting [%s] for logout" , eldest .getValue ().getLogoutSessionWaitingLimit (), sessionsMarkedForInvalidation .size ());
61+ log .debugf ("Limit [%s] reached for sessions waiting [%s] for logout" , eldest .getValue ().getBackChannelLogoutSessionInvalidationLimit (), sessionsMarkedForInvalidation .size ());
5962 }
6063
6164 return remove ;
6265 }
6366 });
6467
6568 boolean tryLogout (OidcHttpFacade facade ) {
66- log .trace ("tryLogout entered" );
6769 RefreshableOidcSecurityContext securityContext = getSecurityContext (facade );
6870 if (securityContext == null ) {
6971 // no active session
@@ -95,6 +97,7 @@ boolean tryLogout(OidcHttpFacade facade) {
9597 boolean isSessionMarkedForInvalidation (OidcHttpFacade facade ) {
9698 HttpScope session = facade .getScope (Scope .SESSION );
9799 if (session == null || ! session .exists ()) return false ;
100+
98101 RefreshableOidcSecurityContext securityContext = (RefreshableOidcSecurityContext ) session .getAttachment (OidcSecurityContext .class .getName ());
99102 if (securityContext == null ) {
100103 return false ;
@@ -104,7 +107,7 @@ boolean isSessionMarkedForInvalidation(OidcHttpFacade facade) {
104107 if (idToken == null ) {
105108 return false ;
106109 }
107- return sessionsMarkedForInvalidation .remove (getSessionKey ( facade , idToken .getSid () )) != null ;
110+ return sessionsMarkedForInvalidation .remove (idToken .getNonce ( )) != null ;
108111 }
109112
110113 private void redirectEndSessionEndpoint (OidcHttpFacade facade ) {
@@ -116,16 +119,17 @@ private void redirectEndSessionEndpoint(OidcHttpFacade facade) {
116119 try {
117120 URIBuilder redirectUriBuilder = new URIBuilder (clientConfiguration .getEndSessionEndpointUrl ())
118121 .addParameter (ID_TOKEN_HINT_PARAM , securityContext .getIDTokenString ());
119- String postLogoutPath = clientConfiguration .getPostLogoutPath ();
120- if (postLogoutPath != null ) {
121- redirectUriBuilder . addParameter ( POST_LOGOUT_REDIRECT_URI_PARAM ,
122- getRedirectUri ( facade ) + postLogoutPath );
122+ String postLogoutRedirectUri = clientConfiguration .getPostLogoutRedirectUri ();
123+ if (postLogoutRedirectUri != null ) {
124+ log . trace ( "post_logout_redirect_uri: " + postLogoutRedirectUri );
125+ redirectUriBuilder . addParameter ( POST_LOGOUT_REDIRECT_URI_PARAM , postLogoutRedirectUri );
123126 }
124127
125128 logoutUri = redirectUriBuilder .build ().toString ();
126- log .trace ("redirectEndSessionEndpoint path: " + redirectUriBuilder . toString () );
129+ log .trace ("redirectEndSessionEndpoint path: " + logoutUri );
127130 } catch (URISyntaxException e ) {
128- throw new RuntimeException (e );
131+ throw log .unableToCreateEndSessionEndpointRequest (
132+ clientConfiguration .getEndSessionEndpointUrl (), e .getMessage ());
129133 }
130134
131135 log .debugf ("Sending redirect to the end_session_endpoint: %s" , logoutUri );
@@ -134,7 +138,6 @@ private void redirectEndSessionEndpoint(OidcHttpFacade facade) {
134138 }
135139
136140 boolean tryBackChannelLogout (OidcHttpFacade facade ) {
137- log .trace ("tryBackChannelLogout entered" );
138141 if (isLogoutCallbackPath (facade )) {
139142 log .trace ("isLogoutCallbackPath" );
140143 if (isBackChannel (facade )) {
@@ -147,24 +150,48 @@ boolean tryBackChannelLogout(OidcHttpFacade facade) {
147150 }
148151
149152 private void handleBackChannelLogoutRequest (OidcHttpFacade facade ) {
153+
154+ OidcClientConfiguration clientConfiguration = facade .getOidcClientConfiguration ();
150155 String logoutToken = facade .getRequest ().getFirstParam (LOGOUT_TOKEN_PARAM );
151- TokenValidator tokenValidator = TokenValidator .builder (facade .getOidcClientConfiguration ())
152- .setSkipExpirationValidator ()
153- .setTokenType (LOGOUT_TOKEN_TYPE )
156+ TokenValidator .Builder tokenBuilder = TokenValidator .builder (clientConfiguration )
157+ .setSkipExpirationValidator ();
158+ // Keycloak uses claim type "Logout". Other OP's may be using "logout+jwt"
159+ // or a typ unique to it.
160+ String providerLogoutTokenType = (facade .getOidcClientConfiguration ().getProviderJwtClaimsTyp () == null ) ?
161+ KEYCLOCK_LOGOUT_TOKEN_TYPE : clientConfiguration .getProviderJwtClaimsTyp ();
162+ TokenValidator tokenValidator = tokenBuilder .setTokenType (providerLogoutTokenType )
154163 .build ();
155- JwtClaims claims ;
156164
165+ JwtClaims claims = null ;
166+ Exception cause = null ;
157167 try {
168+ // check keycloak 'typ'
158169 claims = tokenValidator .verify (logoutToken );
159- } catch (Exception cause ) {
160- log .debug ("Unexpected error when verifying logout token" , cause );
161- facade .getResponse ().setStatus (HttpStatus .SC_BAD_REQUEST );
162- facade .authenticationFailed ();
163- return ;
170+ } catch (Exception expKeyclockClaims ) {
171+ cause = expKeyclockClaims ;
172+ if (expKeyclockClaims .getCause ().getMessage ().contains ("ELY23054: Unexpected value for typ claim" )) {
173+ log .warn ("OpenID Provider claims typ " + providerLogoutTokenType
174+ + " was not valid. Trying typ " + LOGOUT_JWT_TOKEN_TYPE );
175+
176+ // check other OP's 'typ'
177+ tokenValidator = tokenBuilder .setTokenType (LOGOUT_JWT_TOKEN_TYPE )
178+ .build ();
179+ try {
180+ claims = tokenValidator .verify (logoutToken );
181+ } catch (Exception expOtherProviderCliams ) {
182+ cause = expOtherProviderCliams ;
183+ }
184+ }
185+ if (claims == null ) {
186+ log .debugf ("Unexpected error when verifying logout token" , cause );
187+ facade .getResponse ().setStatus (HttpStatus .SC_BAD_REQUEST );
188+ facade .authenticationFailed ();
189+ return ;
190+ }
164191 }
165192
166- if (!isSessionRequiredOnLogout (facade )) {
167- log .warn ("Back-channel logout request received but can not infer sid from logout token to mark it for invalidation" );
193+ if (!isLogoutSessionRequired (facade )) {
194+ log .warn (log . sidCanNotBeInferFromLogoutToken () );
168195 facade .getResponse ().setStatus (HttpStatus .SC_BAD_REQUEST );
169196 facade .authenticationFailed ();
170197 return ;
@@ -178,7 +205,7 @@ private void handleBackChannelLogoutRequest(OidcHttpFacade facade) {
178205 return ;
179206 }
180207
181- log .debug ("Marking session for invalidation during back-channel logout" );
208+ log .debugf ("Marking session for invalidation during back-channel logout" );
182209 sessionsMarkedForInvalidation .put (getSessionKey (facade , sessionId ), facade .getOidcClientConfiguration ());
183210 }
184211
@@ -187,7 +214,7 @@ private String getSessionKey(OidcHttpFacade facade, String sessionId) {
187214 }
188215
189216 private void handleFrontChannelLogoutRequest (OidcHttpFacade facade ) {
190- if (isSessionRequiredOnLogout (facade )) {
217+ if (isLogoutSessionRequired (facade )) {
191218 Request request = facade .getRequest ();
192219 String sessionId = request .getQueryParamValue (SID );
193220
@@ -201,14 +228,14 @@ private void handleFrontChannelLogoutRequest(OidcHttpFacade facade) {
201228 IDToken idToken = context .getIDToken ();
202229 String issuer = request .getQueryParamValue (ISS );
203230
204- if (idToken == null || !sessionId .equals (idToken .getSid ()) || !idToken .getIssuer ().equals (issuer )) {
231+ if (idToken == null || !sessionId .equals (idToken .getNonce ()) || !idToken .getIssuer ().equals (issuer )) {
205232 facade .getResponse ().setStatus (HttpStatus .SC_BAD_REQUEST );
206233 facade .authenticationFailed ();
207234 return ;
208235 }
209236 }
210237
211- log .debug ("Invalidating session during front-channel logout" );
238+ log .debugf ("Invalidating session during front-channel logout" );
212239 facade .getTokenStore ().logout (false );
213240 }
214241
@@ -228,17 +255,33 @@ private String getRedirectUri(OidcHttpFacade facade) {
228255 }
229256
230257 private boolean isLogoutCallbackPath (OidcHttpFacade facade ) {
231- String path = facade .getRequest ().getRelativePath ();
232- return path .endsWith (getLogoutCallbackPath (facade ));
258+ String uriStr = facade .getRequest ().getURI ();
259+ // logoutCallbackPath can be either an URL path component or an absolute path.
260+ // Only the path components are to be compared.
261+ String tmpLogoutCallbackPath = getLogoutCallbackPath (facade );
262+ try {
263+ URL url = new URL (tmpLogoutCallbackPath );
264+ if (uriStr .equals (url .toString ())
265+ || uriStr .endsWith (tmpLogoutCallbackPath )) {
266+ return true ;
267+ }
268+ } catch (MalformedURLException e ) {
269+ // no-op
270+ }
271+ return false ;
233272 }
234273
235274 private boolean isRpInitiatedLogoutPath (OidcHttpFacade facade ) {
236275 String path = facade .getRequest ().getRelativePath ();
237- return path .endsWith (getLogoutPath (facade ));
276+ String logoutPath = getLogoutPath (facade );
277+ if (logoutPath == null ) {
278+ return false ;
279+ }
280+ return path .endsWith (logoutPath );
238281 }
239282
240- private boolean isSessionRequiredOnLogout (OidcHttpFacade facade ) {
241- return facade .getOidcClientConfiguration ().isSessionRequiredOnLogout ();
283+ private boolean isLogoutSessionRequired (OidcHttpFacade facade ) {
284+ return facade .getOidcClientConfiguration ().isLogoutSessionRequired ();
242285 }
243286
244287 private RefreshableOidcSecurityContext getSecurityContext (OidcHttpFacade facade ) {
0 commit comments