Added sso

This commit is contained in:
bobban committed 2026-09-04 23:12:40 +02:00
1 parent c1c40361a7
commit 1aa64c93ae
5 files changed
+278 -3

No files matched your search

+2
View File
@@ -17,6 +17,7 @@ const diag = require('./diag');
const recordsRouter = require('./routes/records');
const settingsRouter = require('./routes/settings');
const authRouter = require('./routes/auth');
const oidcRouter = require('./routes/oidc');
const usersRouter = require('./routes/users');
const db = require('./db');
@@ -29,6 +30,7 @@ app.use(express.json());
// ─── Public routes (no auth required) ────────────────────────────────────────
app.get('/api/ping', (req, res) => res.json({ ok: true }));
app.use('/api/auth/oidc', oidcRouter); // must be before /api/auth
app.use('/api/auth', authRouter);
// ─── Protected routes (JWT required) ─────────────────────────────────────────
+117
View File
@@ -0,0 +1,117 @@
/**
* Authentik OIDC SSO routes
*
* Requires env:
* AUTHENTIK_URL Base URL of your Authentik instance, e.g. https://auth.example.com
* AUTHENTIK_CLIENT_ID OAuth2 application client ID
* AUTHENTIK_CLIENT_SECRET OAuth2 application client secret
*
* Endpoints:
* GET /api/auth/oidc/config — public, returns OIDC config for the frontend
* POST /api/auth/oidc/callback — exchanges code for token, returns a local JWT
*/
const express = require('express');
const router = express.Router();
const { signToken } = require('../auth');
const users = require('../users');
// ─── Discovery doc cache ──────────────────────────────────────────────────────
let _discovery = null;
let _discoveryTime = 0;
const DISCOVERY_TTL_MS = 5 * 60 * 1000; // 5 minutes
async function getDiscovery() {
if (_discovery && Date.now() - _discoveryTime < DISCOVERY_TTL_MS) return _discovery;
const base = (process.env.AUTHENTIK_URL || '').replace(/\/$/, '');
if (!base) throw new Error('AUTHENTIK_URL is not configured');
const res = await fetch(`${base}/application/o/.well-known/openid-configuration`);
if (!res.ok) throw new Error(`Authentik discovery fetch failed: HTTP ${res.status}`);
_discovery = await res.json();
_discoveryTime = Date.now();
return _discovery;
}
function isEnabled() {
return !!(process.env.AUTHENTIK_URL && process.env.AUTHENTIK_CLIENT_ID && process.env.AUTHENTIK_CLIENT_SECRET);
}
// ─── GET /config ──────────────────────────────────────────────────────────────
// Public — called by the login page to decide whether to show the SSO button.
router.get('/config', async (req, res) => {
if (!isEnabled()) return res.json({ enabled: false });
try {
const doc = await getDiscovery();
res.json({
enabled: true,
authorizationEndpoint: doc.authorization_endpoint,
clientId: process.env.AUTHENTIK_CLIENT_ID,
scope: 'openid profile email',
});
} catch (err) {
console.error('[oidc] config error:', err.message);
res.json({ enabled: false, error: err.message });
}
});
// ─── POST /callback ───────────────────────────────────────────────────────────
// Accepts { code, redirectUri } — exchanges for tokens, finds/creates user,
// returns { token, user } matching the standard /api/auth/login response shape.
router.post('/callback', async (req, res) => {
if (!isEnabled()) return res.status(400).json({ error: 'SSO is not configured' });
const { code, redirectUri } = req.body;
if (!code) return res.status(400).json({ error: 'code is required' });
if (!redirectUri) return res.status(400).json({ error: 'redirectUri is required' });
try {
const doc = await getDiscovery();
// Exchange authorization code for tokens
const tokenRes = await fetch(doc.token_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.AUTHENTIK_CLIENT_ID,
client_secret: process.env.AUTHENTIK_CLIENT_SECRET,
code,
redirect_uri: redirectUri,
}),
});
const tokenData = await tokenRes.json();
if (tokenData.error) {
return res.status(401).json({ error: tokenData.error_description ?? tokenData.error });
}
// Fetch user info
const userinfoRes = await fetch(doc.userinfo_endpoint, {
headers: { Authorization: `Bearer ${tokenData.access_token}` },
});
if (!userinfoRes.ok) return res.status(401).json({ error: 'Failed to fetch user info from Authentik' });
const info = await userinfoRes.json();
// Prefer email as username, fall back to preferred_username or sub
const username = (info.preferred_username || info.email || info.sub || '').toLowerCase().trim();
if (!username) return res.status(401).json({ error: 'Could not determine username from Authentik profile' });
// Find existing user or auto-create
let user = await users.findByUsername(username);
if (!user) {
// Auto-create with a random unusable password
const { randomBytes } = require('crypto');
const randomPw = randomBytes(32).toString('hex');
user = await users.create(username, randomPw);
}
const token = signToken(user);
res.json({ token, user: { id: user.id, username: user.username } });
} catch (err) {
console.error('[oidc] callback error:', err.message);
res.status(500).json({ error: err.message });
}
});
module.exports = router;