@simulacrum/auth0-simulator
Version:
Run local instance of Auth0 API for local development and integration testing
1 lines • 13.9 kB
Source Map (JSON)
{"version":3,"file":"auth0-handlers.mjs","names":["authorizeHandlers: Record<ResponseModes, RequestHandler>","responseClientId: string","responseAudience: string","token: string | null","decodeToken"],"sources":["../../src/handlers/auth0-handlers.ts"],"sourcesContent":["import type { ExtendedSimulationStore } from \"../store/index.ts\";\nimport type { Request, RequestHandler } from \"express\";\nimport type {\n Auth0Configuration,\n QueryParams,\n ResponseModes,\n} from \"../types.ts\";\nimport { createLoginRedirectHandler } from \"./login-redirect.ts\";\nimport { createWebMessageHandler } from \"./web-message.ts\";\nimport { loginView } from \"../views/login.ts\";\nimport { createTokens } from \"./oauth-handlers.ts\";\nimport { assert } from \"assert-ts\";\nimport { stringify } from \"querystring\";\nimport { encode } from \"base64-url\";\nimport { userNamePasswordForm } from \"../views/username-password.ts\";\nimport { decode as decodeToken } from \"jsonwebtoken\";\nimport { createPersonQuery } from \"./utils.ts\";\n\nexport type Routes =\n | \"/heartbeat\"\n | \"/authorize\"\n | \"/login\"\n | \"/usernamepassword/login\"\n | \"/login/callback\"\n | \"/oauth/token\"\n | \"/v2/logout\"\n | \"/userinfo\"\n | \"/passwordless/start\";\n\nexport type AuthSession = { username: string; nonce: string };\n\ntype LoggerArgs = Parameters<typeof console.dir>;\n\nconst createLogger = (debug: boolean) => ({\n log: (...args: LoggerArgs): void => {\n if (!debug) {\n return;\n }\n\n console.dir(...args);\n },\n});\n\nexport const createAuth0Handlers = (\n simulationStore: ExtendedSimulationStore,\n serviceURL: (request: Request) => string,\n options: Auth0Configuration,\n debug: boolean\n): Record<Routes, RequestHandler> => {\n let { audience, scope, clientID, rulesDirectory } = options;\n let personQuery = createPersonQuery(simulationStore);\n\n let authorizeHandlers: Record<ResponseModes, RequestHandler> = {\n query: createLoginRedirectHandler(options),\n web_message: createWebMessageHandler(),\n };\n\n let logger = createLogger(debug);\n\n return {\n [\"/heartbeat\"]: function (_, res) {\n res.status(200).json({ ok: true });\n },\n\n [\"/authorize\"]: function (req, res, next) {\n logger.log({\n \"/authorize\": {\n body: req.body,\n query: req.query,\n session: req.session,\n },\n });\n let currentUser = req.query.currentUser as string | undefined;\n\n assert(!!req.session, \"no session\");\n\n if (currentUser) {\n // the request is a silent login.\n // We fake an existing login by\n // adding the user to the session\n req.session.username = currentUser;\n }\n\n let responseMode = (req.query.response_mode ?? \"query\") as ResponseModes;\n\n assert(\n [\"query\", \"web_message\"].includes(responseMode),\n `unknown response_mode ${responseMode}`\n );\n\n let handler = authorizeHandlers[responseMode];\n\n handler(req, res, next);\n },\n\n [\"/login\"]: function (req, res) {\n logger.log({ \"/login\": { body: req.body, query: req.query } });\n let query = req.query as QueryParams;\n let responseClientId = query.client_id ?? clientID;\n let responseAudience = query.audience ?? audience;\n assert(!!responseClientId, `no clientID assigned`);\n\n let html = loginView({\n domain: new URL(serviceURL(req)).host,\n scope,\n redirectUri: query.redirect_uri,\n clientID: responseClientId,\n audience: responseAudience,\n loginFailed: false,\n });\n\n res.set(\"Content-Type\", \"text/html\");\n\n res.status(200).send(Buffer.from(html));\n },\n\n [\"/usernamepassword/login\"]: function (req, res) {\n logger.log({\n \"/usernamepassword/login\": { body: req.body, query: req.query },\n });\n let { username, nonce, password } = req.body;\n\n assert(!!username, \"no username in /usernamepassword/login\");\n assert(!!nonce, \"no nonce in /usernamepassword/login\");\n assert(!!req.session, \"no session\");\n\n let user = personQuery(\n (person) =>\n person.email?.toLowerCase() === username.toLowerCase() &&\n person.password === password\n );\n\n if (!user) {\n let query = req.query as QueryParams;\n let responseClientId = query.client_id ?? clientID;\n let responseAudience = query.audience ?? audience;\n\n assert(!!clientID, `no clientID assigned`);\n\n let html = loginView({\n domain: new URL(serviceURL(req)).host,\n scope,\n redirectUri: query.redirect_uri,\n clientID: responseClientId,\n audience: responseAudience,\n loginFailed: true,\n });\n\n res.set(\"Content-Type\", \"text/html\");\n\n res.status(400).send(html);\n return;\n }\n\n req.session.username = username;\n\n simulationStore.store.dispatch(\n simulationStore.actions.batchUpdater([\n simulationStore.schema.sessions.patch({\n [nonce]: { username, nonce },\n }),\n ])\n );\n\n res.status(200).send(userNamePasswordForm(req.body));\n },\n\n [\"/login/callback\"]: function (req, res) {\n let wctx = JSON.parse(req.body.wctx);\n logger.log({\n \"/login/callback\": { body: req.body, query: req.query, wctx },\n });\n\n let { redirect_uri, nonce } = wctx;\n\n const session = simulationStore.schema.sessions.selectById(\n simulationStore.store.getState(),\n { id: nonce }\n );\n\n const { username } = session ?? {};\n\n let encodedNonce = encode(`${nonce}:${username}`);\n\n let qs = stringify({ code: encodedNonce, ...wctx });\n\n let routerUrl = `${redirect_uri}?${qs}`;\n\n res.redirect(302, routerUrl);\n },\n\n [\"/oauth/token\"]: async function (req, res, next) {\n logger.log({ \"/oauth/token\": { body: req.body, query: req.query } });\n try {\n let iss = serviceURL(req);\n\n let responseClientId: string =\n (req?.body?.client_id as string) ?? clientID;\n let responseAudience: string =\n (req?.body?.audience as string) ?? audience;\n\n assert(\n !!responseClientId,\n \"500::no clientID in options or request body\"\n );\n\n let tokens = await createTokens({\n simulationStore,\n body: req.body,\n iss,\n clientID: responseClientId,\n audience: responseAudience,\n rulesDirectory,\n scope,\n });\n\n res.status(200).json({\n ...tokens,\n expires_in: 86400,\n token_type: \"Bearer\",\n });\n } catch (error) {\n next(error);\n }\n },\n\n [\"/v2/logout\"]: function (req, res) {\n req.session = null;\n\n let returnToUrl = req.query.returnTo ?? req.headers.referer;\n\n assert(typeof returnToUrl === \"string\", `no logical returnTo url`);\n\n res.redirect(returnToUrl);\n },\n\n [\"/userinfo\"]: function (req, res) {\n let token: string | null = null;\n if (req.headers.authorization) {\n let authorizationHeader = req.headers.authorization;\n token = authorizationHeader?.split(\" \")?.[1];\n } else {\n token = req?.query?.access_token as string;\n }\n\n assert(!!token, \"no authorization header or access_token\");\n let { sub } = decodeToken(token, { json: true }) as { sub: string };\n\n let user = personQuery((person) => {\n assert(!!person.id, `no email defined on person scenario`);\n\n return person.id === sub;\n });\n\n assert(!!user, \"no user in /userinfo\");\n\n let userinfo = {\n sub,\n name: user.name,\n given_name: user.name,\n family_name: user.name,\n email: user.email,\n email_verified: true,\n locale: \"en\",\n hd: \"okta.com\",\n };\n\n res.status(200).json(userinfo);\n },\n\n [\"/passwordless/start\"]: function (req, res, next) {\n logger.log({ \"/passwordless/start\": { body: req.body } });\n\n try {\n const { client_id, connection, email, phone_number } = req.body;\n\n // Validate required fields\n if (!client_id) {\n res.status(400).json({ error: \"client_id is required\" });\n return;\n }\n\n if (!connection || (connection !== \"email\" && connection !== \"sms\")) {\n res.status(400).json({\n error: \"connection must be 'email' or 'sms'\",\n });\n return;\n }\n\n if (connection === \"email\" && !email) {\n res.status(400).json({\n error: \"email is required when connection is 'email'\",\n });\n return;\n }\n\n if (connection === \"sms\" && !phone_number) {\n res.status(400).json({\n error: \"phone_number is required when connection is 'sms'\",\n });\n return;\n }\n\n // Return appropriate response based on connection type\n if (connection === \"email\") {\n res.status(200).json({\n _id: \"000000000000000000000000\",\n email: email,\n email_verified: false,\n });\n } else {\n res.status(200).json({\n _id: \"000000000000000000000000\",\n phone_number: phone_number,\n phone_verified: false,\n });\n }\n } catch (error) {\n next(error);\n }\n },\n };\n};\n"],"mappings":"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"}