Files
auth0mock/app.js
T
argoyle 23c9762cb8 chore: add custom claims to management token
Adds custom claims to the access and ID tokens for the 
management API. This modification allows the inclusion 
of specific claims in the tokens, improving the 
flexibility and security of the authentication process. 
The claims are added to support better access control 
and user identification.
2025-10-20 13:29:01 +02:00

453 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 claim = {}
claim[adminCustomClaim] = true
const accessToken = await signToken(
addCustomClaims('management@example.org', [claim], {
iss: jwksOrigin,
aud: [audience],
sub: 'auth0|management',
iat: date,
exp: date + 7200,
azp: req.body.client_id
})
)
const idToken = await signToken(
addCustomClaims('management@example.org', [claim], {
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!')
})