NextJS 中的授权代码流 - 如何将 PKCE code_verifier传递给授权回调?

Authorisation Code Flow in NextJS - how to pass PKCE code_verifier to the authorization callback?

提问人:NotX 提问时间:9/1/2023 最后编辑:NotX 更新时间:9/3/2023 访问量:438

问:

我已经使用 openid-client 在 NextJS 中实现了 OAuth2 授权代码流(还没有 PKCE)。现在我应该在哪里存储它以及如何将其传递给服务器端调用的回调。code_verifier

到目前为止我做了什么(草图):

// 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)

在我的 API 端点中,一旦用户在以下位置输入其凭据,就会调用:api/auth/callbackhttps://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);
}

这一切正常,但不能实现 PKCE。所以我的问题是:

目前,我会将 作为 的一部分传递给客户端。这就足够了吗?但是我如何让它可用于我的回调端点,这是我所理解的纯服务器端?我是否必须使用参数并在服务器端使用 as 键缓存?code_verifierauthorizationUrlapi/auth/callbackstatecode_verifierstate

我发现了一个类似的问题,但提问者似乎并不为将其存储在 cookie 中而烦恼(我必须从一开始就刮掉它,出于什么原因?如果是为了安全起见,我可以在服务器端对其进行加密),而且他们似乎并不担心将其传递给服务器端的回调处理程序。我可以直接在客户端生成我的(没有 from 的帮助),但这并没有像预期的那样打击我,我仍然无法将其传递给 api 回调。authorization_urlcode_verifiergeneratorsopenid-client

(旁注:我不想使用 NextAuth。实际上,我们是从那里来的,但它造成了太多的麻烦。

next.js OAuth-2.0 授权 服务器端 PKCE

评论


答:

1赞 akdombrowski 9/2/2023 #1

如果你可以把它存储在服务器端(加密它不是一个坏主意),那么这通常是更好的途径。麻烦出在重定向上,并寻找哪个code_verifier进行代币交换。您可以创建类似会话 ID 的内容,并将其用作查找的键:https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-properties

另一种方法是加密并在 cookie 中发送它,确保对 cookie 采取适当的预防措施: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html#server-security cookie 应该往返于您可以解密并将其用于令牌交换的地方。这里的风险是有人可以访问和解密(如果未充分加密)您的code_verifier,因为它是通过网络调用传递的。


编辑 2023/9/2

添加一些额外的细节(最初是作为注释添加的,但它变得很长,并认为它可能对其他人有所帮助):

使用 cookie 不需要额外的往返行程。要小心,因为客户端的使用方式略有不同。

方案中的“OAuth 客户端”是指服务器RFC6749

client.authorizationUrl()在 open-id 中使用的只是一个占位符函数调用。它的实现应该告诉浏览器/用户代理重定向到端点的 authz 服务器。这是 authz 请求。PKCE 要求在此请求中包含 and。/authorizecode_challengecode_challenge_method

用户进行身份验证,然后将用户代理发送到redirect_uri。

在redirect_uri,您的服务器通过浏览器获取,因为用户需要能够返回到您的应用程序。code

然后,您的服务器或同一关系图中的客户端直接向 authz 服务器的端点发出请求,而不是通过浏览器发出请求。按照该图中的 (d) 操作。该请求包括 和 。/tokencodecode_verifier

Cookie 在您的服务器上创建并发送到浏览器并存储它。当用户通过浏览器发送回redirect_uri时,对服务器的调用包括 cookie。

或者,换一种说法:

  1. 服务器创建一个code_verifier,加密并存储在cookie中
  2. 从服务器发送到浏览器的 cookie 和 authz url
  3. 浏览器存储 Cookie
  4. 浏览器重定向到终结点/authorize
  5. ...
  6. 浏览器重定向到redirect_uri
  7. 浏览器向服务器发送 cookiecode
  8. 服务器解密 cookie
  9. 服务器发送 并直接发送到 。code_verifiercode/token

所以,这是同样的往返,但现在只是用一块饼干。


编辑 #2 2023/9/2

更多信息可能会有所帮助:

oidc 提供程序在对用户进行身份验证后将浏览器重定向到 。“授权服务器将用户代理重定向到客户端的重定向端点”。当浏览器到达 时,它应该使用 cookie 向您的 API 发出请求。redirect_uriredirect_uri

下图可以帮助您可视化重定向部分为 (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) 假设资源所有者授予访问权限,则授权 服务器使用 之前提供的重定向 URI(在请求中或期间) 客户注册)。重定向 URI 包括 客户端提供的授权代码和任何本地状态 早些时候。

请注意,(C) 实际上有 2 行。一个从 到 (浏览器),然后另一个从 到 .这是在用户向授权服务器(或 OAuth/OIDC 提供商)进行身份验证后重定向回您的应用。授权代码包含在此行中。Authorization ServerUser-AgentUser-AgentClient

第二行 (C) 是图中 to 的那一行,是浏览器将带有加密的 cookie 与 .User-AgentClientcode_verifiercode

 +----|-----+                                 
 |          |                                 
 |  User-   |                                 
 |  Agent   |                                 
 |          |                                 
 |          |                                 
 +------|---+                                 
        |  authorization code                                             
       (C) and                                               
        |  (code_verifier) cookie                                             
        v                                               
 +---------+                                          
 |         |                                             
 |  Client |                                             
 |         |                                             
 |         |  
 +---------+

评论

1赞 NotX 9/2/2023
感谢您的回复!我略微赞成额外的往返 b/c,它比在(可能)多个服务器端实例/pod 之间共享会话信息更直接。如果我理解正确,我不会将结果从 with 传递到客户端,而是将两者分开传递,并在 cookie 中加密。但是,当我在 api 端点/回调中时,我仍然不确定如何从客户端接收它 - 据我了解,我在那里没有与客户端的“连接”。client.authorizationUrl(...)code_verifiercode_verifier/token
0赞 akdombrowski 9/2/2023
没关系!这不是额外的往返。小心,因为客户端的使用方式略有不同。方案中的“OAuth 客户端”是指服务器RFC6749。 在 open-id 中使用的只是一个占位符函数调用。它应该告诉浏览器/用户代理重定向到 authz 服务器。这是 authz 请求。PKCE 要求在此处包含 and。用户进行身份验证,用户代理转到 .(续)client.authorizationUrl()code_challengecode_challenge_methodredirect_uri
1赞 NotX 9/3/2023
再次感谢您抽出宝贵时间接受采访!我对通过 .我认为,这个 uri 是在用户输入凭据后从 oidc-provider 调用的 - 因此它是直接的“oidc-provider 到服务器端端点”调用,端点的响应通过 oidc-provider 循环回浏览器。但也许这只是一些误解,oidc-provider 在输入凭据后返回一个指向我的端点到浏览器的重定向,然后浏览器跟随它到 API 端点(带有 cookie)。这是对的吗?redirect_uri
1赞 akdombrowski 9/3/2023
是的,我想你现在已经明白了!oidc 提供程序在对用户进行身份验证后将浏览器重定向到 。“授权服务器将用户代理重定向到客户端的重定向端点”。当浏览器到达 时,它应该使用 cookie 向您的 API 发出请求。redirect_uriredirect_uri
1赞 NotX 9/4/2023
哦,我明白!再次感谢!;)