Skip to content

Commit 1f5053b

Browse files
committed
Implement Front-Channel Logout endpoint
Implement OpenID Connect Front-Channel Logout 1.0 specification: - Add default /front_channel_logout location that handles logout requests - Both sid and iss parameters must be present - Issuer verification against iss claim in ID token Reference: https://openid.net/specs/openid-connect-frontchannel-1_0.html
1 parent 66c4eaa commit 1f5053b

File tree

4 files changed

+107
-9
lines changed

4 files changed

+107
-9
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ Requests made to the `/logout` location invalidate both the ID token, access tok
100100

101101
RP-initiated logout is supported according to [OpenID Connect RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html). This behavior is controlled by the `$oidc_end_session_endpoint` variable.
102102

103+
#### Front-Channel OIDC Logout
104+
105+
Front-Channel Logout is supported according to [OpenID Connect Front-Channel Logout 1.0](https://openid.net/specs/openid-connect-frontchannel-1_0.html). The `/front_channel_logout endpoint` location handles logout requests from the IdP. Both arguments, `sid` (session identifier) and `iss` (issuer identifier), must be present.
106+
103107
### Multiple IdPs
104108

105109
Where NGINX Plus is configured to proxy requests for multiple websites or applications, or user groups, these may require authentication by different IdPs. Separate IdPs can be configured, with each one matching on an attribute of the HTTP request, e.g. hostname or part of the URI path.
@@ -198,6 +202,7 @@ The key-value store is used to maintain persistent storage for ID tokens and ref
198202
keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
199203
keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
200204
keyval_zone zone=refresh_tokens:1M state=/var/lib/nginx/state/refresh_tokens.json timeout=8h;
205+
keyval_zone zone=oidc_sids:1M state=/var/lib/nginx/state/oidc_sids.json timeout=8h;
201206
keyval_zone zone=oidc_pkce:128K timeout=90s;
202207
```
203208

@@ -314,3 +319,4 @@ This reference implementation for OpenID Connect is supported for NGINX Plus sub
314319
* **R23** PKCE support. Added support for deployments behind another proxy or load balancer.
315320
* **R28** Access token support. Added support for access token to authorize NGINX to access protected backend.
316321
* **R32** Added support for `client_secret_basic` client authentication method.
322+
* **R33** Refactor code to use async/await. Implement Front-Channel Logout endpoint.

openid_connect.js

Lines changed: 91 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export default {
88
auth,
99
codeExchange,
1010
extractTokenClaims,
11-
logout
11+
logout,
12+
handleFrontChannelLogout
1213
};
1314

1415
// The main authentication flow, called before serving a protected resource.
@@ -42,7 +43,7 @@ async function auth(r, afterSyncCheck) {
4243

4344
// Determine session ID and store session data
4445
const sessionId = getSessionId(r, false);
45-
storeSessionData(r, tokenset, false);
46+
storeSessionData(r, sessionId, claims, tokenset, true);
4647

4748
r.log("OIDC success, refreshing session " + sessionId);
4849

@@ -79,7 +80,7 @@ async function codeExchange(r) {
7980

8081
// Determine session ID and store session data for a new session
8182
const sessionId = getSessionId(r, true);
82-
storeSessionData(r, tokenset, true);
83+
storeSessionData(r, sessionId, claims, tokenset, true);
8384

8485
r.log("OIDC success, creating session " + sessionId);
8586

@@ -173,7 +174,12 @@ function validateIdTokenClaims(r, claims) {
173174
}
174175

175176
// Store session data in the key-val store
176-
function storeSessionData(r, tokenset, isNewSession) {
177+
function storeSessionData(r, sessionId, claims, tokenset, isNewSession) {
178+
if (claims.sid) {
179+
r.variables.idp_sid = claims.sid;
180+
r.variables.client_sid = sessionId;
181+
}
182+
177183
if (isNewSession) {
178184
r.variables.new_session = tokenset.id_token;
179185
r.variables.new_access_token = tokenset.access_token || "";
@@ -190,7 +196,7 @@ function storeSessionData(r, tokenset, isNewSession) {
190196
// Extracts claims from the validated ID Token (used by /_token_validation)
191197
function extractTokenClaims(r) {
192198
const claims = {};
193-
const claimNames = ["sub", "iss", "iat", "nonce"];
199+
const claimNames = ["sub", "iss", "iat", "nonce", "sid"];
194200

195201
claimNames.forEach((name) => {
196202
const value = r.variables["jwt_claim_" + name];
@@ -277,7 +283,7 @@ async function refreshTokens(r) {
277283

278284
// Logout handler
279285
function logout(r) {
280-
r.log("RP-Initiated Logout for " + (r.variables.cookie_auth_token || "unknown"));
286+
r.log("OIDC RP-Initiated Logout for " + (r.variables.cookie_auth_token || "unknown"));
281287

282288
function getLogoutRedirectUrl(base, redirect) {
283289
return redirect.match(/^(http|https):\/\//) ? redirect : base + redirect;
@@ -286,7 +292,16 @@ function logout(r) {
286292
var logoutRedirectUrl = getLogoutRedirectUrl(r.variables.redirect_base,
287293
r.variables.oidc_logout_redirect);
288294

289-
function performLogout(redirectUrl) {
295+
async function performLogout(redirectUrl, idToken) {
296+
// Clean up $idp_sid -> $client_sid mapping
297+
if (idToken && idToken !== '-') {
298+
const claims = await getTokenClaims(r, idToken);
299+
if (claims.sid) {
300+
r.variables.idp_sid = claims.sid;
301+
r.variables.client_sid = '-';
302+
}
303+
}
304+
290305
r.variables.session_jwt = '-';
291306
r.variables.access_token = '-';
292307
r.variables.refresh_token = '-';
@@ -305,12 +320,79 @@ function logout(r) {
305320

306321
var logoutArgs = "?post_logout_redirect_uri=" + encodeURIComponent(logoutRedirectUrl) +
307322
"&id_token_hint=" + encodeURIComponent(r.variables.session_jwt);
308-
performLogout(r.variables.oidc_end_session_endpoint + logoutArgs);
323+
performLogout(r.variables.oidc_end_session_endpoint + logoutArgs, r.variables.session_jwt);
309324
} else {
310-
performLogout(logoutRedirectUrl);
325+
performLogout(logoutRedirectUrl, r.variables.session_jwt);
311326
}
312327
}
313328

329+
/**
330+
* Handles Front-Channel Logout as per OpenID Connect Front-Channel Logout 1.0 spec.
331+
* @see https://openid.net/specs/openid-connect-frontchannel-1_0.html
332+
*/
333+
async function handleFrontChannelLogout(r) {
334+
const sid = r.args.sid;
335+
const requestIss = r.args.iss;
336+
337+
// Validate input parameters
338+
if (!sid) {
339+
r.error("Missing sid parameter in front-channel logout request");
340+
r.return(400, "Missing sid");
341+
return;
342+
}
343+
344+
if (!requestIss) {
345+
r.error("Missing iss parameter in front-channel logout request");
346+
r.return(400, "Missing iss");
347+
return;
348+
}
349+
350+
r.log("OIDC Front-Channel Logout initiated for sid: " + sid);
351+
352+
// Define idp_sid as a key to get the client_sid from the key-value store
353+
r.variables.idp_sid = sid;
354+
355+
const clientSid = r.variables.client_sid;
356+
if (!clientSid || clientSid === '-') {
357+
r.log("No client session found for sid: " + sid);
358+
r.return(200, "Logout successful");
359+
return;
360+
}
361+
362+
/* TODO: Since we cannot use the cookie_auth_token var as a key (it does not exist if cookies
363+
are absent), we use the request_id as a workaround. */
364+
r.variables.request_id = clientSid;
365+
var sessionJwt = r.variables.new_session;
366+
367+
if (!sessionJwt || sessionJwt === '-') {
368+
r.log("No associated ID token found for client session: " + clientSid);
369+
cleanSessionData(r);
370+
r.return(200, "Logout successful");
371+
return;
372+
}
373+
374+
const claims = await getTokenClaims(r, sessionJwt);
375+
if (claims.iss !== requestIss) {
376+
r.error("Issuer mismatch during logout. Received iss: " +
377+
requestIss + ", expected: " + claims.iss);
378+
r.return(400, "Issuer mismatch");
379+
return;
380+
}
381+
382+
// idp_sid needs to be updated after subrequest
383+
r.variables.idp_sid = sid;
384+
cleanSessionData(r);
385+
386+
r.return(200, "Logout successful");
387+
}
388+
389+
function cleanSessionData(r) {
390+
r.variables.new_session = '-';
391+
r.variables.new_access_token = '-';
392+
r.variables.new_refresh = '-';
393+
r.variables.client_sid = '-';
394+
}
395+
314396
// Initiate a new authentication flow by redirecting to the IdP's authorization endpoint
315397
function initiateNewAuth(r) {
316398
const oidcConfigurables = ["authz_endpoint", "scopes", "hmac_key", "cookie_flags"];

openid_connect.server_conf

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Advanced configuration START
22
set $internal_error_message "NGINX / OpenID Connect login failure\n";
33
set $pkce_id "";
4+
set $idp_sid "";
45
resolver 8.8.8.8; # For DNS lookup of IdP endpoints;
56
subrequest_output_buffer_size 32k; # To fit a complete tokenset response
67
gunzip on; # Decompress IdP responses if necessary
@@ -79,6 +80,13 @@
7980
js_content oidc.logout;
8081
}
8182

83+
location = /front_channel_logout {
84+
status_zone "OIDC logout";
85+
add_header Cache-Control "no-store";
86+
default_type text/plain;
87+
js_content oidc.handleFrontChannelLogout;
88+
}
89+
8290
location = /_logout {
8391
# This location is the default value of $oidc_logout_redirect (in case it wasn't configured)
8492
default_type text/plain;

openid_connect_configuration.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m;
104104
keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
105105
keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
106106
keyval_zone zone=refresh_tokens:1M state=/var/lib/nginx/state/refresh_tokens.json timeout=8h;
107+
keyval_zone zone=oidc_sids:1M state=/var/lib/nginx/state/oidc_sids.json timeout=8h;
107108
keyval_zone zone=oidc_pkce:128K timeout=90s; # Temporary storage for PKCE code verifier.
108109

109110
keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT
@@ -113,6 +114,7 @@ keyval $request_id $new_session zone=oidc_id_tokens; # For initial
113114
keyval $request_id $new_access_token zone=oidc_access_tokens;
114115
keyval $request_id $new_refresh zone=refresh_tokens; # ''
115116
keyval $pkce_id $pkce_code_verifier zone=oidc_pkce;
117+
keyval $idp_sid $client_sid zone=oidc_sids;
116118

117119
auth_jwt_claim_set $jwt_audience aud; # In case aud is an array
118120
js_import oidc from conf.d/openid_connect.js;

0 commit comments

Comments
 (0)