Commit 07bf42e2 authored by Donghwan Kim's avatar Donghwan Kim
Browse files

Support PKCE as well as state param

1 merge request!12Support PKCE as well as state param
Showing with 1064 additions and 537 deletions
+1064 -537
......@@ -48,7 +48,14 @@ To analyze the flow, focus on `/server/index.js` and `/client/index.js`.
#### How to refresh token
##### confidential client
```sh
curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=refresh_token&client_id=1b0fbe1836d2133ba3c3d3475b3bad9acae57ff6&client_secret=68529ae2c23361ebd5f4946f09c1cf36f44bb580&refresh_token=47ad5e82afafd0289bbd7ad914674a1f41176cbd' http://localhost:4000/oauth2/token
curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=refresh_token&client_id=1b0fbe1836d2133ba3c3d3475b3bad9acae57ff6&client_secret=68529ae2c23361ebd5f4946f09c1cf36f44bb580&refresh_token=046b64c4992ebcd2528e54abb5f1fd373ecc505a' http://localhost:4000/oauth2/token
```
##### public client
```sh
curl -X POST -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=refresh_token&client_id=6123bbbf9687fa95b97836a55a3b262416b9abc4&refresh_token=db03698e15e6c53c1c234d032cbb3c6811e4519b' http://localhost:4000/oauth2/token
```
node_modules
const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2').Strategy;
const request = require('request');
const session = require('express-session');
const app = express();
app.set('view engine', 'ejs');
app.use(session({ secret: 'SECRET' }));
app.use(passport.initialize());
app.use(passport.session());
passport.use(new OAuth2Strategy({
authorizationURL: 'http://localhost:4000/oauth2/authorize',
tokenURL: 'http://localhost:4000/oauth2/token',
clientID: '6123bbbf9687fa95b97836a55a3b262416b9abc4',
callbackURL: 'http://localhost:3003/auth/cryptobadge/callback',
state: true,
pkceMethod: 'S256'
}, (accessToken, refreshToken, params, profile, done) => {
console.log(accessToken, refreshToken, params, profile);
const options = {
url: 'http://localhost:4000/api/user',
headers: {
'Authorization': 'Bearer ' + accessToken
}
};
request.post(options, (error, response, user) => {
if (error || response.statusCode !== 200) {
return done(error);
}
console.log(user);
done(null, user);
});
}
));
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
app.get('/', (req, res) => res.render('index'));
app.get('/success', (req, res) => res.render('success'));
app.get('/auth/cryptobadge', passport.authenticate('oauth2', {scope: ['email']}));
app.get('/auth/cryptobadge/callback', passport.authenticate('oauth2', {failureRedirect: '/close.html?error=foo'}), function (req, res) {
res.statusCode = 302;
res.setHeader('Location', '/success');
res.end();
});
app.listen(3003);
This diff is collapsed.
{
"name": "client-pkce",
"private": "true",
"version": "0.1.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"ejs": "^2.6.1",
"express": "^4.16.4",
"express-session": "^1.15.6",
"passport": "~0.1.17",
"passport-oauth2": "git+https://github.com/flowersinthesand/passport-oauth2.git#31e656cacfb5dfcce4b148b046117e3f349a2cda",
"request": "^2.88.0"
}
}
<head>
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css' integrity='sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy' crossorigin='anonymous' />
</head>
<h2>Vingle</h2>
<a href="/auth/cryptobadge">Log In with CryptoBadge</a>
<head>
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css' integrity='sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy' crossorigin='anonymous' />
</head>
<h2>Vingle</h2>
const express = require('express');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth').OAuth2Strategy;
const OAuth2Strategy = require('passport-oauth2').Strategy;
const request = require('request');
const app = express();
......
This diff is collapsed.
......@@ -2,25 +2,20 @@
"name": "client",
"private": "true",
"version": "0.1.0",
"description": "A Tool for YSA wards to track home and visiting teaching",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": "",
"author": "AJ ONeal <coolaj86@gmail.com> (http://coolaj86.com)",
"license": "Apache2",
"author": "",
"license": "ISC",
"dependencies": {
"connect": "~2.12.0",
"connect_router": "~1.8.7",
"ejs": "^2.6.1",
"express": "^4.16.4",
"jade": "^1.11.0",
"express-session": "^1.15.6",
"passport": "~0.1.17",
"passport-http-bearer": "~1.0.1",
"passport-ldsauth": "~0.9.0",
"passport-oauth": "~1.0.0",
"passport-oauth2": "^1.4.0",
"request": "^2.88.0"
}
}
......@@ -2,11 +2,13 @@ const express = require('express');
const bodyParser = require('body-parser');
const session = require('cookie-session');
const UnauthorizedRequestError = require('oauth2-server/lib/errors/unauthorized-request-error');
const InvalidClientError = require('oauth2-server/lib/errors/invalid-client-error');
const OAuth2Server = require('oauth2-server');
const Request = OAuth2Server.Request;
const Response = OAuth2Server.Response;
const path = require('path');
const {generateRandomToken} = require('./util');
const TokenHandler = require('oauth2-server/lib/handlers/token-handler');
// ## OAuth2 scopes
// Now we have only one scope.
......@@ -33,6 +35,21 @@ const Application = [
}
];
const OAuth2Client = [
// Public client e.g. Android application
{
// Id
"id": "6123bbbf9687fa95b97836a55a3b262416b9abc4",
"secret": null,
"redirectUris": [
"http://localhost:3003/auth/cryptobadge/callback"
],
"grants": [
"authorization_code",
"refresh_token"
],
"applicationId": "1"
},
// Confidential client e.g. Web application w/ backend
{
// Id
"id": "1b0fbe1836d2133ba3c3d3475b3bad9acae57ff6",
......@@ -56,6 +73,8 @@ const OAuth2AuthorizationCode = [
// "redirectUri": "http://localhost:3002/auth/cryptobadge/callback",
// "scope": "email",
// "clientId": "1b0fbe1836d2133ba3c3d3475b3bad9acae57ff6",
// "codeChallenge": "4KQNrHzb3O1jt3AEooNyp3fFfu0z_rvlyra9SaScYqI",
// "codeChallengeMethod": "S256",
// "userId": "1"
// }
];
......@@ -128,7 +147,7 @@ const OAuth2Adapter = {
},
getClient: async (clientId, clientSecret) => {
const answer = await OAuth2Client.find(client => client.id === clientId);
if (answer != null && clientSecret && clientSecret !== answer.secret ) {
if (answer != null && clientSecret && clientSecret !== answer.secret) {
return null;
}
......@@ -173,6 +192,10 @@ const OAuth2Adapter = {
// String
scope: code.scope,
// String
codeChallenge: code.codeChallenge,
// String
codeChallengeMethod: code.codeChallengeMethod,
// String
clientId: client.id,
// String
userId: user.id
......@@ -217,8 +240,8 @@ app.use(bodyParser.urlencoded({extended: false}));
// ## CryptoBadge integration (Authorization server)
const oauth = new OAuth2Server({ model: OAuth2Adapter });
const handleResponse = function(req, res, response) {
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;
......@@ -230,7 +253,7 @@ const handleResponse = function(req, res, response) {
res.status(response.status).send(response.body);
}
};
const handleError = function(e, req, res, response) {
const handleError = function (e, req, res, response) {
if (response) {
res.set(response.headers);
}
......@@ -238,7 +261,7 @@ const handleError = function(e, req, res, response) {
if (e instanceof UnauthorizedRequestError) {
return res.send();
}
res.send({ error: e.name, error_description: e.message });
res.send({error: e.name, error_description: e.message});
};
// Port the routes to cryptobadge.app properly
......@@ -266,7 +289,10 @@ app.get('/oauth2/authorize', async (req, res) => {
client_id: req.query.client_id,
scope: req.query.scope,
ticket,
redirect_uri: req.query.redirect_uri
redirect_uri: req.query.redirect_uri,
state: req.query.state,
code_challenge: req.query.code_challenge,
code_challenge_method: req.query.code_challenge_method
});
}
const request = new Request({
......@@ -286,7 +312,7 @@ app.get('/oauth2/authorize', async (req, res) => {
allowEmptyState: true
});
handleResponse(req, res, response);
} catch(e) {
} catch (e) {
handleError(e, req, res, response);
}
});
......@@ -298,7 +324,7 @@ app.post('/oauth2/authorize', async (req, res) => {
// If there's no ticket or invalid ticket is included, redirects it back to Authorization Endpoint
if (!req.body.ticket || !validateTicket(req.body.ticket)) {
return res.redirect(`/oauth2/authorize?response_type=${req.body.response_type}&client_id=${req.body.client_id}&scope=${req.body.scope}&redirect_uri=${encodeURIComponent(req.body.redirect_uri)}`);
return res.redirect(`/oauth2/authorize?response_type=${req.body.response_type}&client_id=${req.body.client_id}&scope=${req.body.scope}&redirect_uri=${encodeURIComponent(req.body.redirect_uri)}&state=${request.body.state}&code_challenge=${req.query.code_challenge}&code_challenge_method=${req.query.code_challenge_method}`);
}
const request = new Request({
......@@ -319,7 +345,7 @@ app.post('/oauth2/authorize', async (req, res) => {
allowEmptyState: true
});
handleResponse(req, res, response);
} catch(e) {
} catch (e) {
handleError(e, req, res, response);
}
});
......@@ -332,10 +358,18 @@ app.post('/oauth2/token', async (req, res) => {
body: req.body,
});
const response = new Response();
const client = TokenHandler.prototype.getClientCredentials.call({isClientAuthenticationRequired: () => false}, request);
if (!client) {
throw new InvalidClientError('Invalid client: cannot retrieve client credentials');
}
try {
await oauth.token(request, response, {
refreshTokenLifetime: 60 * 60 * 24 * 90 // 90 days
// TODO check requireClientAuthentication w/ PKCE
refreshTokenLifetime: 60 * 60 * 24 * 90, // 90 days
requireClientAuthentication: {
[req.body.grant_type]: !!client.clientSecret
}
});
handleResponse(req, res, response);
} catch (e) {
......
......@@ -13,6 +13,9 @@
<input type="hidden" name="scope" value="<%= scope %>" />
<input type="hidden" name="ticket" value="<%= ticket %>" />
<input type="hidden" name="redirect_uri" value="<%= redirect_uri %>">
<input type="hidden" name="state" value="<%= state %>">
<input type="hidden" name="code_challenge" value="<%= code_challenge %>">
<input type="hidden" name="code_challenge_method" value="<%= code_challenge_method %>">
</div>
<div>
<input type="submit" value="Allow" id="allow" />
......
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