diff --git a/.changeset/registration-session-cookie.md b/.changeset/registration-session-cookie.md new file mode 100644 index 0000000..9a5b010 --- /dev/null +++ b/.changeset/registration-session-cookie.md @@ -0,0 +1,13 @@ +--- +"@seamless-auth/core": minor +"@seamless-auth/express": patch +--- + +Issue a session on OTP-based registration. Registration now starts with just an +email, and verifying the registration email OTP completes sign-up and returns a +session. The adapter previously proxied `/otp/verify-email-otp` and +`/otp/verify-phone-otp` without setting cookies, so browser users finished +registration unauthenticated. A new `verifyRegistrationOtpHandler` (core) plus a +`verifyRegistrationOtp` express handler now set the session cookies on these +routes (tolerating a phone-first step that returns no session yet), mirroring the +login OTP verify handlers. diff --git a/packages/core/src/handlers/verifyLoginOtpHandler.ts b/packages/core/src/handlers/verifyLoginOtpHandler.ts index 1dc257c..7057763 100644 --- a/packages/core/src/handlers/verifyLoginOtpHandler.ts +++ b/packages/core/src/handlers/verifyLoginOtpHandler.ts @@ -28,15 +28,16 @@ export interface VerifyLoginOtpResult { }[]; } -export async function verifyLoginOtpHandler( +// Shared by the login and registration OTP verify handlers: POST to the given +// auth-server path and, when the response carries a session, validate the signed +// access token and build the session cookies. Registration can complete without +// a session yet (e.g. a phone-first step before email is verified), in which case +// there is no token to turn into cookies — the body is returned as-is. +async function verifyOtp( + path: string, input: VerifyLoginOtpInput, opts: VerifyLoginOtpOptions, ): Promise { - const path = - input.kind === "email" - ? "otp/verify-login-email-otp" - : "otp/verify-login-phone-otp"; - const up = await authFetch(`${opts.authServerUrl}/${path}`, { method: "POST", body: input.body, @@ -53,6 +54,13 @@ export async function verifyLoginOtpHandler( }; } + if (!data?.token) { + return { + status: up.status, + body: data, + }; + } + const verifiedAccessToken = await verifySignedAuthResponse( data.token, opts.authServerUrl, @@ -101,3 +109,25 @@ export async function verifyLoginOtpHandler( ], }; } + +export async function verifyLoginOtpHandler( + input: VerifyLoginOtpInput, + opts: VerifyLoginOtpOptions, +): Promise { + const path = + input.kind === "email" + ? "otp/verify-login-email-otp" + : "otp/verify-login-phone-otp"; + + return verifyOtp(path, input, opts); +} + +export async function verifyRegistrationOtpHandler( + input: VerifyLoginOtpInput, + opts: VerifyLoginOtpOptions, +): Promise { + const path = + input.kind === "email" ? "otp/verify-email-otp" : "otp/verify-phone-otp"; + + return verifyOtp(path, input, opts); +} diff --git a/packages/express/src/createServer.ts b/packages/express/src/createServer.ts index 62deeae..2625073 100644 --- a/packages/express/src/createServer.ts +++ b/packages/express/src/createServer.ts @@ -8,7 +8,10 @@ import { login } from "./handlers/login"; import { finishLogin } from "./handlers/finishLogin"; import { register } from "./handlers/register"; import { requestOtp } from "./handlers/requestOtp"; -import { verifyLoginOtp } from "./handlers/verifyLoginOtp"; +import { + verifyLoginOtp, + verifyRegistrationOtp, +} from "./handlers/verifyLoginOtp"; import { switchOrganization } from "./handlers/switchOrganization"; import { finishRegister } from "./handlers/finishRegister"; import { me } from "./handlers/me"; @@ -274,13 +277,11 @@ export function createSeamlessAuthServer( finishRegister(req, res, resolvedOpts), ); - r.post( - "/otp/verify-phone-otp", - proxyWithIdentity("otp/verify-phone-otp", "preAuth"), + r.post("/otp/verify-phone-otp", (req, res) => + verifyRegistrationOtp(req, res, resolvedOpts, "phone"), ); - r.post( - "/otp/verify-email-otp", - proxyWithIdentity("otp/verify-email-otp", "preAuth"), + r.post("/otp/verify-email-otp", (req, res) => + verifyRegistrationOtp(req, res, resolvedOpts, "email"), ); r.post("/otp/verify-login-phone-otp", (req, res) => verifyLoginOtp(req, res, resolvedOpts, "phone"), diff --git a/packages/express/src/handlers/verifyLoginOtp.ts b/packages/express/src/handlers/verifyLoginOtp.ts index 19f361c..8a4a556 100644 --- a/packages/express/src/handlers/verifyLoginOtp.ts +++ b/packages/express/src/handlers/verifyLoginOtp.ts @@ -1,15 +1,19 @@ import { Request, Response } from "express"; -import { verifyLoginOtpHandler } from "@seamless-auth/core/handlers/verifyLoginOtpHandler"; +import { + verifyLoginOtpHandler, + verifyRegistrationOtpHandler, +} from "@seamless-auth/core/handlers/verifyLoginOtpHandler"; import { setSessionCookie } from "../internal/cookie"; import { buildServiceAuthorization } from "../internal/buildAuthorization"; import { buildForwardedClientIp } from "../internal/buildForwardedClientIp"; import { SeamlessAuthServerOptions } from "../createServer"; -export async function verifyLoginOtp( +async function verifyOtp( req: Request & { cookiePayload?: any }, res: Response, opts: SeamlessAuthServerOptions, kind: "email" | "phone", + flow: "login" | "register", ) { const cookieSigner = { secret: opts.cookieSecret, @@ -20,7 +24,10 @@ export async function verifyLoginOtp( : ("lax" as "none" | "lax"), }; - const result = await verifyLoginOtpHandler( + const handler = + flow === "register" ? verifyRegistrationOtpHandler : verifyLoginOtpHandler; + + const result = await handler( { body: req.body, authorization: buildServiceAuthorization(req, opts), @@ -60,3 +67,24 @@ export async function verifyLoginOtp( return res.status(result.status).json(result.body); } + +export function verifyLoginOtp( + req: Request & { cookiePayload?: any }, + res: Response, + opts: SeamlessAuthServerOptions, + kind: "email" | "phone", +) { + return verifyOtp(req, res, opts, kind, "login"); +} + +// Registration OTP verify: identical cookie handling, but a successful email +// verify now completes registration and issues a session, so the session cookies +// must be set here (a phone-first step that returns no session sets none). +export function verifyRegistrationOtp( + req: Request & { cookiePayload?: any }, + res: Response, + opts: SeamlessAuthServerOptions, + kind: "email" | "phone", +) { + return verifyOtp(req, res, opts, kind, "register"); +}