译自: http://thecodebarbarian.com/oauth-with-node-js-and-express.html
原作者: Valeri Karpov
英文标题: Implementing an OAuth Server With Node.js and Express
网站 thecodebarbarian.com 上有非常多的关于Node.js(尤其是Mongoose)的文章,感兴趣的可以看看。
关于 token生成 和 oauth授权流程,之前译过2篇:
OAuth是一种协议,它允许应用程序获取一定的权限来访问用户在另一应用程序上的数据。它通常有2类用途:
单点登录(Single sign on),像 Facebook login。
方便一方app与其它app打通,像 Buffer for scheduling tweets on Twitter。
本文将描述如何使用Node.js和Express构建一个最小的OAuth Server,不依赖第三方OAuth模块。唯一的例外是Matt Mueller优秀的 oauth-open 包,用于在客户端显示oauth 弹出窗口,以验证我们实际上有一个可工作的oauth设定。
The OAuth Flow (OAuth 流程)
标准的 web OAuth 2.0 流程包含3步:
应用客户端打开一个对话框,要求用户对应用授权。该对话框通常位于不同的域上,如 Facebook's OAuth 登录框。
授权后,对话框重定向回客户端域,并在url中附带 auth code 参数。auth code 是一串短期代码,其用于换取更长生命期的 access token。
应用程序从url中提取 auth code 参数,并用它向授权服务器发送POST请求。授权服务器验证该 code 并返回一个 access token,应用客户端可以使用该token进行后续操作。
在本例中,OAuth工作流涉及到2个组件:
客户端应用程序。可以将此视为你的应用程序正在尝试从授权应用程序访问数据。
授权应用程序。可以将其视为Facebook、Google、Twitter或其他应用程序,它们是客户端应用程序试图代表你而访问的应用程序。
Client App Implementation (客户端应用程序的实现)
注意下面的代码只是简化了的说教性示例,并非可用于生产级的OAuth授权服务器。不要直接复制粘贴到生产服务器。
我们先看客户端应用程序,看看授权服务器需要实现哪些端点(endpoints)。客户端应用服务器的入口是一个简单的静态服务器,它监听3000端口:
'use strict';
const express = require('express');
run().catch(err => console.log(err));
async function run() {
const app = express();
app.use(express.static('./'));
await app.listen(3000);
console.log('Listening on port 3000');
}
客户端应用程序有一个文件,index.html。它负责打开OAuth对话框,用 auth code 交换 access token,并使用该 token 作为授权码向安全端点发送HTTP请求。身份验证服务器将在 http://localhost:3001 上运行。
<html>
<body>
<div id="content"></div>
<script type="text/javascript" src="https://codebarbarian-images.s3.amazonaws.com/open.dist.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script type="text/javascript">
Step 1: open an OAuth dialog
oauthOpen('http://localhost:3001/oauth-dialog.html', async (err, code) => {
Step 2: exchange the code for an access token
const resToken = await axios.post('http://localhost:3001/token', { code: code.code });
Step 3: use the access token to make a request to a secure
endpoint and display some data
const res = await axios.get('http://localhost:3001/secure', {
headers: { authorization: resToken.data['access_token'] }
});
document.querySelector('#content').innerHTML =
`The secret answer is ${res.data.answer}`;
});
</script>
</body>
</html>
文件 https://codebarbarian-images.s3.amazonaws.com/open.dist.js 是webpack打包好的文件。为了方便,我预先编译了该脚本。它包含下面代码:
window.oauthOpen = require('oauth-open');
客户端还需要一个 oauth-callback.html 文件。oauth-callback.html 文件不需要做任何事情,oauthOpen() 函数负责提取 auth code。下面是一个最简化的oauth-callback.html:
<html>
<body>
<div>Authorized</div>
</body>
</html>
Auth Server Implementation (授权服务器实现)
Auth Server 需要4个访问端点(endpoints):
一个Oauth 对话框,要求用户对客户端授权。
一个生成 auth code 并重定向回客户端应用的路由。
一个用 auth code 交换 access token 的路由。
一个 安全的端点,只有当 Authorization header头中含有有效的 token 值时才作出响应。
由于 Auth Server 运行在与客户端应用程序不同的域上,因此它还需要开启 CORS,以免浏览器抛出跨源请求的错误。
流程从用户打开 OAuth对话框 开始。在本例中,授权服务器有一个静态的 oauth-dialog.html 文件,它有一个按钮,用户点击即可授权。单击该按钮会访问 /code 路由,后者将生成 auth code 并重定向到客户端程序。
<html>
<body>
<div>Authorize OAuth Test App?</div>
<button>OK</button>
<script type="text/javascript">
document.querySelector('button').addEventListener('click', () => {
window.location.href = '/code';
});
</script>
</body>
</html>
下面是完整的 auth-server.js 文件:
'use strict';
const cors = require('cors');
const express = require('express');
run().catch(err => console.log(err));
async function run() {
const app = express();
// Store the auth codes and access tokens in memory. In a real
// auth server, you would store these in a database.
const authCodes = new Set();
const accessTokens = new Set();
app.use(cors());
app.use(express.json());
// Generate an auth code and redirect to your app client's
// domain with the auth code
app.get('/code', (req, res) => {
// Generate a string of 10 random digits
const authCode = new Array(10).fill(null).map(() => Math.floor(Math.random() * 10)).join('');
authCodes.add(authCode);
// Normally this would be a `redirect_uri` parameter, but for
// this example it is hard coded.
res.redirect(`http://localhost:3000/oauth-callback.html?code=${authCode}`);
});
// Verify an auth code and exchange it for an access token
app.post('/token', (req, res) => {
if (authCodes.has(req.body.code)) {
// Generate a string of 50 random digits
const token = new Array(50).fill(null).map(() => Math.floor(Math.random() * 10)).join('');
authCodes.delete(req.body.code);
accessTokens.add(token);
res.json({ 'access_token': token, 'expires_in': 60 * 60 * 24 });
} else {
res.status(400).json({ message: 'Invalid auth token' });
}
});
// Endpoint secured by auth token
app.get('/secure', (req, res) => {
const authorization = req.get('authorization');
if (!accessTokens.has(authorization)) {
return res.status(403).json({ message: 'Unauthorized' });
}
return res.json({ answer: 42 });
});
// Serve up `oauth-dialog.html`
app.use(express.static('./'));
await app.listen(3001);
console.log('Listening on port 3001');
}
Moving On (继续前行)
OAuth可能会让初学者感到困惑,但是一旦理解了OAuth工作流,实现OAuth Server 的过程就很简单了。你只需要一个对话框、一个获取 auth code 的端点和用 auth code 交换 access token 的端点。一旦得到了access token,用户在效果上就相当于 “登录” 了授权服务。
如果你希望实现一个真正的OAuth服务器,下一步是将 auth code 和 access token 存储在数据库中。对于更复杂的应用程序,你可能需要添加对 OAuth scopes 的支持,它告知用户客户端应用程序将要有哪些权限,例如客户端应用程序是否具有代你发送tweet的权限。
多多转发
共同学习





