UNPKG

authkit-js

Version:

Express auth toolkit (JWT, Sessions with Redis, Google/GitHub OAuth) in JavaScript

91 lines (82 loc) 4.08 kB
const { issueTokens, verifyToken } = require('../utils/jwt'); const { cookieOptions } = require('../utils/cookies'); const { Errors } = require('../utils/errors'); class JWTStrategy { constructor(cfg) { if (!cfg || !cfg.secret) throw new Error('JWTStrategy: secret required'); this.secret = cfg.secret; this.opts = cfg.opts || {}; this.cookieCfg = Object.assign({ names: { access: 'atk', refresh: 'rtk' } }, cfg.cookie || {}); this.deserialize = cfg.deserializeUser || (async (payload) => ({ id: payload.sub })); // Optional refresh token store (Map-like). // API expected: get(userId) => storedToken | null, set(userId, token, ttlSec?), del(userId) this.refreshStore = cfg.refreshStore || null; } async login(res, user) { const tokens = issueTokens(user, this.secret, this.opts); const c = cookieOptions(this.cookieCfg); const names = (this.cookieCfg.names || { access: 'atk', refresh: 'rtk' }); const atMax = (this.opts.accessTokenTtlSec || 900) * 1000; const rtMax = (this.opts.refreshTokenTtlSec || 604800) * 1000; res.cookie(names.access, tokens.accessToken, { ...c, maxAge: atMax }); res.cookie(names.refresh, tokens.refreshToken, { ...c, maxAge: rtMax }); // Persist refresh token if store is present (single active token per user) if (this.refreshStore && user && user.id != null) { const ttlSec = this.opts.refreshTokenTtlSec || 604800; await this.refreshStore.set(String(user.id), tokens.refreshToken, ttlSec); } return tokens; } async authenticate(req) { const names = (this.cookieCfg.names || { access: 'atk', refresh: 'rtk' }); const hdr = req.headers && (req.headers.authorization || req.headers.Authorization); const bearer = (typeof hdr === 'string' && hdr.startsWith('Bearer ')) ? hdr.slice(7) : undefined; const token = bearer || (req.cookies && req.cookies[names.access]); if (!token) return null; try { const payload = verifyToken(token, this.secret, this.opts); const user = await this.deserialize(payload); if (!user) return null; return { user, token }; } catch (_) { return null; } } async refresh(req, res) { const names = (this.cookieCfg.names || { access: 'atk', refresh: 'rtk' }); const rtk = req.cookies && req.cookies[names.refresh]; if (!rtk) throw Errors.BAD_REQUEST('Missing refresh token'); let payload; try { payload = verifyToken(rtk, this.secret, this.opts); } catch { throw Errors.TOKEN_INVALID(); } const user = await this.deserialize(payload); if (!user) throw Errors.UNAUTHORIZED('User not found'); // Reuse detection: if store exists, the incoming token must match the stored one if (this.refreshStore) { const stored = await this.refreshStore.get(String(user.id)); if (!stored) { // No stored token means logout/rotation elsewhere throw Errors.TOKEN_REUSE('Refresh token not recognized'); } if (stored !== rtk) { // Token reuse attempt: clear stored token and reject await this.refreshStore.del(String(user.id)); throw Errors.TOKEN_REUSE(); } } const tokens = issueTokens(user, this.secret, this.opts); const c = cookieOptions(this.cookieCfg); res.cookie(names.access, tokens.accessToken, { ...c, maxAge: (this.opts.accessTokenTtlSec || 900) * 1000 }); res.cookie(names.refresh, tokens.refreshToken, { ...c, maxAge: (this.opts.refreshTokenTtlSec || 604800) * 1000 }); // Rotate stored token on successful refresh if (this.refreshStore && user && user.id != null) { const ttlSec = this.opts.refreshTokenTtlSec || 604800; await this.refreshStore.set(String(user.id), tokens.refreshToken, ttlSec); } return tokens; } async logout(res) { const names = (this.cookieCfg.names || { access: 'atk', refresh: 'rtk' }); res.clearCookie(names.access); res.clearCookie(names.refresh); } } module.exports = { JWTStrategy };