Skip to content

Commit 0fbe529

Browse files
authored
Add OIDC fix for ID token nonce claim validation (#6761)
1 parent d21cca6 commit 0fbe529

File tree

2 files changed

+44
-20
lines changed

2 files changed

+44
-20
lines changed

Diff for: .github/scripts/variables.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ get_docker_md5() {
1818
}
1919

2020
get_go_code_md5() {
21-
find . -type f \( -name "*.go" -o -name go.mod -o -name go.sum -o -name "*.tmpl" -o -name "version.txt" \) -not -path "./site*" -exec md5sum {} + | LC_ALL=C sort | md5sum | awk '{ print $1 }'
21+
find . -type f \( -name "*.go" -o -name go.mod -o -name go.sum -o -name "*.tmpl" -o -name "version.txt" -o -name "*.js" \) -not -path "./site*" -exec md5sum {} + | LC_ALL=C sort | md5sum | awk '{ print $1 }'
2222
}
2323

2424
get_tests_md5() {

Diff for: internal/configs/oidc/openid_connect.js

+43-19
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
*
44
* Copyright (C) 2020 Nginx, Inc.
55
*/
6-
var newSession = false; // Used by oidcAuth() and validateIdToken()
7-
86
export default {auth, codeExchange, validateIdToken, logout};
97

108
function retryOriginalRequest(r) {
@@ -32,8 +30,6 @@ function auth(r, afterSyncCheck) {
3230
}
3331

3432
if (!r.variables.refresh_token || r.variables.refresh_token == "-") {
35-
newSession = true;
36-
3733
// Check we have all necessary configuration variables (referenced only by njs)
3834
var oidcConfigurables = ["authz_endpoint", "scopes", "hmac_key", "cookie_flags"];
3935
var missingConfig = [];
@@ -54,7 +50,7 @@ function auth(r, afterSyncCheck) {
5450

5551
// Pass the refresh token to the /_refresh location so that it can be
5652
// proxied to the IdP in exchange for a new id_token
57-
r.subrequest("/_refresh", "token=" + r.variables.refresh_token,
53+
r.subrequest("/_refresh", generateTokenRequestParams(r, "refresh_token"),
5854
function(reply) {
5955
if (reply.status != 200) {
6056
// Refresh request failed, log the reason
@@ -142,7 +138,7 @@ function codeExchange(r) {
142138

143139
// Pass the authorization code to the /_token location so that it can be
144140
// proxied to the IdP in exchange for a JWT
145-
r.subrequest("/_token",idpClientAuth(r), function(reply) {
141+
r.subrequest("/_token", generateTokenRequestParams(r, "authorization_code"), function(reply) {
146142
if (reply.status == 504) {
147143
r.error("OIDC timeout connecting to IdP when sending authorization code");
148144
r.return(504);
@@ -200,7 +196,7 @@ function codeExchange(r) {
200196

201197
r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags;
202198
r.return(302, r.variables.redirect_base + decodeURIComponent(r.variables.cookie_auth_redir));
203-
}
199+
}
204200
);
205201
} catch (e) {
206202
r.error("OIDC authorization code sent but token response is not JSON. " + reply.responseText);
@@ -241,10 +237,9 @@ function validateIdToken(r) {
241237
validToken = false;
242238
}
243239

244-
// If we receive a nonce in the ID Token then we will use the auth_nonce cookies
245-
// to check that the JWT can be validated as being directly related to the
246-
// original request by this client. This mitigates against token replay attacks.
247-
if (newSession) {
240+
// According to OIDC Core 1.0 Section 2:
241+
// "If present in the ID Token, Clients MUST verify that the nonce Claim Value is equal to the value of the nonce parameter sent in the Authentication Request."
242+
if (r.variables.jwt_claim_nonce) {
248243
var client_nonce_hash = "";
249244
if (r.variables.cookie_auth_nonce) {
250245
var c = require('crypto');
@@ -255,6 +250,9 @@ function validateIdToken(r) {
255250
r.error("OIDC ID Token validation error: nonce from token (" + r.variables.jwt_claim_nonce + ") does not match client (" + client_nonce_hash + ")");
256251
validToken = false;
257252
}
253+
} else if (!r.variables.refresh_token || r.variables.refresh_token == "-") {
254+
r.error("OIDC ID Token validation error: missing nonce claim in ID Token during initial authentication.");
255+
validToken = false;
258256
}
259257

260258
if (validToken) {
@@ -297,7 +295,7 @@ function logout(r) {
297295

298296
// Construct logout arguments for RP-initiated logout
299297
var logoutArgs = "?post_logout_redirect_uri=" + encodeURIComponent(logoutRedirectUrl) +
300-
"&id_token_hint=" + encodeURIComponent(r.variables.session_jwt);
298+
"&id_token_hint=" + encodeURIComponent(r.variables.session_jwt);
301299
performLogout(r.variables.oidc_end_session_endpoint + logoutArgs);
302300
} else {
303301
// Fallback to traditional logout approach
@@ -337,12 +335,38 @@ function getAuthZArgs(r) {
337335
return authZArgs;
338336
}
339337

340-
function idpClientAuth(r) {
341-
// If PKCE is enabled we have to use the code_verifier
342-
if ( r.variables.oidc_pkce_enable == 1 ) {
343-
r.variables.pkce_id = r.variables.arg_state;
344-
return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier;
345-
} else {
346-
return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret;
338+
function generateTokenRequestParams(r, grant_type) {
339+
var body = "grant_type=" + grant_type + "&client_id=" + r.variables.oidc_client;
340+
341+
switch(grant_type) {
342+
case "authorization_code":
343+
body += "&code=" + r.variables.arg_code + "&redirect_uri=" + r.variables.redirect_base + r.variables.redir_location;
344+
if (r.variables.oidc_pkce_enable == 1) {
345+
r.variables.pkce_id = r.variables.arg_state;
346+
body += "&code_verifier=" + r.variables.pkce_code_verifier;
347+
}
348+
break;
349+
case "refresh_token":
350+
body += "&refresh_token=" + r.variables.refresh_token;
351+
break;
352+
default:
353+
r.error("Unsupported grant type: " + grant_type);
354+
return;
347355
}
356+
357+
var options = {
358+
body: body,
359+
method: "POST"
360+
};
361+
362+
if (r.variables.oidc_pkce_enable != 1) {
363+
if (r.variables.oidc_client_auth_method === "client_secret_basic") {
364+
let auth_basic = "Basic " + Buffer.from(r.variables.oidc_client + ":" + r.variables.oidc_client_secret).toString('base64');
365+
options.args = "secret_basic=" + auth_basic;
366+
} else {
367+
options.body += "&client_secret=" + r.variables.oidc_client_secret;
368+
}
369+
}
370+
371+
return options;
348372
}

0 commit comments

Comments
 (0)