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
9 changes: 9 additions & 0 deletions .changeset/otp-generate-csrf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@seamless-auth/express": minor
---

Serve the OTP generate routes over POST instead of GET.

`GET /auth/otp/generate-phone-otp`, `-email-otp`, and their `-login-` variants were state-changing routes (each sends an SMS or email) reachable as a simple cross-site request, so an `<img src>` on any page could trigger unbounded OTP messages to a signed-in user. This is the same vector already closed for `/auth/magic-link`.

BREAKING: the four `GET /auth/otp/generate-*` routes are removed and replaced with POST. Pair this with `@seamless-auth/react` 0.5.0 or later, which requests them over POST. An older SDK that still issues GET will get a 404.
13 changes: 8 additions & 5 deletions packages/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,14 @@ Routes include:
- `DELETE /auth/logout` for the current session
- `DELETE /auth/logout/all` for every session owned by the current user
- `POST /auth/magic-link` to request a magic-link email

`GET /auth/logout` and `GET /auth/magic-link` were removed. Both were
state-changing routes reachable as simple cross-site requests (an `<img src>` on
any page could revoke every session or trigger magic-link emails). Use
`DELETE /auth/logout/all` and `POST /auth/magic-link` instead.
- `POST /auth/otp/generate-phone-otp`, `-email-otp`, and their `-login-` variants to send an OTP

`GET /auth/logout` and `GET /auth/magic-link` were removed, and the four
`GET /auth/otp/generate-*` routes are now POST only. All were state-changing
routes reachable as simple cross-site requests (an `<img src>` on any page could
revoke every session, trigger magic-link emails, or send unbounded OTP SMS and
email). Use `DELETE /auth/logout/all`, `POST /auth/magic-link`, and the POST
generate routes instead.

**Options**

Expand Down
8 changes: 4 additions & 4 deletions packages/express/src/createServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,16 +341,16 @@ export function createSeamlessAuthServer(
verifyLoginOtp(req, res, resolvedOpts, "email"),
);

r.get("/otp/generate-phone-otp", (req, res) =>
r.post("/otp/generate-phone-otp", (req, res) =>
requestOtp(req, res, resolvedOpts, "phone"),
);
r.get("/otp/generate-email-otp", (req, res) =>
r.post("/otp/generate-email-otp", (req, res) =>
requestOtp(req, res, resolvedOpts, "email"),
);
r.get("/otp/generate-login-phone-otp", (req, res) =>
r.post("/otp/generate-login-phone-otp", (req, res) =>
requestOtp(req, res, resolvedOpts, "phone", "login"),
);
r.get("/otp/generate-login-email-otp", (req, res) =>
r.post("/otp/generate-login-email-otp", (req, res) =>
requestOtp(req, res, resolvedOpts, "email", "login"),
);

Expand Down
23 changes: 22 additions & 1 deletion packages/express/tests/loginOtpRoutes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe("login OTP routes", () => {
);

const res = await request(createApp())
.get("/auth/otp/generate-login-email-otp")
.post("/auth/otp/generate-login-email-otp")
.set("Cookie", createPreAuthCookie());

expect(res.status).toBe(200);
Expand Down Expand Up @@ -105,4 +105,25 @@ describe("login OTP routes", () => {
}),
);
});
// The generate routes are state changing (they send an SMS or email). Serving
// them over GET made them reachable as a simple cross-site request, so an
// <img> tag could trigger unbounded sends to a signed-in user. They are now
// POST only. Carries the pre-auth cookie so the cookie middleware does not
// answer 400 before routing, which would pass whether or not the route exists.
it("no longer exposes the generate routes over GET", async () => {
for (const path of [
"/auth/otp/generate-phone-otp",
"/auth/otp/generate-email-otp",
"/auth/otp/generate-login-phone-otp",
"/auth/otp/generate-login-email-otp",
]) {
const res = await request(createApp())
.get(path)
.set("Cookie", createPreAuthCookie());

expect([404, 405]).toContain(res.status);
}

expect(global.fetch).not.toHaveBeenCalled();
});
});
Loading