const express = require('express') const { body } = require('express-validator') const ctrl = require('./sso.controller') const { loginGuards } = require('./loginGuards') const { requireAuth } = require('../../../auth/session.middleware') const { ssoStartLimiter } = require('../../../middleware/rateLimit') const validate = require('../../../middleware/validate') const ssoRouter = express.Router() // Public discovery — the login page reads this to render provider buttons. ssoRouter.get( '/providers', // #swagger.tags = ['Auth · SSO'] // #swagger.summary = 'List enabled SSO providers' // #swagger.description = 'Public discovery used by the login page to render provider buttons. Never exposes secrets.' /* #swagger.responses[200] = { description: 'Enabled, valid providers', content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Provider" } } } } } */ ctrl.listProviders, ) // Begin login (public) — redirects to the IdP. ssoRouter.get( '/sso/:provider/start', // #swagger.tags = ['Auth · SSO'] // #swagger.summary = 'Begin SSO login (redirect to the IdP)' // #swagger.description = 'Sets a short-lived signed transaction cookie and 302-redirects to the provider authorize URL. On error redirects back to the login page with an sso_error query param.' // #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id (e.g. google, discord).' } // #swagger.parameters['returnTo'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'Internal /admin path to return to after login.' } /* #swagger.responses[302] = { description: 'Redirect to the identity provider (or back to the login page on error)' } */ ssoStartLimiter, ctrl.start, ) // Begin account linking (must be signed in — the tx captures the acting user). ssoRouter.get( '/sso/:provider/link', // #swagger.tags = ['Auth · SSO'] // #swagger.summary = 'Begin linking an SSO identity to the current account' // #swagger.description = 'Requires an authenticated session; the signed transaction captures the acting user so the callback can attach the external identity.' // #swagger.security = [{ "cookieAuth": [] }, { "bearerAuth": [] }] // #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id (e.g. google, discord).' } /* #swagger.responses[302] = { description: 'Redirect to the identity provider (or back to the account page on error)' } */ /* #swagger.responses[401] = { description: 'Not authenticated', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ requireAuth, ctrl.linkStart, ) // OAuth redirect target — completes login or linking. Not behind requireAuth: // the signed tx cookie authorizes link mode; login mode is link-only anyway. ssoRouter.get( '/sso/:provider/callback', // #swagger.tags = ['Auth · SSO'] // #swagger.summary = 'OAuth redirect target — completes login or linking' // #swagger.description = 'The provider redirects here with code + state. On success sets the session cookie (login) or links the identity (link), then 302-redirects into /admin. Login is link-only: unknown identities are refused (sso_error=not_linked).' // #swagger.parameters['provider'] = { in: 'path', required: true, schema: { type: 'string' }, description: 'Provider id (e.g. google, discord).' } // #swagger.parameters['code'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'OAuth authorization code.' } // #swagger.parameters['state'] = { in: 'query', required: false, schema: { type: 'string' }, description: 'OAuth state (matched against the tx cookie).' } /* #swagger.responses[302] = { description: 'Redirect into /admin on success, or back to login/account with an error code' } */ ctrl.callback, ) // Second factor for an SSO login whose account has TOTP enabled. The callback // stages an httpOnly pending-TOTP cookie and bounces the browser to the login // page (?sso_totp=1); the page posts the code here to finish and receive a session. ssoRouter.post( '/sso/totp', // #swagger.tags = ['Auth · SSO'] // #swagger.summary = 'Complete an SSO login with a TOTP code' // #swagger.description = 'Second step when a linked account has 2FA enabled. Reads the staged pending-TOTP cookie set by the callback plus the current authenticator code, and on success sets the session cookie. Set trustDevice to remember this browser and skip TOTP on future SSO sign-ins (30 days) — on the mobile flow this browser is the app Custom Tab, and the app additionally receives its own trustToken at /auth/mobile/sso/exchange. If the trusted-device limit is reached the sign-in still completes and the response carries { trustLimitReached, devices }. Rate limited and behind bot/backoff guards.' /* #swagger.requestBody = { required: true, content: { "application/json": { schema: { type: "object", required: ["code"], properties: { code: { type: "string" }, trustDevice: { type: "boolean" }, deviceName: { type: "string" } } } } } } */ /* #swagger.responses[200] = { description: 'Session issued (web), or a deep link to redeem (mobile bridge); optionally with a trusted-device-limit prompt', content: { "application/json": { schema: { type: "object", properties: { user: { $ref: "#/components/schemas/SafeUser" }, returnTo: { type: "string" }, redirect: { type: "string" }, trustLimitReached: { type: "boolean" }, devices: { type: "array", items: { $ref: "#/components/schemas/TrustedDevice" } } } } } } } */ /* #swagger.responses[401] = { description: 'Invalid code or expired challenge', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ /* #swagger.responses[429] = { description: 'Too many attempts (rate limited / backoff)', content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } } */ ...loginGuards, body('code').isString().trim().isLength({ min: 6, max: 8 }), body('trustDevice').optional().isBoolean(), body('deviceName').optional({ values: 'falsy' }).isString().trim().isLength({ max: 100 }), validate, ctrl.finishSsoTotp, ) module.exports = ssoRouter