Commit 453fdbe0 authored by Donghwan Kim's avatar Donghwan Kim
Browse files

Trim again and again

2 merge requests!12Support PKCE as well as state param,!11Trim again and again
Showing with 77 additions and 90 deletions
+77 -90
......@@ -18,32 +18,37 @@ const SCOPES = [
// ## DynamoDB integration
// `User`, `Application`, `AuthorizationCode`, and `Token` are models to implement OAuth2 protocol.
// `User`, `Application`, `OAuth2Client`, `OAuth2AuthorizationCode`, and `OAuth2Token` are models to implement OAuth2 protocol.
// Port the models to DynamoDB. Of course, contents are fake.
const User = [
// This User schema doesn't mean much.
{id: '1', username: 'bob', password: 'secret', name: 'Bob Smith', email: 'bob@gmail.com'},
{id: '2', username: 'joe', password: 'password', name: 'Joe Davis', email: 'Joe@gmail.com'},
];
// Application and OAuth2Client are supposed to be created from CryptoBadge Developer website
const Application = [
{
"id": "1",
"name": "Vingle",
// unique
"clientId": "1b0fbe1836d2133ba3c3d3475b3bad9acae57ff6",
// nullable
"clientSecret": "68529ae2c23361ebd5f4946f09c1cf36f44bb580",
}
];
const OAuth2Client = [
{
// Id
"id": "1b0fbe1836d2133ba3c3d3475b3bad9acae57ff6",
"secret": "68529ae2c23361ebd5f4946f09c1cf36f44bb580",
"redirectUris": [
"http://localhost:3002/auth/cryptobadge/callback"
],
"grantTypes": [
"grants": [
"authorization_code",
"refresh_token",
]
"refresh_token"
],
"applicationId": "1"
}
];
// Created by `saveAuthorizationCode` and deleted by `revokeAuthorizationCode`
const AuthorizationCode = [
const OAuth2AuthorizationCode = [
// {
// "id": "1",
// "authorizationCode": "abaf922582d8001c9fc5ff11f0a453334fafca3d",
......@@ -55,7 +60,7 @@ const AuthorizationCode = [
// }
];
// Created by `saveToken` and deleted by `revokeToken`
const Token = [
const OAuth2Token = [
// {
// "id": "1",
// "accessToken": "65e7698f665959e6cff663264c85d481edb25f66",
......@@ -95,7 +100,7 @@ const OAuth2Adapter = {
// FYI, `async` and `await` keywords are acutally meaningless.
// I used them simply to emphasize what you should do
getAccessToken: async (accessToken) => {
const answer = await Token.find(token => token.accessToken === accessToken);
const answer = await OAuth2Token.find(token => token.accessToken === accessToken);
if (answer != null) {
answer.client = {id: answer.clientId};
answer.user = {id: answer.userId};
......@@ -104,7 +109,7 @@ const OAuth2Adapter = {
return answer;
},
getRefreshToken: async (refreshToken) => {
const answer = await Token.find(token => token.refreshToken === refreshToken);
const answer = await OAuth2Token.find(token => token.refreshToken === refreshToken);
if (answer != null) {
answer.client = {id: answer.clientId};
answer.user = {id: answer.userId};
......@@ -113,7 +118,7 @@ const OAuth2Adapter = {
return answer;
},
getAuthorizationCode: async (authorizationCode) => {
const answer = await AuthorizationCode.find(code => code.authorizationCode === authorizationCode);
const answer = await OAuth2AuthorizationCode.find(code => code.authorizationCode === authorizationCode);
if (answer != null) {
answer.client = {id: answer.clientId};
answer.user = {id: answer.userId};
......@@ -122,39 +127,18 @@ const OAuth2Adapter = {
return answer;
},
getClient: async (clientId, clientSecret) => {
const app = await Application.find(app => {
if (app.clientId !== clientId) {
return false;
}
// If the client has a secret (confidential client), the client secret should be checked too.
if (app.secret != null && (app.secret !== clientSecret)) {
return false;
}
return true;
});
if (app == null) {
const answer = await OAuth2Client.find(client => client.id === clientId);
if (answer != null && clientSecret && clientSecret !== answer.secret ) {
return null;
}
return {
// String
id: app.clientId,
// TODO revisit this is workaround and it seems to be fixed in some commit
// http://git.baikal.io/bombkyu/login-with-cryptobadge-express-oauth2/merge_requests/6
// String
clientId: app.clientId,
// Array<String>
redirectUris: app.redirectUris,
// Array<String>
grants: app.grantTypes,
};
return answer;
},
saveToken: async (token, client, user) => {
const answer = {
// Auto-increment id string
// 'id' is not required by oauth2-server but used to identify a token to revoke
id: `${Token.length + 1}`,
id: `${OAuth2Token.length + 1}`,
// String
accessToken: token.accessToken,
// Date
......@@ -171,7 +155,7 @@ const OAuth2Adapter = {
userId: user.id
};
await Token.push(answer);
await OAuth2Token.push(answer);
return {...answer, client: {id: answer.clientId}, user: {id: answer.userId}};
},
......@@ -179,7 +163,7 @@ const OAuth2Adapter = {
const answer = {
// Auto-increment id string
// 'id' is not required by oauth2-server but used to identify a code to revoke
id: `${AuthorizationCode.length + 1}`,
id: `${OAuth2AuthorizationCode.length + 1}`,
// String
authorizationCode: code.authorizationCode,
// Date
......@@ -194,14 +178,14 @@ const OAuth2Adapter = {
userId: user.id
};
await AuthorizationCode.push(answer);
await OAuth2AuthorizationCode.push(answer);
return {...answer, client: {id: answer.clientId}, user: {id: answer.userId}};
},
revokeToken: async ({id}) => {
const index = await Token.findIndex(token => token.id === id);
const index = await OAuth2Token.findIndex(token => token.id === id);
if (index !== -1) {
await Token.splice(index, 1);
await OAuth2Token.splice(index, 1);
// If the revocation was successful
return true;
}
......@@ -210,9 +194,9 @@ const OAuth2Adapter = {
return false;
},
revokeAuthorizationCode: async ({id}) => {
const index = await AuthorizationCode.findIndex(code => code.id === id);
const index = await OAuth2AuthorizationCode.findIndex(code => code.id === id);
if (index !== -1) {
await AuthorizationCode.splice(index, 1);
await OAuth2AuthorizationCode.splice(index, 1);
// If the revocation was successful
return true;
}
......@@ -232,10 +216,32 @@ app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
// ## CryptoBadge Integration
// Port the following routes to cryptobadge.app properly
// ## CryptoBadge integration (Authorization server)
const oauth = new OAuth2Server({ model: OAuth2Adapter });
const handleResponse = function(req, res, response) {
// TODO should set response from the Lambda Function
if (response.status === 302) {
const location = response.headers.location;
delete response.headers.location;
res.set(response.headers);
res.redirect(location);
} else {
res.set(response.headers);
res.status(response.status).send(response.body);
}
};
const handleError = function(e, req, res, response) {
if (response) {
res.set(response.headers);
}
res.status(e.code);
if (e instanceof UnauthorizedRequestError) {
return res.send();
}
res.send({ error: e.name, error_description: e.message });
};
// Port the routes to cryptobadge.app properly
// ### `GET /oauth2/authorize` - Authorization Endpoint: https://tools.ietf.org/html/rfc6749#section-3.1
app.get('/oauth2/authorize', async (req, res) => {
// `req.session.user` is set if a user signs in and is removed if a user signs out
......@@ -243,9 +249,10 @@ app.get('/oauth2/authorize', async (req, res) => {
return res.redirect(`/login?redirect=${encodeURIComponent(req.originalUrl)}`);
}
const application = Application.find((app) => app.clientId == req.query.client_id);
const client = OAuth2Client.find((client) => client.id === req.query.client_id);
const application = Application.find((app) => app.id == client.applicationId);
// If a user has never approved this client, he/she should decide whether to approve it.
const token = Token.find((token) => token.clientId === req.query.client_id && token.userId === req.session.user.id);
const token = OAuth2Token.find((token) => token.clientId === req.query.client_id && token.userId === req.session.user.id);
if (token == null) {
// A ticket is an one-time token to protect `POST /oauth2/authorize`
// You need to replace the below stub with one to generate an one-time token using JWT or something
......@@ -255,7 +262,7 @@ app.get('/oauth2/authorize', async (req, res) => {
// In this page, a user will review what scopes a client want and decide whether to approve it.
return res.render('permission', {
response_type: req.query.response_type,
clientName: application.name,
applicationName: application.name,
client_id: req.query.client_id,
scope: req.query.scope,
ticket,
......@@ -335,8 +342,6 @@ app.post('/oauth2/token', async (req, res) => {
handleError(e, req, res, response);
}
});
// #######
// These routes are only for this example so not that meaningful.
app.get('/login', (req, res) => {
res.render('login', {redirect: req.query.redirect})
......@@ -355,50 +360,33 @@ app.post('/login', (req, res) => {
// #######
// ## User API
// ## Sample User API (Resource server)
app.post('/api/user', async (req, res) => {
// Authenticates the request
const request = new Request({
headers: req.headers,
method: req.method,
query: req.query,
});
const response = new Response();
let token;
try {
const token = await oauth.authenticate(request, response);
// TODO revisit user.xx
const {scope, user} = token;
const info = {user: user.username};
if (scope.includes('email')) {
info.email = user.email;
}
res.send(info);
token = await oauth.authenticate(request, response);
} catch (e) {
handleError(e, req, res, response);
// TODO replace this handleError with other one since this route belongs to the resource server
return handleError(e, req, res, response);
}
// Now that authentication succeeded, handle the request
const {userId, scope} = token;
const user = User.find(user => user.id === userId);
delete user.password;
if (!scope.split(' ').includes('email')) {
delete user.email;
}
res.send(user);
});
// #######
app.listen(4000);
const handleResponse = function(req, res, response) {
// TODO should set response from the Lambda Function
if (response.status === 302) {
const location = response.headers.location;
delete response.headers.location;
res.set(response.headers);
res.redirect(location);
} else {
res.set(response.headers);
res.status(response.status).send(response.body);
}
};
const handleError = function(e, req, res, response) {
if (response) {
res.set(response.headers);
}
res.status(e.code);
if (e instanceof UnauthorizedRequestError) {
return res.send();
}
res.send({ error: e.name, error_description: e.message });
};
......@@ -321,9 +321,8 @@
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"oauth2-server": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/oauth2-server/-/oauth2-server-3.0.1.tgz",
"integrity": "sha512-LFAT4MeTaOgdW+b8YMVMsPhJ8LrbSfVkYZRPgRmELJEJoXcchb/L4b9/lEmgpeNtjH8PlFiqof+YwI+y/oJuOg==",
"version": "git+https://github.com/flowersinthesand/node-oauth2-server.git#875fdb990c149210f2f3fd409303070f907eab1a",
"from": "git+https://github.com/flowersinthesand/node-oauth2-server.git#875fdb990c149210f2f3fd409303070f907eab1a",
"requires": {
"basic-auth": "^2.0.0",
"bluebird": "^3.5.1",
......
......@@ -15,6 +15,6 @@
"cookie-session": "^2.0.0-beta.3",
"ejs": "^2.6.1",
"express": "^4.16.4",
"oauth2-server": "^3.0.1"
"oauth2-server": "git+https://github.com/flowersinthesand/node-oauth2-server.git#875fdb990c149210f2f3fd409303070f907eab1a"
}
}
......@@ -3,7 +3,7 @@
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<p>Hello,</p>
<p><b><%= clientName %></b> is requesting access to your account.</p>
<p><b><%= applicationName %></b> is requesting access to your account.</p>
<p><b><%= scope %></b></p>
<p>Do you approve?</p>
<form action="/oauth2/authorize" method="post">
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment