0

I've implemented the OAuth2 Authorization Code Flow (without PKCE yet) in NextJS with the openid-client. Now where I should store the code_verifier and how I could pass it to the invoked callback at server-side.

What I did so far (sketch):

// on server-side:
import { BaseClient, Issuer, custom, generators } from 'openid-client';
const issuer = await Issuer.discover('https://www.my-oidc-provider-endpoint');
const client = new issuer.Client({
  client_id: 'myClientId',
  client_secret: 'myClientSecret',
  redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback`,
});
// This must happen on server-side b/c the library is not available in the browser:
// const code_verifier = generators.codeVerifier();
// const code_challenge = generators.codeChallenge(code_verifier);
const authorizationUrl = client.authorizationUrl({
  scope: 'openid email profile rights',
  // code_challenge,
  // code_challenge_method: 'S256',
})
// on client-side
// the authorizationUrl was passed via getStaticProps
window.location.assign(authorizationUrl)

and in my api endpoint at api/auth/callback, called once the user has entered their credentials at https://www.my-oidc-provider-endpoint.tld:

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const client = await authClientProvider.getClient();
  const params = client.callbackParams(req);
  const tokenSet = await client.callback(`${APP_URL}/api/auth/callback`, params/* , {code_verifier}*/);
  setTokenCookies(req, res, tokenSet.access_token, tokenSet.refresh_token); // implemented elsewhere
  return res.redirect(APP_URL);
}

This works all fine, but doesn't implement PKCE. So my question is:

At the moment, I would pass the code_verifier to the client-side as a part of authorizationUrl. Does this suffice? But how would I make it available to my callback endpoint api/auth/callback, which is for all I understand pure server-side? Do I have to utilize the state param and cache the code_verifier with the state as key at the server-side?

I've found a similar question, but the questioner don't seem to be troubled with storing it in cookies (I would have to scrape it from the authorization_url first, and for what reason anyway? If it was for security, I could encrypt it at the server-side), and they don't seem to be troubled with passing it to the callback handler at the server-side. I could generate my code_verifier directly on the client-side (without the help of the generators from openid-client), but this doesn't strike me as intended, and I still wouldn't be able to pass it to the api callback.

(Sidenote: I don't want to use NextAuth. Actually, we came from there, but it was causing too much trouble.)

NotX
  • 1,516
  • 1
  • 14
  • 28

1 Answers1

1

If you can store it server side (encrypting it is not a bad idea), then that's typically the better route. The trouble comes on the redirect and looking which code_verifier for the token exchange. You can create something like a session id to and use that as a key for lookup: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-properties

The other route would be to encrypt and send it in a cookie, making sure to take proper precautions with the cookie: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#server-security The cookie should make the round trip back where you can decrypt and use it for the token exchange. The risk here is that someone can access and decrypt (if not sufficiently encrypted) your code_verifier since it's being passed through network calls.


Edit 9/2/2023

to add some additional detail (originally added as comments, but it became long and thought it might be helpful for others):

It's not an additional round trip to use the cookie. Be careful because client can be used in slightly different ways.

The "OAuth client" in your scenario would refer to your server RFC6749.

client.authorizationUrl() used in open-id is just a placeholder function call. Its implementation should tell the browser/user-agent to redirect to the authz server at the /authorize endpoint. This is the authz request. PKCE requires the code_challenge and code_challenge_method to be included in this request.

The user authenticates and then the user-agent is sent to the redirect_uri.

At the redirect_uri, your server gets the code through the browser because the user needs to be able to return to your app. Follow (c) in the diagram here.

Then, your server, or client in that same diagram, makes a request directly to the /token endpoint of the authz server, and not through the browser. Follow (d) in that diagram. The request includes the code and code_verifier.

The cookie is created on your server and sent to the browser and it stores it. When the user is sent back to the redirect_uri via the browser, the call to your server includes the cookie.

Or, said another way:

  1. the server creates a code_verifier, encrypts, & stores in a cookie
  2. the cookie & authz url sent from server to browser
  3. the browser stores cookie
  4. the browser redirects to the /authorize endpoint
  5. ...
  6. the browser redirects to the redirect_uri
  7. the browser sends code and the cookie to the server
  8. the server decrypts cookie
  9. the server sends code_verifier and code directly to /token.

So, it's the same round trip but just with a cookie now.


Edit #2 9/2/2023

a little more info that might help:

The oidc provider redirects the browser to the redirect_uri after authenticating the user. "The authorization server redirects the user-agent to the client's redirection endpoint". When the browser gets to the redirect_uri, it should make a request to your API with the cookie.

Here's the diagram that can help you visualize the flow with the redirect part being (C)

 +----------+
 | Resource |
 |   Owner  |
 |          |
 +----------+
      ^
      |
     (B)
 +----|-----+          Client Identifier      +---------------+
 |         -+----(A)-- & Redirection URI ---->|               |
 |  User-   |                                 | Authorization |
 |  Agent  -+----(B)-- User authenticates --->|     Server    |
 |          |                                 |               |
 |         -+----(C)-- Authorization Code ---<|               |
 +-|----|---+                                 +---------------+
   |    |                                         ^      v
  (A)  (C)                                        |      |
   |    |                                         |      |
   ^    v                                         |      |
 +---------+                                      |      |
 |         |>---(D)-- Authorization Code ---------'      |
 |  Client |          & Redirection URI                  |
 |         |                                             |
 |         |<---(E)----- Access Token -------------------'
 +---------+       (w/ Optional Refresh Token)

(C) Assuming the resource owner grants access, the authorization server redirects the user-agent back to the client using the redirection URI provided earlier (in the request or during client registration). The redirection URI includes an authorization code and any local state provided by the client earlier.

Notice that (C) actually has 2 lines. One from the Authorization Server to the User-Agent (browser) and then another one from the User-Agent to the Client. This is the redirect back to your app after the user authenticates with the authorization server (or OAuth/OIDC provider). The authorization code is included in this line.

The second (C) line, the one from User-Agent to Client in the diagram, would be where the browser sends the cookie with the encrypted code_verifier to your server along with the code.

 +----|-----+                                 
 |          |                                 
 |  User-   |                                 
 |  Agent   |                                 
 |          |                                 
 |          |                                 
 +------|---+                                 
        |  authorization code                                             
       (C) and                                               
        |  (code_verifier) cookie                                             
        v                                               
 +---------+                                          
 |         |                                             
 |  Client |                                             
 |         |                                             
 |         |  
 +---------+
akdombrowski
  • 673
  • 4
  • 9
  • 1
    Thanks for your response! I'm slightly in favor of the additional roundtrip b/c it's more straight-forward than sharing session information between (possibly) multiple server-side instances/pods. If I understand correctly, I wouldn't pass the result from `client.authorizationUrl(...)` with `code_verifier` to the client-side then, but both separately, with the `code_verifier` encrypted in a cookie. But still, I'm not sure on how to receive it from the client-side when I'm in the `/token` api endpoint/callback - as I understand it, I have no "connection" to the client-side there. – NotX Sep 02 '23 at 07:33
  • No problem! It's not an _additional_ round trip. Careful because **client** can be used in slightly different ways. The "OAuth client" in your scenario would refer to your server [RFC6749](https://www.rfc-editor.org/rfc/rfc6749.html#section-2.1). `client.authorizationUrl()` used in open-id is just a placeholder function call. It should tell the browser/user-agent to redirect to the authz server. This is the authz request. PKCE requires the `code_challenge` and `code_challenge_method` to be included here. The user authenticates and the user-agent goes to the `redirect_uri`. (continued) – akdombrowski Sep 02 '23 at 15:11
  • At the `redirect_uri`, your server gets the `code` _through_ the browser. Follow `(c)` [in the diagram here](https://www.rfc-editor.org/rfc/rfc6749.html#section-4.1). Then, your **server**, or _client_ in that same diagram, makes a request **directly** to the `/token` endpoint of the authz server, and _not_ through the browser as indicated by (d) in that diagram. The request includes the `code` and `code_verifier`. (continued) – akdombrowski Sep 02 '23 at 15:20
  • The cookie is created on your server and sent to the browser and it stores it. When the user is sent back to the `redirect_uri` via the browser, the call to your server includes the cookie. So, server creates `code_verifier`, encrypts, & stores in a cookie -> cookie & authz url sent from server to browser -> browser stores cookie -> browser redirects to `/authorize` -> ... -> browser redirects to `redirect_uri` -> browser sends `code` **and cookie** to server -> server decrypts cookie -> server sends `code_verifier` and `code` directly to `/token`. Same round trip but with a cookie now. (done) – akdombrowski Sep 02 '23 at 15:28
  • 1
    Thanks again for your time! I was a bit confused about the redirect via `redirect_uri`. I thought, this uri is called from the oidc-provider after the user has entered their credentials - so that it was a direct "oidc-provider to endpoint at server-side" call, and the endpoint's response gets looped back via oidc-provider to the browser. But maybe that was just some misconception, and the oidc-provider returns a just a redirect pointing at my endpoint _to the browser_ after the credentials have been entered, and then the browser follows it to the API endpoint (with cookie). Is that correct? – NotX Sep 02 '23 at 18:50
  • Yep, I think you've got it now! The oidc provider **redirects the browser** to the `redirect_uri` after authenticating the user. ["The authorization server redirects the user-agent to the client's redirection endpoint"](https://www.rfc-editor.org/rfc/rfc6749#section-3.1.2:~:text=authorization%20server%20redirects%20the%20user%2Dagent%20to%20the%0A%20%20%20client%27s%20redirection%20endpoint). When the browser gets to the `redirect_uri`, it should make a request to your API with the cookie. – akdombrowski Sep 02 '23 at 23:07
  • [Here's the diagram that can help you visualize the flow with the redirect part being (C)](https://www.rfc-editor.org/rfc/rfc6749#section-4.1:~:text=(B)%0A%20%20%20%20%20%2B%2D%2D%2D%2D%7C%2D%2D%2D%2D%2D%2B%20%20%20%20%20%20%20%20%20%20Client%20Identifier,Client%20%7C%20%20%20%20%20%20%20%20%20%20%26%20Redirection%20URI%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%0A%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C) – akdombrowski Sep 02 '23 at 23:08