diff --git a/api/package-lock.json b/api/package-lock.json index fc9a63c..d6283cb 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -20,6 +20,7 @@ "morgan": "^1.10.1", "mysql2": "^3.14.3", "passport": "^0.7.0", + "passport-custom": "^1.1.1", "passport-openidconnect": "^0.1.2" }, "devDependencies": { @@ -3037,6 +3038,18 @@ "url": "https://github.com/sponsors/jaredhanson" } }, + "node_modules/passport-custom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz", + "integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/passport-openidconnect": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz", diff --git a/api/package.json b/api/package.json index 81d6beb..9cb3506 100644 --- a/api/package.json +++ b/api/package.json @@ -11,13 +11,11 @@ "dev": "tsc && tsc-alias && node ./built/api/src/index.js", "prod": "tsc && tsc-alias && node ./built/api/src/index.js", "build": "tsc && tsc-alias", - "migrate": "node ./scripts/migrate.js", "migrate:create": "npm run migrate -- create -ext sql -dir /migrations", "migrate:seed": "node ./scripts/seed.js", "migrate:up": "npm run migrate -- up", "migrate:down": "npm run migrate -- down 1" - }, "dependencies": { "@sentry/node": "^10.27.0", @@ -31,6 +29,7 @@ "morgan": "^1.10.1", "mysql2": "^3.14.3", "passport": "^0.7.0", + "passport-custom": "^1.1.1", "passport-openidconnect": "^0.1.2" }, "devDependencies": { @@ -41,4 +40,4 @@ "tsc-alias": "^1.8.16", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index b4c1e5d..0be26bd 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -15,6 +15,7 @@ import { logger } from '../services/logging/logger'; const querystring = require('querystring'); import { performance } from 'perf_hooks'; import { CacheService } from '../services/cache/cache'; +import { Strategy as CustomStrategy } from 'passport-custom'; function parseJwt(token) { @@ -33,125 +34,137 @@ const devLogin = (req: any, res: any, next: any) => { }); }; -passport.use(new OpenIDConnectStrategy({ - issuer: process.env.AUTH_ISSUER, - authorizationURL: process.env.AUTH_DOMAIN + '/authorize/', - tokenURL: process.env.AUTH_DOMAIN + '/token/', - userInfoURL: process.env.AUTH_DOMAIN + '/userinfo/', - clientID: process.env.AUTH_CLIENT_ID, - clientSecret: process.env.AUTH_CLIENT_SECRET, - callbackURL: process.env.AUTH_REDIRECT_URI, - scope: ['openid', 'profile', 'discord'] -}, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { +if (process.env.AUTH_MODE === "mock") { + passport.use('mock', new CustomStrategy(async (req, done) => { + const mockUser = { memberId: 1 }; + return done(null, mockUser); + })) +} else { + passport.use('oidc', new OpenIDConnectStrategy({ + issuer: process.env.AUTH_ISSUER, + authorizationURL: process.env.AUTH_DOMAIN + '/authorize/', + tokenURL: process.env.AUTH_DOMAIN + '/token/', + userInfoURL: process.env.AUTH_DOMAIN + '/userinfo/', + clientID: process.env.AUTH_CLIENT_ID, + clientSecret: process.env.AUTH_CLIENT_SECRET, + callbackURL: process.env.AUTH_REDIRECT_URI, + scope: ['openid', 'profile', 'discord'] + }, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { - // console.log('--- OIDC verify() called ---'); - // console.log('issuer:', issuer); - // console.log('sub:', sub); - // // console.log('discord:', discord); - // console.log('profile:', profile); - // console.log('jwt: ', parseJwt(jwtClaims)); - // console.log('params:', params); - let con; + // console.log('--- OIDC verify() called ---'); + // console.log('issuer:', issuer); + // console.log('sub:', sub); + // // console.log('discord:', discord); + // console.log('profile:', profile); + // console.log('jwt: ', parseJwt(jwtClaims)); + // console.log('params:', params); + let con; - try { - con = await pool.getConnection(); + try { + con = await pool.getConnection(); - await con.beginTransaction(); + await con.beginTransaction(); - //lookup existing user - const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); - let memberId: number | null = null; - //if member exists - if (existing.length > 0) { - //login - memberId = existing[0].id; - logger.info('auth', `Existing member login`, { - memberId, - issuer, - }); - - } else { - //otherwise: create account mode - const jwt = parseJwt(jwtClaims); - const discordID = jwt.discord?.id as number; - - //check if account is available to claim - if (discordID) - memberId = await mapDiscordtoID(discordID); - - if (discordID && memberId) { - // claim account - const result = await con.query( - `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`, - [sub, issuer, memberId] - ) - logger.info('auth', `Existing member claimed via Discord`, { + //lookup existing user + const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); + let memberId: number | null = null; + //if member exists + if (existing.length > 0) { + //login + memberId = existing[0].id; + logger.info('auth', `Existing member login`, { memberId, - discordID, issuer, }); } else { - // new account - const username = sub.username; - const result = await con.query( - `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`, - [username, sub, issuer] - ) - memberId = Number(result.insertId); + //otherwise: create account mode + const jwt = parseJwt(jwtClaims); + const discordID = jwt.discord?.id as number; - logger.info('auth', `New member account created`, { - memberId, - username, - issuer, - }); + //check if account is available to claim + if (discordID) + memberId = await mapDiscordtoID(discordID); + if (discordID && memberId) { + // claim account + const result = await con.query( + `UPDATE members SET authentik_sub = ?, authentik_issuer = ? WHERE id = ?;`, + [sub, issuer, memberId] + ) + logger.info('auth', `Existing member claimed via Discord`, { + memberId, + discordID, + issuer, + }); + + } else { + // new account + const username = sub.username; + const result = await con.query( + `INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`, + [username, sub, issuer] + ) + memberId = Number(result.insertId); + + logger.info('auth', `New member account created`, { + memberId, + username, + issuer, + }); + + } } - } - await con.query(`UPDATE members SET last_login = ? WHERE id = ?`, [toDateTime(new Date()), memberId]) + await con.query(`UPDATE members SET last_login = ? WHERE id = ?`, [toDateTime(new Date()), memberId]) - await con.commit(); - return cb(null, { memberId }); - } catch (error) { - logger.error('auth', `Authentication transaction failed`, { - issuer, - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - }); + await con.commit(); + return cb(null, { memberId }); + } catch (error) { + logger.error('auth', `Authentication transaction failed`, { + issuer, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); - if (con) { - try { - await con.rollback(); - } catch (rollbackError) { - logger.error('auth', `Rollback failed`, { - error: rollbackError instanceof Error - ? rollbackError.message - : String(rollbackError), - }); + if (con) { + try { + await con.rollback(); + } catch (rollbackError) { + logger.error('auth', `Rollback failed`, { + error: rollbackError instanceof Error + ? rollbackError.message + : String(rollbackError), + }); + } } + return cb(error); + } finally { + if (con) con.release(); } - return cb(error); - } finally { - if (con) con.release(); - } -})); + })); +} router.get('/login', (req, res, next) => { - // Store redirect target in session if provided - req.session.redirectTo = req.query.redirect; + req.session.redirectTo = req.query.redirect as string; - if (process.env.AUTH_MODE === 'mock') { - return devLogin(req, res, next); - } - - next(); -}, passport.authenticate('openidconnect')); + const strategy = process.env.AUTH_MODE === 'mock' ? 'mock' : 'oidc'; + passport.authenticate(strategy, { + successRedirect: (req.session.redirectTo || process.env.CLIENT_URL), + failureRedirect: '/login' + })(req, res, next); +}); router.get('/callback', (req, res, next) => { + + //escape if mocked + if (process.env.AUTH_MODE === 'mock') { + return res.redirect(process.env.CLIENT_URL || '/'); + } + const redirectURI = req.session.redirectTo; - passport.authenticate('openidconnect', (err, user) => { + passport.authenticate('oidc', (err, user) => { if (err) return next(err); if (!user) return res.redirect(process.env.CLIENT_URL); @@ -181,12 +194,21 @@ router.get('/logout', [requireLogin], function (req, res, next) { sameSite: 'lax' }); + if (process.env.AUTH_MODE === 'mock') { + return res.redirect(process.env.CLIENT_URL || '/'); + } + var params = { client_id: process.env.AUTH_CLIENT_ID, returnTo: process.env.CLIENT_URL }; - res.redirect(process.env.AUTH_END_SESSION_URI + '?' + querystring.stringify(params)); + const endSessionUri = process.env.AUTH_END_SESSION_URI; + if (endSessionUri) { + return res.redirect(endSessionUri + '?' + querystring.stringify(params)); + } else { + return res.redirect(process.env.CLIENT_URL || '/'); + } }) }); });