github.com/nginxinc/kubernetes-ingress@v1.12.5/internal/configs/oidc/openid_connect.js (about) 1 /* 2 * JavaScript functions for providing OpenID Connect with NGINX Plus 3 * 4 * Copyright (C) 2020 Nginx, Inc. 5 */ 6 var newSession = false; // Used by oidcAuth() and validateIdToken() 7 8 export default { auth, codeExchange, validateIdToken, logout }; 9 10 function auth(r) { 11 if (!r.variables.refresh_token || r.variables.refresh_token == "-") { 12 newSession = true; 13 14 // Check we have all necessary configuration variables (referenced only by njs) 15 var oidcConfigurables = ["authz_endpoint", "scopes", "hmac_key", "cookie_flags"]; 16 var missingConfig = []; 17 for (var i in oidcConfigurables) { 18 if (!r.variables["oidc_" + oidcConfigurables[i]] || r.variables["oidc_" + oidcConfigurables[i]] == "") { 19 missingConfig.push(oidcConfigurables[i]); 20 } 21 } 22 if (missingConfig.length) { 23 r.error("OIDC missing configuration variables: $oidc_" + missingConfig.join(" $oidc_")); 24 r.return(500, r.variables.internal_error_message); 25 return; 26 } 27 // Redirect the client to the IdP login page with the cookies we need for state 28 r.return(302, r.variables.oidc_authz_endpoint + getAuthZArgs(r)); 29 return; 30 } 31 32 // Pass the refresh token to the /_refresh location so that it can be 33 // proxied to the IdP in exchange for a new id_token 34 r.subrequest("/_refresh", "token=" + r.variables.refresh_token, 35 function (reply) { 36 if (reply.status != 200) { 37 // Refresh request failed, log the reason 38 var error_log = "OIDC refresh failure"; 39 if (reply.status == 504) { 40 error_log += ", timeout waiting for IdP"; 41 } else if (reply.status == 400) { 42 try { 43 var errorset = JSON.parse(reply.responseBody); 44 error_log += ": " + errorset.error + " " + errorset.error_description; 45 } catch (e) { 46 error_log += ": " + reply.responseBody; 47 } 48 } else { 49 error_log += " " + reply.status; 50 } 51 r.error(error_log); 52 53 // Clear the refresh token, try again 54 r.variables.refresh_token = "-"; 55 r.return(302, r.variables.request_uri); 56 return; 57 } 58 59 // Refresh request returned 200, check response 60 try { 61 var tokenset = JSON.parse(reply.responseBody); 62 if (!tokenset.id_token) { 63 r.error("OIDC refresh response did not include id_token"); 64 if (tokenset.error) { 65 r.error("OIDC " + tokenset.error + " " + tokenset.error_description); 66 } 67 r.variables.refresh_token = "-"; 68 r.return(302, r.variables.request_uri); 69 return; 70 } 71 72 // Send the new ID Token to auth_jwt location for validation 73 r.subrequest("/_id_token_validation", "token=" + tokenset.id_token, 74 function (reply) { 75 if (reply.status != 204) { 76 r.variables.refresh_token = "-"; 77 r.return(302, r.variables.request_uri); 78 return; 79 } 80 81 // ID Token is valid, update keyval 82 r.log("OIDC refresh success, updating id_token for " + r.variables.cookie_auth_token); 83 r.variables.session_jwt = tokenset.id_token; // Update key-value store 84 85 // Update refresh token (if we got a new one) 86 if (r.variables.refresh_token != tokenset.refresh_token) { 87 r.log("OIDC replacing previous refresh token (" + r.variables.refresh_token + ") with new value: " + tokenset.refresh_token); 88 r.variables.refresh_token = tokenset.refresh_token; // Update key-value store 89 } 90 91 delete r.headersOut["WWW-Authenticate"]; // Remove evidence of original failed auth_jwt 92 r.internalRedirect(r.variables.request_uri); // Continue processing original request 93 } 94 ); 95 } catch (e) { 96 r.variables.refresh_token = "-"; 97 r.return(302, r.variables.request_uri); 98 return; 99 } 100 } 101 ); 102 } 103 104 function codeExchange(r) { 105 // First check that we received an authorization code from the IdP 106 if (r.variables.arg_code.length == 0) { 107 if (r.variables.arg_error) { 108 r.error("OIDC error receiving authorization code from IdP: " + r.variables.arg_error_description); 109 } else { 110 r.error("OIDC expected authorization code from IdP but received: " + r.uri); 111 } 112 r.return(502); 113 return; 114 } 115 116 // Pass the authorization code to the /_token location so that it can be 117 // proxied to the IdP in exchange for a JWT 118 r.subrequest("/_token", idpClientAuth(r), function (reply) { 119 if (reply.status == 504) { 120 r.error("OIDC timeout connecting to IdP when sending authorization code"); 121 r.return(504); 122 return; 123 } 124 125 if (reply.status != 200) { 126 try { 127 var errorset = JSON.parse(reply.responseBody); 128 if (errorset.error) { 129 r.error("OIDC error from IdP when sending authorization code: " + errorset.error + ", " + errorset.error_description); 130 } else { 131 r.error("OIDC unexpected response from IdP when sending authorization code (HTTP " + reply.status + "). " + reply.responseBody); 132 } 133 } catch (e) { 134 r.error("OIDC unexpected response from IdP when sending authorization code (HTTP " + reply.status + "). " + reply.responseBody); 135 } 136 r.return(502); 137 return; 138 } 139 140 // Code exchange returned 200, check for errors 141 try { 142 var tokenset = JSON.parse(reply.responseBody); 143 if (tokenset.error) { 144 r.error("OIDC " + tokenset.error + " " + tokenset.error_description); 145 r.return(500); 146 return; 147 } 148 149 // Send the ID Token to auth_jwt location for validation 150 r.subrequest("/_id_token_validation", "token=" + tokenset.id_token, 151 function (reply) { 152 if (reply.status != 204) { 153 r.return(500); // validateIdToken() will log errors 154 return; 155 } 156 157 // If the response includes a refresh token then store it 158 if (tokenset.refresh_token) { 159 r.variables.new_refresh = tokenset.refresh_token; // Create key-value store entry 160 r.log("OIDC refresh token stored"); 161 } else { 162 r.warn("OIDC no refresh token"); 163 } 164 165 // Add opaque token to keyval session store 166 r.log("OIDC success, creating session " + r.variables.request_id); 167 r.variables.new_session = tokenset.id_token; // Create key-value store entry 168 r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags; 169 r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir); 170 } 171 ); 172 } catch (e) { 173 r.error("OIDC authorization code sent but token response is not JSON. " + reply.responseBody); 174 r.return(502); 175 } 176 } 177 ); 178 } 179 180 function validateIdToken(r) { 181 // Check mandatory claims 182 var required_claims = ["iat", "iss", "sub"]; // aud is checked separately 183 var missing_claims = []; 184 for (var i in required_claims) { 185 if (r.variables["jwt_claim_" + required_claims[i]].length == 0) { 186 missing_claims.push(required_claims[i]); 187 } 188 } 189 if (r.variables.jwt_audience.length == 0) missing_claims.push("aud"); 190 if (missing_claims.length) { 191 r.error("OIDC ID Token validation error: missing claim(s) " + missing_claims.join(" ")); 192 r.return(403); 193 return; 194 } 195 var validToken = true; 196 197 // Check iat is a positive integer 198 var iat = Math.floor(Number(r.variables.jwt_claim_iat)); 199 if (String(iat) != r.variables.jwt_claim_iat || iat < 1) { 200 r.error("OIDC ID Token validation error: iat claim is not a valid number"); 201 validToken = false; 202 } 203 204 // Audience matching 205 var aud = r.variables.jwt_audience.split(","); 206 if (!aud.includes(r.variables.oidc_client)) { 207 r.error("OIDC ID Token validation error: aud claim (" + r.variables.jwt_audience + ") does not include configured $oidc_client (" + r.variables.oidc_client + ")"); 208 validToken = false; 209 } 210 211 // If we receive a nonce in the ID Token then we will use the auth_nonce cookies 212 // to check that the JWT can be validated as being directly related to the 213 // original request by this client. This mitigates against token replay attacks. 214 if (newSession) { 215 var client_nonce_hash = ""; 216 if (r.variables.cookie_auth_nonce) { 217 var c = require('crypto'); 218 var h = c.createHmac('sha256', r.variables.oidc_hmac_key).update(r.variables.cookie_auth_nonce); 219 client_nonce_hash = h.digest('base64url'); 220 } 221 if (r.variables.jwt_claim_nonce != client_nonce_hash) { 222 r.error("OIDC ID Token validation error: nonce from token (" + r.variables.jwt_claim_nonce + ") does not match client (" + client_nonce_hash + ")"); 223 validToken = false; 224 } 225 } 226 227 if (validToken) { 228 r.return(204); 229 } else { 230 r.return(403); 231 } 232 } 233 234 function logout(r) { 235 r.log("OIDC logout for " + r.variables.cookie_auth_token); 236 r.variables.session_jwt = "-"; 237 r.variables.refresh_token = "-"; 238 r.return(302, r.variables.oidc_logout_redirect); 239 } 240 241 function getAuthZArgs(r) { 242 // Choose a nonce for this flow for the client, and hash it for the IdP 243 var noncePlain = r.variables.request_id; 244 var c = require('crypto'); 245 var h = c.createHmac('sha256', r.variables.oidc_hmac_key).update(noncePlain); 246 var nonceHash = h.digest('base64url'); 247 var authZArgs = "?response_type=code&scope=" + r.variables.oidc_scopes + "&client_id=" + r.variables.oidc_client + "&redirect_uri=" + r.variables.redirect_base + r.variables.redir_location + "&nonce=" + nonceHash; 248 249 r.headersOut['Set-Cookie'] = [ 250 "auth_redir=" + r.variables.request_uri + "; " + r.variables.oidc_cookie_flags, 251 "auth_nonce=" + noncePlain + "; " + r.variables.oidc_cookie_flags 252 ]; 253 254 if (r.variables.oidc_pkce_enable == 1) { 255 var pkce_code_verifier = c.createHmac('sha256', r.variables.oidc_hmac_key).update(String(Math.random())).digest('hex'); 256 r.variables.pkce_id = c.createHash('sha256').update(String(Math.random())).digest('base64url'); 257 var pkce_code_challenge = c.createHash('sha256').update(pkce_code_verifier).digest('base64url'); 258 r.variables.pkce_code_verifier = pkce_code_verifier; 259 260 authZArgs += "&code_challenge_method=S256&code_challenge=" + pkce_code_challenge + "&state=" + r.variables.pkce_id; 261 } else { 262 authZArgs += "&state=0"; 263 } 264 return authZArgs; 265 } 266 267 function idpClientAuth(r) { 268 // If PKCE is enabled we have to use the code_verifier 269 if (r.variables.oidc_pkce_enable == 1) { 270 r.variables.pkce_id = r.variables.arg_state; 271 return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier; 272 } else { 273 return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret; 274 } 275 }