diff --git a/.changeset/forward-access-token-in-require-auth.md b/.changeset/forward-access-token-in-require-auth.md new file mode 100644 index 0000000..58115ef --- /dev/null +++ b/.changeset/forward-access-token-in-require-auth.md @@ -0,0 +1,9 @@ +--- +"@seamless-auth/express": patch +--- + +Fix requireAuth dropping the access token from req.user. The middleware now +attaches the inner access token as req.user.token, so getSeamlessUser and +buildServiceAuthorization can forward it as the bearer credential when calling +the auth server. Previously trusted server adapters received a "Missing bearer" +error and every authenticated route returned 401. diff --git a/packages/express/src/createServer.ts b/packages/express/src/createServer.ts index 03980e0..e004e19 100644 --- a/packages/express/src/createServer.ts +++ b/packages/express/src/createServer.ts @@ -85,6 +85,7 @@ export interface SeamlessAuthUser { phone: string; iat?: number; exp?: number; + token?: string; } function buildProxyQueryString(queryInput: Request["query"]): string { @@ -395,12 +396,6 @@ export function createSeamlessAuthServer( "/step-up/webauthn/finish", proxyWithIdentity("step-up/webauthn/finish", "access"), ); - - // TOTP enrollment, management, and step-up verification. All require a full - // access session and update server-side state only (no new session cookies), - // so they proxy the user's access token upstream like the step-up routes. - // TOTP-as-a-login-second-factor is intentionally not mounted here: the auth - // API does not gate login on TOTP today, so /totp/verify-login has no trigger. r.get("/totp/status", proxyWithIdentity("totp/status", "access", "GET")); r.post( "/totp/enroll/start", diff --git a/packages/express/src/middleware/requireAuth.ts b/packages/express/src/middleware/requireAuth.ts index 4ae76e1..94739ed 100644 --- a/packages/express/src/middleware/requireAuth.ts +++ b/packages/express/src/middleware/requireAuth.ts @@ -107,6 +107,7 @@ export function requireAuth(opts: RequireAuthOptions) { phone: payload.phone, iat: payload.iat, exp: payload.exp, + token: payload.token, }; req.user = user; diff --git a/packages/express/tests/requireAuth.test.js b/packages/express/tests/requireAuth.test.js index 96e6ba8..75ac4a5 100644 --- a/packages/express/tests/requireAuth.test.js +++ b/packages/express/tests/requireAuth.test.js @@ -31,4 +31,35 @@ describe("requireAuth (smoke)", () => { expect(res.status).toBe(200); expect(res.body.user.sub).toBe("user-123"); }); + + it("forwards the inner access token as req.user.token", async () => { + const secret = "cookie-secret"; + const innerAccessToken = "inner-access-jwt"; + + const token = jwt.sign( + { sub: "user-123", token: innerAccessToken }, + secret, + { expiresIn: "1h" }, + ); + + const app = express(); + app.use(cookieParser()); + app.use( + requireAuth({ + cookieName: "access", + cookieSecret: secret, + }), + ); + + app.get("/protected", (req, res) => { + res.json({ user: req.user }); + }); + + const res = await request(app) + .get("/protected") + .set("Cookie", [`access=${token}`]); + + expect(res.status).toBe(200); + expect(res.body.user.token).toBe(innerAccessToken); + }); });