feat: updated to be compatible with Auth0 SPA which uses the 'Authorization Code Grant using Proof Key for Code Exchange (PKCE)' flow
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
process.env.DEBUG = 'app*';
|
||||
|
||||
const express = require('express');
|
||||
const cookieParser = require('cookie-parser')
|
||||
const app = express();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const Debug = require('debug');
|
||||
@@ -16,150 +17,252 @@ const audience = process.env.AUDIENCE || 'https://generic-audience';
|
||||
|
||||
const debug = Debug('app');
|
||||
|
||||
let {privateKey, certDer, thumbprint, exponent, modulus} = cert(jwksOrigin);
|
||||
let { privateKey, certDer, thumbprint, exponent, modulus } = cert(jwksOrigin);
|
||||
|
||||
const sessions = {}
|
||||
const challenges = {}
|
||||
|
||||
const corsOpts = (req, cb) => {
|
||||
cb(null, { origin: req.headers.origin })
|
||||
}
|
||||
|
||||
// Configure our small auth0-mock-server
|
||||
app.options('*', cors())
|
||||
.use(cors())
|
||||
.use(bodyParser.json())
|
||||
.use(bodyParser.urlencoded({extended: true}))
|
||||
.use(express.static(`${__dirname}/public`))
|
||||
.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
|
||||
app.options('*', cors(corsOpts))
|
||||
.use(cors())
|
||||
.use(bodyParser.json())
|
||||
.use(bodyParser.urlencoded({ extended: true }))
|
||||
.use(cookieParser())
|
||||
.use(express.static(`${__dirname}/public`))
|
||||
.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
|
||||
|
||||
// This route can be used to generate a valid jwt-token.
|
||||
app.post('/token', function (req, res) {
|
||||
if (!req.body.email || !req.body.password) {
|
||||
debug('Body is invalid!', req.body);
|
||||
return res.status(400).send('Email or password is missing!');
|
||||
}
|
||||
let date = Math.floor(Date.now() / 1000);
|
||||
let accessToken = jwt.sign(Buffer.from(JSON.stringify({
|
||||
iss: jwksOrigin,
|
||||
aud: [audience],
|
||||
sub: 'auth0|' + req.body.email,
|
||||
iat: date,
|
||||
exp: date + 7200,
|
||||
azp: req.body.clientId,
|
||||
'https://unbound.se/email': req.body.email
|
||||
})), privateKey, {
|
||||
algorithm: 'RS256',
|
||||
keyid: thumbprint
|
||||
});
|
||||
app.post('/oauth/token', (req, res) => {
|
||||
const code = req.body.code
|
||||
const session = sessions[code]
|
||||
|
||||
let idToken = jwt.sign(Buffer.from(JSON.stringify({
|
||||
iss: jwksOrigin,
|
||||
aud: req.body.clientId,
|
||||
nonce: req.body.nonce,
|
||||
sub: 'auth0|' + req.body.email,
|
||||
iat: date,
|
||||
exp: date + 7200,
|
||||
azp: req.body.clientId
|
||||
})), privateKey, {
|
||||
algorithm: 'RS256',
|
||||
keyid: thumbprint
|
||||
});
|
||||
let date = Math.floor(Date.now() / 1000);
|
||||
let accessToken = jwt.sign(Buffer.from(JSON.stringify({
|
||||
iss: jwksOrigin,
|
||||
aud: [audience],
|
||||
sub: 'auth0|' + session.email,
|
||||
iat: date,
|
||||
exp: date + 7200,
|
||||
azp: session.clientId,
|
||||
})), privateKey, {
|
||||
algorithm: 'RS256',
|
||||
keyid: thumbprint
|
||||
});
|
||||
|
||||
debug('Signed token for ' + req.body.email);
|
||||
// res.json({ token });
|
||||
res.redirect(`${req.body.redirect}?domain=${issuer}#access_token=${accessToken}&state=${req.body.state}&id_token=${idToken}&scope=openid%20profile%20email&expires_in=7200&token_type=Bearer`)
|
||||
let idToken = jwt.sign(Buffer.from(JSON.stringify({
|
||||
iss: jwksOrigin,
|
||||
aud: session.clientId,
|
||||
nonce: session.nonce,
|
||||
sub: 'auth0|' + session.email,
|
||||
iat: date,
|
||||
exp: date + 7200,
|
||||
azp: session.clientId,
|
||||
name: 'Example Person',
|
||||
picture: 'https://cdn.playbuzz.com/cdn/5458360f-32ea-460e-a707-1a2d26760558/70bda687-cb84-4756-8a44-8cf735ed87b3.jpg',
|
||||
'https://unbound.se/roles': session.roles
|
||||
})), privateKey, {
|
||||
algorithm: 'RS256',
|
||||
keyid: thumbprint
|
||||
});
|
||||
|
||||
debug('Signed token for ' + session.email);
|
||||
// res.json({ token });
|
||||
|
||||
res.json({
|
||||
access_token: accessToken,
|
||||
id_token: idToken,
|
||||
scope: 'openid%20profile%20email',
|
||||
expires_in: 7200,
|
||||
token_type: 'Bearer'
|
||||
})
|
||||
});
|
||||
|
||||
// This route can be used to generate a valid jwt-token.
|
||||
app.get('/token/:email', function (req, res) {
|
||||
if (!req.params.email) {
|
||||
debug('No user was given!');
|
||||
return res.status(400).send('user is missing');
|
||||
}
|
||||
const token = jwt.sign({
|
||||
user_id: 'auth0|' + req.params.email,
|
||||
}, privateKey);
|
||||
debug('Signed token for ' + req.params.email);
|
||||
res.json({token});
|
||||
app.get('/token/:email', (req, res) => {
|
||||
if (!req.params.email) {
|
||||
debug('No user was given!');
|
||||
return res.status(400).send('user is missing');
|
||||
}
|
||||
const token = jwt.sign({
|
||||
user_id: 'auth0|' + req.params.email,
|
||||
}, privateKey);
|
||||
debug('Signed token for ' + req.params.email);
|
||||
res.json({ token });
|
||||
});
|
||||
|
||||
app.get('/authorize', function (req, res) {
|
||||
let redirect = req.query.redirect_uri;
|
||||
let state = req.query.state;
|
||||
let nonce = req.query.nonce;
|
||||
let clientId = req.query.client_id;
|
||||
app.post('/code', (req, res) => {
|
||||
if (!req.body.email || !req.body.password || !req.body.codeChallenge) {
|
||||
debug('Body is invalid!', req.body);
|
||||
return res.status(400).send('Email or password is missing!');
|
||||
}
|
||||
|
||||
const code = req.body.codeChallenge
|
||||
challenges[req.body.codeChallenge] = code
|
||||
const state = req.body.state
|
||||
let roles = []
|
||||
if (req.body.admin === 'true') {
|
||||
roles = ['admin']
|
||||
}
|
||||
sessions[code] = {
|
||||
email: req.body.email,
|
||||
password: req.body.password,
|
||||
state: req.body.state,
|
||||
nonce: req.body.nonce,
|
||||
clientId: req.body.clientId,
|
||||
codeChallenge: req.body.codeChallenge,
|
||||
roles: roles
|
||||
}
|
||||
res.redirect(`${req.body.redirect}?domain=${issuer}&code=${code}&state=${encodeURIComponent(state)}`)
|
||||
})
|
||||
|
||||
app.get('/authorize', (req, res) => {
|
||||
const redirect = req.query.redirect_uri;
|
||||
const state = req.query.state;
|
||||
const nonce = req.query.nonce;
|
||||
const clientId = req.query.client_id;
|
||||
const codeChallenge = req.query.code_challenge;
|
||||
const prompt = req.query.prompt;
|
||||
const responseMode = req.query.response_mode;
|
||||
if (prompt === 'none' && responseMode === 'web_message') {
|
||||
const code = req.cookies['auth0']
|
||||
const session = sessions[code]
|
||||
session.nonce = nonce
|
||||
session.state = state
|
||||
session.codeChallenge = codeChallenge
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
(() => {
|
||||
const msg = {
|
||||
type: 'authorization_response',
|
||||
response: {
|
||||
code: '${code}',
|
||||
state: '${state}'
|
||||
}
|
||||
}
|
||||
parent.postMessage(msg, "*")
|
||||
})()
|
||||
</script>
|
||||
</body>
|
||||
</html>`)
|
||||
} else {
|
||||
res.cookie('auth0', codeChallenge, {
|
||||
sameSite: 'None',
|
||||
secure: true,
|
||||
httpOnly: true
|
||||
})
|
||||
res.send(`
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Auth</title>
|
||||
</head>
|
||||
<body>
|
||||
<form method="post" action="/token">
|
||||
<input type="text" name="email">
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Auth</title>
|
||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<form method="post" action="/code">
|
||||
<div class="card" style="width: 18rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Login</h5>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="text" name="email" id="email" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" name="password" id="password" class="form-control">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="admin" value="true" id="admin">
|
||||
<label class="form-check-label" for="admin">
|
||||
Admin
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
<input type="hidden" value="${redirect}" name="redirect">
|
||||
<input type="hidden" value="${state}" name="state">
|
||||
<input type="hidden" value="${nonce}" name="nonce">
|
||||
<input type="hidden" value="${clientId}" name="clientId">
|
||||
</form>
|
||||
</body>
|
||||
<input type="hidden" value="${codeChallenge}" name="codeChallenge">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/userinfo', function (req, res) {
|
||||
res.contentType("application/json").send(JSON.stringify({picture: 'https://cdn.playbuzz.com/cdn/5458360f-32ea-460e-a707-1a2d26760558/70bda687-cb84-4756-8a44-8cf735ed87b3.jpg'}))
|
||||
app.get('/userinfo', (req, res) => {
|
||||
res.contentType('application/json').send(JSON.stringify({ picture: 'https://cdn.playbuzz.com/cdn/5458360f-32ea-460e-a707-1a2d26760558/70bda687-cb84-4756-8a44-8cf735ed87b3.jpg' }))
|
||||
});
|
||||
|
||||
app.get('/.well-known/jwks.json', function (req, res) {
|
||||
res
|
||||
.contentType("application/json")
|
||||
.send(JSON.stringify({
|
||||
keys: [
|
||||
{
|
||||
alg: 'RS256',
|
||||
// e: 'AQAB',
|
||||
e: exponent,
|
||||
kid: thumbprint,
|
||||
kty: 'RSA',
|
||||
n: modulus,
|
||||
use: 'sig',
|
||||
x5c: [certDer],
|
||||
x5t: thumbprint,
|
||||
},
|
||||
],
|
||||
}));
|
||||
app.get('/v2/logout', (req, res) => {
|
||||
res.redirect(`${req.query.returnTo}?domain=${issuer}`)
|
||||
})
|
||||
|
||||
app.get('/.well-known/jwks.json', (req, res) => {
|
||||
res
|
||||
.contentType('application/json')
|
||||
.send(JSON.stringify({
|
||||
keys: [
|
||||
{
|
||||
alg: 'RS256',
|
||||
// e: 'AQAB',
|
||||
e: exponent,
|
||||
kid: thumbprint,
|
||||
kty: 'RSA',
|
||||
n: modulus,
|
||||
use: 'sig',
|
||||
x5c: [certDer],
|
||||
x5t: thumbprint,
|
||||
},
|
||||
],
|
||||
}));
|
||||
});
|
||||
|
||||
// This route returns the inside of a jwt-token. Your main application
|
||||
// should use this route to keep the auth0-flow
|
||||
app.post('/tokeninfo', function (req, res) {
|
||||
if (!req.body.id_token) {
|
||||
debug('No token given in the body!');
|
||||
return res.status(401).send('missing id_token');
|
||||
}
|
||||
const data = jwt.decode(req.body.id_token);
|
||||
if (data) {
|
||||
debug('Return token data from ' + data.user_id);
|
||||
res.json(data);
|
||||
} else {
|
||||
debug('The token was invalid and could not be decoded!');
|
||||
res.status(401).send('invalid id_token');
|
||||
}
|
||||
app.post('/tokeninfo', (req, res) => {
|
||||
if (!req.body.id_token) {
|
||||
debug('No token given in the body!');
|
||||
return res.status(401).send('missing id_token');
|
||||
}
|
||||
const data = jwt.decode(req.body.id_token);
|
||||
if (data) {
|
||||
debug('Return token data from ' + data.user_id);
|
||||
res.json(data);
|
||||
} else {
|
||||
debug('The token was invalid and could not be decoded!');
|
||||
res.status(401).send('invalid id_token');
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/issuer', (req, res) => {
|
||||
if (!req.body.issuer) {
|
||||
debug('No issuer given in the body!');
|
||||
return res.status(401).send('missing issuer');
|
||||
}
|
||||
issuer = req.body.issuer;
|
||||
jwksOrigin = `https://${issuer}/`;
|
||||
const {privateKey: key, certDer: der, thumbPrint: thumb, exponent: exp, modulus: mod} = cert(jwksOrigin);
|
||||
privateKey = key;
|
||||
certDer = der;
|
||||
thumbprint = thumb;
|
||||
exponent = exp;
|
||||
modulus = mod;
|
||||
debug('Issuer set to ' + req.body.issuer);
|
||||
res.send('ok')
|
||||
if (!req.body.issuer) {
|
||||
debug('No issuer given in the body!');
|
||||
return res.status(401).send('missing issuer');
|
||||
}
|
||||
issuer = req.body.issuer;
|
||||
jwksOrigin = `https://${issuer}/`;
|
||||
const { privateKey: key, certDer: der, thumbPrint: thumb, exponent: exp, modulus: mod } = cert(jwksOrigin);
|
||||
privateKey = key;
|
||||
certDer = der;
|
||||
thumbprint = thumb;
|
||||
exponent = exp;
|
||||
modulus = mod;
|
||||
debug('Issuer set to ' + req.body.issuer);
|
||||
res.send('ok')
|
||||
});
|
||||
|
||||
app.listen(3333, function () {
|
||||
debug('Auth0-Mock-Server listening on port 3333!');
|
||||
app.listen(3333, () => {
|
||||
debug('Auth0-Mock-Server listening on port 3333!');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user