Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/registration-session-cookie.md
Original file line number Diff line number Diff line change
@@ -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.
42 changes: 36 additions & 6 deletions packages/core/src/handlers/verifyLoginOtpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifyLoginOtpResult> {
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,
Expand All @@ -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,
Expand Down Expand Up @@ -101,3 +109,25 @@ export async function verifyLoginOtpHandler(
],
};
}

export async function verifyLoginOtpHandler(
input: VerifyLoginOtpInput,
opts: VerifyLoginOtpOptions,
): Promise<VerifyLoginOtpResult> {
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<VerifyLoginOtpResult> {
const path =
input.kind === "email" ? "otp/verify-email-otp" : "otp/verify-phone-otp";

return verifyOtp(path, input, opts);
}
15 changes: 8 additions & 7 deletions packages/express/src/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"),
Expand Down
34 changes: 31 additions & 3 deletions packages/express/src/handlers/verifyLoginOtp.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -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");
}
Loading