a9fe91bd84
All wildcard paths now need a name.
447 lines
13 KiB
JavaScript
447 lines
13 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 jose = require('node-jose');
|
|
const favicon = require('serve-favicon')
|
|
const initialUsers = require('./users')
|
|
|
|
const issuer = process.env.ISSUER || 'localhost:3333'
|
|
const 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')
|
|
|
|
const keyStore = jose.JWK.createKeyStore()
|
|
keyStore.generate('RSA', 2048, { alg: 'RS256', use: 'sig' })
|
|
// let { privateKey, certDer, thumbprint, exponent, modulus } = cert(jwksOrigin)
|
|
const users = initialUsers(process.env.USERS_FILE || './users.json')
|
|
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 = async (token) => {
|
|
const [key] = keyStore.all({ use: 'sig' })
|
|
const opt = { compact: true, jwk: key, fields: { typ: 'jwt' } }
|
|
return await jose.JWS.createSign(opt, key)
|
|
.update(JSON.stringify(token))
|
|
.final()
|
|
}
|
|
|
|
// Configure our small auth0-mock-server
|
|
app
|
|
.options('*all', 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', async (req, res) => {
|
|
const date = Math.floor(Date.now() / 1000)
|
|
if (req.body.grant_type === 'client_credentials' && req.body.client_id) {
|
|
const accessToken = await signToken({
|
|
iss: jwksOrigin,
|
|
aud: [audience],
|
|
sub: 'auth0|management',
|
|
iat: date,
|
|
exp: date + 7200,
|
|
azp: req.body.client_id
|
|
})
|
|
|
|
const idToken = await 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]
|
|
const accessToken = await signToken(
|
|
addCustomClaims(session.email, session.customClaims, {
|
|
iss: jwksOrigin,
|
|
aud: [audience],
|
|
sub: 'auth0|' + session.email,
|
|
iat: date,
|
|
exp: date + 7200,
|
|
azp: session.clientId
|
|
})
|
|
)
|
|
|
|
const idToken = await 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/openid-configuration', (req, res) => {
|
|
debug('Fetching OpenID configuration')
|
|
res.contentType('application/json').send(
|
|
JSON.stringify({
|
|
"issuer":
|
|
`${jwksOrigin}`,
|
|
"authorization_endpoint":
|
|
`${jwksOrigin}authorize`,
|
|
"token_endpoint":
|
|
`${jwksOrigin}oauth/token`,
|
|
"token_endpoint_auth_methods_supported":
|
|
["client_secret_basic", "private_key_jwt"],
|
|
"token_endpoint_auth_signing_alg_values_supported":
|
|
["RS256"],
|
|
"userinfo_endpoint":
|
|
`${jwksOrigin}userinfo`,
|
|
"check_session_iframe":
|
|
`${jwksOrigin}check_session`,
|
|
"end_session_endpoint":
|
|
`${jwksOrigin}end_session`,
|
|
"jwks_uri":
|
|
`${jwksOrigin}.well-known/jwks.json`,
|
|
"registration_endpoint":
|
|
`${jwksOrigin}register`,
|
|
"scopes_supported":
|
|
["openid", "profile", "email", "address",
|
|
"phone", "offline_access"],
|
|
"response_types_supported":
|
|
["code", "code id_token", "id_token", "id_token token"],
|
|
"acr_values_supported":
|
|
[],
|
|
"subject_types_supported":
|
|
["public", "pairwise"],
|
|
"userinfo_signing_alg_values_supported":
|
|
["RS256", "ES256", "HS256"],
|
|
"userinfo_encryption_alg_values_supported":
|
|
["RSA-OAEP-256", "A128KW"],
|
|
"userinfo_encryption_enc_values_supported":
|
|
["A128CBC-HS256", "A128GCM"],
|
|
"id_token_signing_alg_values_supported":
|
|
["RS256", "ES256", "HS256"],
|
|
"id_token_encryption_alg_values_supported":
|
|
["RSA-OAEP-256", "A128KW"],
|
|
"id_token_encryption_enc_values_supported":
|
|
["A128CBC-HS256", "A128GCM"],
|
|
"request_object_signing_alg_values_supported":
|
|
["none", "RS256", "ES256"],
|
|
"display_values_supported":
|
|
["page", "popup"],
|
|
"claim_types_supported":
|
|
["normal", "distributed"],
|
|
"claims_supported":
|
|
["sub", "iss", "auth_time", "acr",
|
|
"name", "given_name", "family_name", "nickname",
|
|
"profile", "picture", "website",
|
|
"email", "email_verified", "locale", "zoneinfo",
|
|
"https://unbound.se/email", "https://unbound.se/admin"],
|
|
"claims_parameter_supported":
|
|
true,
|
|
"service_documentation":
|
|
"http://auth0/",
|
|
"ui_locales_supported":
|
|
["en-US"]
|
|
})
|
|
)
|
|
})
|
|
|
|
app.get('/.well-known/jwks.json', (req, res) => {
|
|
debug('Fetching JWKS')
|
|
res.contentType('application/json').send(keyStore.toJSON())
|
|
})
|
|
|
|
// 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.get('/api/v2/users-by-email', (req, res) => {
|
|
const email = req.query.email
|
|
console.log('users', users)
|
|
const user = users[email]
|
|
if (user === undefined) {
|
|
res.json([])
|
|
} else {
|
|
res.json([user])
|
|
}
|
|
})
|
|
|
|
app.patch('/api/v2/users/:userid', (req, res) => {
|
|
const email = req.params.userid.slice(6)
|
|
console.log('patching user with id', email)
|
|
const user = users[email]
|
|
if (!user) {
|
|
res.sendStatus(404)
|
|
return
|
|
}
|
|
users[email] = {
|
|
email: email,
|
|
given_name: req.body.given_name || user.given_name,
|
|
family_name: req.body.family_name || user.family_name,
|
|
user_id: email,
|
|
picture: req.body.picture || user.picture
|
|
}
|
|
res.json({
|
|
user_id: `auth0|${email}`
|
|
})
|
|
})
|
|
|
|
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!')
|
|
})
|