我对你如何表达你的问题和你的代码没有意义感到有点困惑,但我得到的要点是你想从后端获取一个令牌并且你正在尝试实现OAuth 2.0,所以我会尽量在 Node.js 的上下文中回答这个问题。顺便说一句,当您说:“调用第一个令牌端点并获取令牌,然后使用令牌调用实际端点以获取数据。”,这不符合 OAuth 2.0 指定的推荐协议,并且在事实不安全。我将首先解释你应该做什么,然后是如何实现它。
我将从实现RFC6749中定义的授权码授权类型的角度来回答这个问题,这是推荐使用的(与RFC6736定义的PKCE,但我会离开PKCE 在这个答案中,因为它只会使事情复杂化。不过,请随时阅读链接的 RFC。
RFC6749 定义了两个授权服务器端点用于授权代码授权类型:授权端点和令牌端点。这在 RFC6749 的第 3 节中有描述,但我会总结一下:
授权端点被客户端用来获取授权,在本例中是一个代码。这段代码是你需要生成服务器端并以某种方式存储的东西。代码应该具有高熵,并且应该按照规范的定义在不超过 10 分钟的时间内有效。您可以通过使用crypto 模块生成一些随机字节并以某种方式将其缓存在您的后端10 分钟来完成此操作。
令牌端点是客户端用来获取令牌的端点,必须出示有效的代码才能获取令牌。
现在让我们查看整个流程:
客户端点击客户端前端的登录按钮。客户端被重定向到授权服务器。 uri 非常重要,应该如下所示:
http://auth.example.com/authorize?grant_type=code&redirect_uri=http://example.com/callback&scope=openidscope1 scope2&client_id=example-client-id&state=some-opaque-value。
这个 url 不是 URL 编码的,但你明白我的意思(空格将替换为适当的 url 安全字符)。该 URL 包含五项内容:
- grant_type=code
- redirect_uri=客户端的重定向 uri
- scope= 由空格分隔的一组范围
- client_id= 您的客户 ID
- state= 在发送任何请求之前在客户端生成的具有大量熵的随机字符串。
根据RFC6749 Section 4.1.1,授权服务器应验证此 URL 并确保其具有必要的查询参数,如果不是,则不应处理任何请求
如果 URL 有效,则 Authz 应显示登录或注册页面,并验证最终用户的凭据。如果它们不正确,Authz 不应将它们重定向到 redirect_uri,而是显示某种错误消息。但是,如果客户端身份验证成功,则 Authz 应该生成代码,即在您的情况下,您可以使用 Node.js crypto 模块生成随机字符串。这应该与客户端先前呈现的状态一起附加到重定向 uri
http://example.com/callback?code=your-generated-code&state=the-state-that-was-presented
一旦发生这种情况,客户端应该验证 url 中的 state 参数是否与它最初创建的参数相同。如果它无效,则不应使用该代码 --> 有人在攻击您或您有错误。如果 state 参数正确,那么您在该重定向 url 中收到的 code 现在应该用于在我之前解释的令牌端点获取 token。
您在授权服务器上对令牌端点的请求可能如下所示:
http://oauth.example.com/token?grant_type=token&code=the-code-you-received&redirect_uri=http://example.com/callback&client_id=example-client-id
您的 Authz 现在应该验证这一点
- 提供的代码是有效的
- redirect_uri 有效
- client_id 有效
- 代码尚未过期(它们的最大 ttl 应为 10 分钟)
如果一切顺利,Authz 应该创建并生成一个令牌,通常是包含以下声明的 JSON Web 令牌 (JWT),如 RFC9068 中所指定。为了节省您的时间,这些声明应该是:
- 是
- exp
- 听
- 分
- client_id
- 知道
- jti
现在您的客户拥有一个令牌,他们应该能够使用它来针对您的 api 进行身份验证。
好的,现在我们已经看到了流程,这里有几个伪代码模板,您可以尝试实现它们,但是有很多因素可能会影响到这一点,例如,您的客户是公开的还是机密的 (公共客户端不会获得刷新令牌,除非它们被旋转并且使用了 PKCE --> 参见 Auth0: Authorization Code Flow with PKCE
您的 Authz 可能有一个如下所示的文件:
// forgive me if my node.js isn't perfect, it's been a while since I've
// written it but you get the gist:
import { Router } from 'express'
const router = Router();
//authorization endpoint
router.route('/authorize').get(async(req,res,next)=> {
if(!validateRequestQuery(req.query)) return res.status(401).json({valid-oauth-error-msg-here});
const state = req.query.state;
const code: string = generateAuthorizationCode();
const cacheSuccess = await cacheCodeForTenMinutes();
if(!cacheSuccess) return res.status(500).json('valid-oauth-error-here');
const callbackUri = `http://example.com/callback?code=${code}&state=${state}`;
res.redirect(callbackUri);
}
)
//once the above function is called, the client needs to validate the state //it sends back!
//token endpoint
router.route('/token').get(async(req, res, next){
if(!validateTokenRequestQuery()) return a valid oauth error
const code = req.query.code
const isCodeValid = await checkCache(code);
if(!isCodeValid) return a valid oauth error
const accessToken = new Token(); // <--create a valid Access Token here
const refreshToken = new Token(accessToken); // if your client is confidential or you are using refresh token rotation and PKCE!
const idToken = new IdToken() // <-- if the openid scope is included
return res.status(200).json('accesstoken, idtoken');
// NEVER SEND REFRESH TOKEN TO FRONTEND. it is highly insecure. store it
// server side instead.
});
如果您有任何问题,请随时回复。这是一个非常复杂的话题,我所说的某些内容可能过于模糊,所以请让我澄清一下。