@wepublish/oauth2
Version:
OAuth2 Provider for we.publish
218 lines (192 loc) • 6.58 kB
text/typescript
import {Application, NextFunction, Request, Response, urlencoded} from 'express'
import Provider from 'oidc-provider'
import isEmpty from 'lodash/isEmpty'
import querystring from 'querystring'
import {inspect} from 'util'
import * as assert from 'assert'
import {PrismaClient} from '@prisma/client'
import {getUserForCredentials} from '@wepublish/api'
const body = urlencoded({extended: false})
const keys = new Set()
const debug = (obj: any) =>
querystring.stringify(
Object.entries(obj).reduce((acc: any, [key, value]) => {
keys.add(key)
if (isEmpty(value)) return acc
acc[key] = inspect(value, {depth: null})
return acc
}, {}),
'<br/>',
': ',
{
encodeURIComponent(value) {
return keys.has(value) ? `<strong>${value}</strong>` : value
}
}
)
export function routes(app: Application, provider: Provider, prisma: PrismaClient): void {
//const { constructor: { errors: { SessionNotFound } } } = provider;
app.use((req, res, next) => {
const orig = res.render
// you'll probably want to use a full blown render engine capable of layouts
res.render = (view: string, locals: any) => {
app.render(view, locals, (err, html) => {
if (err) throw err
orig.call(res, '_layout', {
...locals,
body: html
})
})
}
next()
})
function setNoCache(req: Request, res: Response, next: NextFunction) {
res.set('Pragma', 'no-cache')
res.set('Cache-Control', 'no-cache, no-store')
next()
}
app.get('/interaction/:uid', setNoCache, async (req, res, next) => {
try {
const {uid, prompt, params, session} = await provider.interactionDetails(req, res)
const client = await provider.Client.find(params.client_id)
switch (prompt.name) {
case 'login': {
return res.render('login', {
client,
uid,
details: prompt.details,
params,
title: 'Sign-in',
session: session ? debug(session) : undefined,
dbg: {
params: debug(params),
prompt: debug(prompt)
}
})
}
case 'consent': {
return res.render('interaction', {
client,
uid,
details: prompt.details,
params,
title: 'Authorize',
session: session ? debug(session) : undefined,
dbg: {
params: debug(params),
prompt: debug(prompt)
}
})
}
/*case 'select_account': {
if (!session) {
return provider.interactionFinished(req, res, { select_account: {} }, { mergeWithLastSubmission: false });
}
const account = await provider.Account.findAccount(undefined, session.accountId);
const { email } = await account.claims('prompt', 'email', { email: null }, []);
return res.render('select_account', {
client,
uid,
email,
details: prompt.details,
params,
title: 'Sign-in',
session: session ? debug(session) : undefined,
dbg: {
params: debug(params),
prompt: debug(prompt),
},
});
}*/
default:
return undefined
}
} catch (err) {
return next(err)
}
})
app.post('/interaction/:uid/login', setNoCache, body, async (req, res, next) => {
try {
const {
prompt: {name}
} = await provider.interactionDetails(req, res)
assert.equal(name, 'login')
const account = await getUserForCredentials(req.body.login, req.body.password, prisma.user)
if (!account) {
throw new Error('User not found')
}
const result = {
select_account: {}, // make sure its skipped by the interaction policy since we just logged in
login: {
account: account.id
}
}
await provider.interactionFinished(req, res, result, {mergeWithLastSubmission: false})
} catch (err) {
next(err)
}
})
app.post('/interaction/:uid/continue', setNoCache, body, async (req, res, next) => {
try {
const interaction = await provider.interactionDetails(req, res)
const {
prompt: {name}
} = interaction
assert.equal(name, 'select_account')
if (req.body.switch) {
if (interaction.params.prompt) {
const prompts = new Set(interaction.params.prompt.split(' '))
prompts.add('login')
interaction.params.prompt = [...prompts].join(' ')
} else {
interaction.params.prompt = 'logout'
}
await interaction.save()
}
const result = {select_account: {}}
await provider.interactionFinished(req, res, result, {mergeWithLastSubmission: false})
} catch (err) {
next(err)
}
})
app.post('/interaction/:uid/confirm', setNoCache, body, async (req, res, next) => {
try {
const {
prompt: {name}
} = await provider.interactionDetails(req, res)
assert.equal(name, 'consent')
const consent: any = {}
// any scopes you do not wish to grant go in here
// otherwise details.scopes.new.concat(details.scopes.accepted) will be granted
consent.rejectedScopes = []
// any claims you do not wish to grant go in here
// otherwise all claims mapped to granted scopes
// and details.claims.new.concat(details.claims.accepted) will be granted
consent.rejectedClaims = []
// replace = false means previously rejected scopes and claims remain rejected
// changing this to true will remove those rejections in favour of just what you rejected above
consent.replace = false
const result = {consent}
await provider.interactionFinished(req, res, result, {mergeWithLastSubmission: true})
} catch (err) {
next(err)
}
})
app.get('/interaction/:uid/abort', setNoCache, async (req, res, next) => {
try {
const result = {
error: 'access_denied',
error_description: 'End-User aborted interaction'
}
await provider.interactionFinished(req, res, result, {mergeWithLastSubmission: false})
} catch (err) {
next(err)
}
})
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
/*if (err instanceof SessionNotFound) {
// handle interaction expired / session not found error
}*/
next(err)
})
}