|
|
|
@ -6,6 +6,17 @@ import express from 'express'; |
|
|
|
import { existsSync } from 'node:fs'; |
|
|
|
import { join } from 'node:path'; |
|
|
|
import <% if (isStandalone) { %>bootstrap<% } else { %>AppServerModule<% } %> from './main.server'; |
|
|
|
import {environment} from './environments/environment'; |
|
|
|
import * as oidc from 'openid-client'; |
|
|
|
|
|
|
|
const ISSUER = new URL(environment.oAuthConfig.issuer); |
|
|
|
const CLIENT_ID = environment.oAuthConfig.clientId; |
|
|
|
const REDIRECT_URI = environment.oAuthConfig.redirectUri; |
|
|
|
const SCOPE = environment.oAuthConfig.scope; |
|
|
|
|
|
|
|
const config = await oidc.discovery(ISSUER, CLIENT_ID, /* client_secret */ undefined); |
|
|
|
const secureCookie = { httpOnly: true, sameSite: 'lax' as const, secure: environment.production, path: '/' }; |
|
|
|
const tokenCookie = { ...secureCookie, httpOnly: false }; |
|
|
|
|
|
|
|
// The Express app is exported so that it can be used by serverless Functions. |
|
|
|
export function app(): express.Express { |
|
|
|
@ -20,6 +31,119 @@ export function app(): express.Express { |
|
|
|
server.set('view engine', 'html'); |
|
|
|
server.set('views', distFolder); |
|
|
|
|
|
|
|
server.use(ServerCookieParser.middleware()); |
|
|
|
|
|
|
|
const sessions = new Map<string, { pkce?: string; state?: string; refresh?: string; at?: string, returnUrl?: string }>(); |
|
|
|
|
|
|
|
server.get('/authorize', async (_req, res) => { |
|
|
|
const code_verifier = oidc.randomPKCECodeVerifier(); |
|
|
|
const code_challenge = await oidc.calculatePKCECodeChallenge(code_verifier); |
|
|
|
const state = oidc.randomState(); |
|
|
|
|
|
|
|
if (_req.query.returnUrl) { |
|
|
|
const returnUrl = String(_req.query.returnUrl || null); |
|
|
|
res.cookie('returnUrl', returnUrl, { ...secureCookie, maxAge: 5 * 60 * 1000 }); |
|
|
|
} |
|
|
|
|
|
|
|
const sid = crypto.randomUUID(); |
|
|
|
sessions.set(sid, { pkce: code_verifier, state }); |
|
|
|
res.cookie('sid', sid, secureCookie); |
|
|
|
|
|
|
|
const url = oidc.buildAuthorizationUrl(config, { |
|
|
|
redirect_uri: REDIRECT_URI, |
|
|
|
scope: SCOPE, |
|
|
|
code_challenge, |
|
|
|
code_challenge_method: 'S256', |
|
|
|
state, |
|
|
|
}); |
|
|
|
res.redirect(url.toString()); |
|
|
|
}); |
|
|
|
|
|
|
|
server.get('/logout', async (req, res) => { |
|
|
|
try { |
|
|
|
const sid = req.cookies.sid; |
|
|
|
|
|
|
|
if (sid && sessions.has(sid)) { |
|
|
|
sessions.delete(sid); |
|
|
|
} |
|
|
|
|
|
|
|
res.clearCookie('sid', secureCookie); |
|
|
|
res.clearCookie('access_token', tokenCookie); |
|
|
|
res.clearCookie('refresh_token', secureCookie); |
|
|
|
res.clearCookie('expires_at', tokenCookie); |
|
|
|
res.clearCookie('returnUrl', secureCookie); |
|
|
|
|
|
|
|
const endSessionEndpoint = config.serverMetadata().end_session_endpoint; |
|
|
|
if (endSessionEndpoint) { |
|
|
|
const logoutUrl = new URL(endSessionEndpoint); |
|
|
|
logoutUrl.searchParams.set('post_logout_redirect_uri', REDIRECT_URI); |
|
|
|
logoutUrl.searchParams.set('client_id', CLIENT_ID); |
|
|
|
|
|
|
|
return res.redirect(logoutUrl.toString()); |
|
|
|
} |
|
|
|
res.redirect('/'); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('Logout error:', error); |
|
|
|
res.status(500).send('Logout error'); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
server.get('/', async (req, res, next) => { |
|
|
|
try { |
|
|
|
const { code, state } = req.query as any; |
|
|
|
if (!code || !state) return next(); |
|
|
|
|
|
|
|
const sid = req.cookies.sid; |
|
|
|
const sess = sid && sessions.get(sid); |
|
|
|
if (!sess || state !== sess.state) return res.status(400).send('invalid state'); |
|
|
|
|
|
|
|
const tokenEndpoint = config.serverMetadata().token_endpoint!; |
|
|
|
const body = new URLSearchParams({ |
|
|
|
grant_type: 'authorization_code', |
|
|
|
code: String(code), |
|
|
|
redirect_uri: environment.oAuthConfig.redirectUri, |
|
|
|
code_verifier: sess.pkce!, |
|
|
|
client_id: CLIENT_ID |
|
|
|
}); |
|
|
|
|
|
|
|
const resp = await fetch(tokenEndpoint, { |
|
|
|
method: 'POST', |
|
|
|
headers: { 'content-type': 'application/x-www-form-urlencoded' }, |
|
|
|
body, |
|
|
|
}); |
|
|
|
|
|
|
|
if (!resp.ok) { |
|
|
|
const errTxt = await resp.text(); |
|
|
|
console.error('token error:', resp.status, errTxt); |
|
|
|
return res.status(500).send('token error'); |
|
|
|
} |
|
|
|
|
|
|
|
const tokens = await resp.json(); |
|
|
|
|
|
|
|
const expiresInSec = |
|
|
|
Number(tokens.expires_in ?? tokens.expiresIn ?? 3600); |
|
|
|
const skewSec = 60; |
|
|
|
const accessExpiresAt = new Date( |
|
|
|
Date.now() + Math.max(0, expiresInSec - skewSec) * 1000 |
|
|
|
); |
|
|
|
|
|
|
|
sessions.set(sid, { ...sess, at: tokens.access_token, refresh: tokens.refresh_token }); |
|
|
|
res.cookie('access_token', tokens.access_token, {...tokenCookie, maxAge: accessExpiresAt.getTime()}); |
|
|
|
res.cookie('refresh_token', tokens.refresh_token, secureCookie); |
|
|
|
res.cookie('expires_at', String(accessExpiresAt.getTime()), tokenCookie); |
|
|
|
|
|
|
|
const returnUrl = req.cookies?.returnUrl ?? '/'; |
|
|
|
res.clearCookie('returnUrl', secureCookie); |
|
|
|
|
|
|
|
return res.redirect(returnUrl); |
|
|
|
} catch (e) { |
|
|
|
console.error('OIDC error:', e); |
|
|
|
return res.status(500).send('oidc error'); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Example Express Rest API endpoints |
|
|
|
// server.get('/api/{*splat}', (req, res) => { }); |
|
|
|
// Serve static files from /browser |
|
|
|
|