Files
auth0mock/app.js
T
2023-05-31 23:24:38 +02:00

397 lines
10 KiB
JavaScript

process.env.DEBUG = 'app*'
const express = require('express')
const cookieParser = require('cookie-parser')
const app = express()
const jwt = require('jsonwebtoken')
const Debug = require('debug')
const path = require('path')
const cors = require('cors')
const bodyParser = require('body-parser')
const favicon = require('serve-favicon')
const cert = require('./cert')
let issuer = process.env.ISSUER || 'localhost:3333'
let jwksOrigin = `https://${issuer}/`
const audience = process.env.AUDIENCE || 'https://generic-audience'
const adminCustomClaim =
process.env.ADMIN_CUSTOM_CLAIM || 'https://unbound.se/admin'
const emailCustomClaim =
process.env.EMAIL_CUSTOM_CLAIM || 'https://unbound.se/email'
const debug = Debug('app')
let { privateKey, certDer, thumbprint, exponent, modulus } = cert(jwksOrigin)
const users = {}
const sessions = {}
const challenges = {}
const corsOpts = (req, cb) => {
cb(null, { origin: req.headers.origin })
}
const addCustomClaims = (email, customClaims, token) => {
const emailClaim = {}
emailClaim[emailCustomClaim] = email
return [...customClaims, emailClaim].reduce((acc, claim) => {
return {
...acc,
...claim
}
}, token)
}
const signToken = (token) => {
return jwt.sign(Buffer.from(JSON.stringify(token)), privateKey, {
algorithm: 'RS256',
keyid: thumbprint
})
}
// Configure our small auth0-mock-server
app
.options('*', cors(corsOpts))
.use(cors())
.use(bodyParser.json({ strict: false }))
.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('/oauth/token', (req, res) => {
let date = Math.floor(Date.now() / 1000)
if (req.body.grant_type === 'client_credentials' && req.body.client_id) {
let accessToken = signToken({
iss: jwksOrigin,
aud: [audience],
sub: 'auth0|management',
iat: date,
exp: date + 7200,
azp: req.body.client_id
})
let idToken = signToken({
iss: jwksOrigin,
aud: req.body.client_id,
sub: 'auth0|management',
iat: date,
exp: date + 7200,
azp: req.body.client_id,
name: 'Management API'
})
debug('Signed token for management API')
res.json({
access_token: accessToken,
id_token: idToken,
scope: 'openid%20profile%20email',
expires_in: 7200,
token_type: 'Bearer'
})
} else if (req.body.code) {
const code = req.body.code
const session = sessions[code]
let accessToken = signToken(
addCustomClaims(session.email, session.customClaims, {
iss: jwksOrigin,
aud: [audience],
sub: 'auth0|' + session.email,
iat: date,
exp: date + 7200,
azp: session.clientId
})
)
let idToken = signToken(
addCustomClaims(session.email, session.customClaims, {
iss: jwksOrigin,
aud: session.clientId,
nonce: session.nonce,
sub: 'auth0|' + session.email,
iat: date,
exp: date + 7200,
azp: session.clientId,
name: 'Example Person',
given_name: 'Example',
family_name: 'Person',
email: session.email,
picture:
'https://cdn.playbuzz.com/cdn/5458360f-32ea-460e-a707-1a2d26760558/70bda687-cb84-4756-8a44-8cf735ed87b3.jpg'
})
)
debug('Signed token for ' + session.email)
res.json({
access_token: accessToken,
id_token: idToken,
scope: 'openid%20profile%20email',
expires_in: 7200,
token_type: 'Bearer'
})
} else {
res.status(401)
res.send('Missing client_id or client_secret')
}
})
// This route can be used to generate a valid jwt-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.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
const claim = {}
claim[adminCustomClaim] = req.body.admin === 'true'
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,
customClaims: [claim]
}
res.redirect(
`${req.body.redirect}?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 (responseMode === 'query') {
const code = req.cookies['auth0']
const session = sessions[code]
if (session) {
session.nonce = nonce
session.state = state
session.codeChallenge = codeChallenge
sessions[codeChallenge] = session
res.redirect(`${redirect}?code=${codeChallenge}&state=${state}`)
return
}
}
if (prompt === 'none' && responseMode === 'web_message') {
const code = req.cookies['auth0']
const session = sessions[code]
if (session) {
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>`)
return
}
}
res.cookie('auth0', codeChallenge, {
sameSite: 'None',
secure: true,
httpOnly: true
})
res.send(`
<html lang='en'>
<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'>
<input type='hidden' value='${codeChallenge}' name='codeChallenge'>
</div>
</div>
</form>
</div>
</body>
</html>
`)
})
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('/v2/logout', (req, res) => {
const code = req.cookies['auth0']
const session = sessions[code]
if (session) {
delete sessions[code]
}
res.redirect(req.query.returnTo)
})
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', (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')
})
app.get('/api/v2/users-by-email', (req, res) => {
const email = req.query.email
const user = users[email]
if (user === undefined) {
res.json([])
} else {
res.json([
user
])
}
})
app.post('/api/v2/users', (req, res) => {
const email = req.body.email
users[email] = {
"email": email,
"given_name": "Given",
"family_name": "Last",
"user_id": email,
}
res.json({
user_id: `auth0|${email}`
})
})
app.post('/api/v2/tickets/password-change', (req, res) => {
res.json({
ticket: `https://some-url`
})
})
app.use(function (req, res, next) {
console.log('404', req.path)
res.status(404).send('error: 404 Not Found ' + req.path)
})
app.listen(3333, () => {
debug('Auth0-Mock-Server listening on port 3333!')
})