const crypto = require('crypto'); const express = require('express'); const session = require('express-session'); const { Issuer, generators } = require('openid-client'); const port = Number(process.env.PORT || 3000); const appUrl = (process.env.APP_URL || process.env.COOLIFY_URL || '').replace(/\/$/, ''); const issuerUrl = process.env.KEYCLOAK_ISSUER; const clientId = process.env.KEYCLOAK_CLIENT_ID; const clientSecret = process.env.KEYCLOAK_CLIENT_SECRET; for (const [name, value] of Object.entries({ APP_URL: appUrl, KEYCLOAK_ISSUER: issuerUrl, KEYCLOAK_CLIENT_ID: clientId, KEYCLOAK_CLIENT_SECRET: clientSecret })) { if (!value) throw new Error(`${name} is required`); } const escapeHtml = (value = '') => String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); async function main() { const issuer = await Issuer.discover(issuerUrl); const client = new issuer.Client({ client_id: clientId, client_secret: clientSecret, redirect_uris: [`${appUrl}/callback`], response_types: ['code'], }); const app = express(); app.set('trust proxy', 1); app.use(session({ secret: process.env.SESSION_SECRET || crypto.randomBytes(32).toString('hex'), resave: false, saveUninitialized: false, cookie: { secure: true, httpOnly: true, sameSite: 'lax' }, })); app.get('/health', (_req, res) => res.status(200).send('ok')); app.get('/login', (req, res) => { const codeVerifier = generators.codeVerifier(); const state = generators.state(); req.session.oidc = { codeVerifier, state }; const authorizationUrl = client.authorizationUrl({ scope: 'openid profile email', state, code_challenge: generators.codeChallenge(codeVerifier), code_challenge_method: 'S256', }); res.redirect(authorizationUrl); }); app.get('/callback', async (req, res, next) => { try { const pending = req.session.oidc; if (!pending) return res.status(400).send('Missing login session'); const tokenSet = await client.callback( `${appUrl}/callback`, client.callbackParams(req), { state: pending.state, code_verifier: pending.codeVerifier }, ); req.session.oidc = undefined; req.session.user = tokenSet.claims(); req.session.idToken = tokenSet.id_token; res.redirect('/'); } catch (error) { next(error); } }); app.get('/logout', (req, res) => { req.session.destroy(() => res.redirect('/logged-out')); }); app.get('/logged-out', (_req, res) => { res.type('html').send(`Signed out

Signed out

Sign in with Keycloak

`); }); app.get('/', (req, res) => { if (!req.session.user) return res.redirect('/login'); const user = req.session.user; const name = user.name || user.preferred_username || user.sub; res.type('html').send(` Arrowstack + Keycloak

Authenticated ✅

Hello, ${escapeHtml(name)}.

Client: ${escapeHtml(clientId)}
Issuer: ${escapeHtml(issuerUrl)}
Subject: ${escapeHtml(user.sub)}

Gitea → Coolify → Keycloak, provisioned through @Arrowstack.

Sign out locally

`); }); app.use((error, _req, res, _next) => { console.error(error); res.status(500).send('Authentication error'); }); app.listen(port, '0.0.0.0', () => console.log(`hello-keycloak-web listening on ${port}`)); } main().catch((error) => { console.error(error); process.exit(1); });