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  }