overhauled mock auth solution

This commit is contained in:
2026-01-26 01:14:19 -05:00
parent b4fcb1a366
commit 083ddc345b
3 changed files with 133 additions and 99 deletions

13
api/package-lock.json generated
View File

@@ -20,6 +20,7 @@
"morgan": "^1.10.1", "morgan": "^1.10.1",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-custom": "^1.1.1",
"passport-openidconnect": "^0.1.2" "passport-openidconnect": "^0.1.2"
}, },
"devDependencies": { "devDependencies": {
@@ -3037,6 +3038,18 @@
"url": "https://github.com/sponsors/jaredhanson" "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": { "node_modules/passport-openidconnect": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz", "resolved": "https://registry.npmjs.org/passport-openidconnect/-/passport-openidconnect-0.1.2.tgz",

View File

@@ -11,13 +11,11 @@
"dev": "tsc && tsc-alias && node ./built/api/src/index.js", "dev": "tsc && tsc-alias && node ./built/api/src/index.js",
"prod": "tsc && tsc-alias && node ./built/api/src/index.js", "prod": "tsc && tsc-alias && node ./built/api/src/index.js",
"build": "tsc && tsc-alias", "build": "tsc && tsc-alias",
"migrate": "node ./scripts/migrate.js", "migrate": "node ./scripts/migrate.js",
"migrate:create": "npm run migrate -- create -ext sql -dir /migrations", "migrate:create": "npm run migrate -- create -ext sql -dir /migrations",
"migrate:seed": "node ./scripts/seed.js", "migrate:seed": "node ./scripts/seed.js",
"migrate:up": "npm run migrate -- up", "migrate:up": "npm run migrate -- up",
"migrate:down": "npm run migrate -- down 1" "migrate:down": "npm run migrate -- down 1"
}, },
"dependencies": { "dependencies": {
"@sentry/node": "^10.27.0", "@sentry/node": "^10.27.0",
@@ -31,6 +29,7 @@
"morgan": "^1.10.1", "morgan": "^1.10.1",
"mysql2": "^3.14.3", "mysql2": "^3.14.3",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-custom": "^1.1.1",
"passport-openidconnect": "^0.1.2" "passport-openidconnect": "^0.1.2"
}, },
"devDependencies": { "devDependencies": {
@@ -41,4 +40,4 @@
"tsc-alias": "^1.8.16", "tsc-alias": "^1.8.16",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

View File

@@ -15,6 +15,7 @@ import { logger } from '../services/logging/logger';
const querystring = require('querystring'); const querystring = require('querystring');
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { CacheService } from '../services/cache/cache'; import { CacheService } from '../services/cache/cache';
import { Strategy as CustomStrategy } from 'passport-custom';
function parseJwt(token) { function parseJwt(token) {
@@ -33,125 +34,137 @@ const devLogin = (req: any, res: any, next: any) => {
}); });
}; };
passport.use(new OpenIDConnectStrategy({ if (process.env.AUTH_MODE === "mock") {
issuer: process.env.AUTH_ISSUER, passport.use('mock', new CustomStrategy(async (req, done) => {
authorizationURL: process.env.AUTH_DOMAIN + '/authorize/', const mockUser = { memberId: 1 };
tokenURL: process.env.AUTH_DOMAIN + '/token/', return done(null, mockUser);
userInfoURL: process.env.AUTH_DOMAIN + '/userinfo/', }))
clientID: process.env.AUTH_CLIENT_ID, } else {
clientSecret: process.env.AUTH_CLIENT_SECRET, passport.use('oidc', new OpenIDConnectStrategy({
callbackURL: process.env.AUTH_REDIRECT_URI, issuer: process.env.AUTH_ISSUER,
scope: ['openid', 'profile', 'discord'] authorizationURL: process.env.AUTH_DOMAIN + '/authorize/',
}, async function verify(issuer, sub, profile, jwtClaims, accessToken, refreshToken, params, cb) { 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('--- OIDC verify() called ---');
// console.log('issuer:', issuer); // console.log('issuer:', issuer);
// console.log('sub:', sub); // console.log('sub:', sub);
// // console.log('discord:', discord); // // console.log('discord:', discord);
// console.log('profile:', profile); // console.log('profile:', profile);
// console.log('jwt: ', parseJwt(jwtClaims)); // console.log('jwt: ', parseJwt(jwtClaims));
// console.log('params:', params); // console.log('params:', params);
let con; let con;
try { try {
con = await pool.getConnection(); con = await pool.getConnection();
await con.beginTransaction(); await con.beginTransaction();
//lookup existing user //lookup existing user
const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]); const existing = await con.query(`SELECT id FROM members WHERE authentik_issuer = ? AND authentik_sub = ? LIMIT 1;`, [issuer, sub]);
let memberId: number | null = null; let memberId: number | null = null;
//if member exists //if member exists
if (existing.length > 0) { if (existing.length > 0) {
//login //login
memberId = existing[0].id; memberId = existing[0].id;
logger.info('auth', `Existing member login`, { 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`, {
memberId, memberId,
discordID,
issuer, issuer,
}); });
} else { } else {
// new account //otherwise: create account mode
const username = sub.username; const jwt = parseJwt(jwtClaims);
const result = await con.query( const discordID = jwt.discord?.id as number;
`INSERT INTO members (name, authentik_sub, authentik_issuer) VALUES (?, ?, ?)`,
[username, sub, issuer]
)
memberId = Number(result.insertId);
logger.info('auth', `New member account created`, { //check if account is available to claim
memberId, if (discordID)
username, memberId = await mapDiscordtoID(discordID);
issuer,
});
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(); await con.commit();
return cb(null, { memberId }); return cb(null, { memberId });
} catch (error) { } catch (error) {
logger.error('auth', `Authentication transaction failed`, { logger.error('auth', `Authentication transaction failed`, {
issuer, issuer,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined, stack: error instanceof Error ? error.stack : undefined,
}); });
if (con) { if (con) {
try { try {
await con.rollback(); await con.rollback();
} catch (rollbackError) { } catch (rollbackError) {
logger.error('auth', `Rollback failed`, { logger.error('auth', `Rollback failed`, {
error: rollbackError instanceof Error error: rollbackError instanceof Error
? rollbackError.message ? rollbackError.message
: String(rollbackError), : String(rollbackError),
}); });
}
} }
return cb(error);
} finally {
if (con) con.release();
} }
return cb(error); }));
} finally { }
if (con) con.release();
}
}));
router.get('/login', (req, res, next) => { router.get('/login', (req, res, next) => {
// Store redirect target in session if provided req.session.redirectTo = req.query.redirect as string;
req.session.redirectTo = req.query.redirect;
if (process.env.AUTH_MODE === 'mock') { const strategy = process.env.AUTH_MODE === 'mock' ? 'mock' : 'oidc';
return devLogin(req, res, next);
}
next();
}, passport.authenticate('openidconnect'));
passport.authenticate(strategy, {
successRedirect: (req.session.redirectTo || process.env.CLIENT_URL),
failureRedirect: '/login'
})(req, res, next);
});
router.get('/callback', (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; const redirectURI = req.session.redirectTo;
passport.authenticate('openidconnect', (err, user) => { passport.authenticate('oidc', (err, user) => {
if (err) return next(err); if (err) return next(err);
if (!user) return res.redirect(process.env.CLIENT_URL); if (!user) return res.redirect(process.env.CLIENT_URL);
@@ -181,12 +194,21 @@ router.get('/logout', [requireLogin], function (req, res, next) {
sameSite: 'lax' sameSite: 'lax'
}); });
if (process.env.AUTH_MODE === 'mock') {
return res.redirect(process.env.CLIENT_URL || '/');
}
var params = { var params = {
client_id: process.env.AUTH_CLIENT_ID, client_id: process.env.AUTH_CLIENT_ID,
returnTo: process.env.CLIENT_URL 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 || '/');
}
}) })
}); });
}); });