From b3f47771ec34663a3924490dafdfcc53191144e4 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Fri, 29 May 2026 22:57:48 -0400 Subject: [PATCH 1/4] chore: updated logout function to support single session or multiple sessions --- packages/core/src/ensureCookies.ts | 1 + packages/core/src/handlers/logout.ts | 28 ++++- packages/core/tests/logoutHandler.test.js | 59 ++++++++++ packages/express/README.md | 4 +- packages/express/src/createServer.ts | 10 +- packages/express/src/handlers/logout.ts | 7 +- .../src/internal/buildAuthorization.ts | 2 + packages/express/tests/logoutRoutes.test.js | 108 ++++++++++++++++++ 8 files changed, 210 insertions(+), 9 deletions(-) create mode 100644 packages/core/tests/logoutHandler.test.js create mode 100644 packages/express/tests/logoutRoutes.test.js diff --git a/packages/core/src/ensureCookies.ts b/packages/core/src/ensureCookies.ts index 8761342..8d6b847 100644 --- a/packages/core/src/ensureCookies.ts +++ b/packages/core/src/ensureCookies.ts @@ -107,6 +107,7 @@ const COOKIE_REQUIREMENTS: Record< name: "preAuthCookieName", required: true, }, + "/logout/all": { name: "accessCookieName", required: true }, "/logout": { name: "accessCookieName", required: true }, "/users/me": { name: "accessCookieName", required: true }, "/organizations": { name: "accessCookieName", required: true }, diff --git a/packages/core/src/handlers/logout.ts b/packages/core/src/handlers/logout.ts index 55c2764..b7cb4ad 100644 --- a/packages/core/src/handlers/logout.ts +++ b/packages/core/src/handlers/logout.ts @@ -5,7 +5,9 @@ export interface LogoutOptions { accessCookieName: string; registrationCookieName: string; refreshCookieName: string; + authorization?: string; forwardedClientIp?: string; + scope?: LogoutScope; } export interface LogoutResult { @@ -13,16 +15,22 @@ export interface LogoutResult { clearCookies: string[]; } -export async function logoutHandler( - opts: LogoutOptions, -): Promise { - await authFetch(`${opts.authServerUrl}/logout`, { - method: "GET", +export type LogoutScope = "current_session" | "all_sessions"; + +function getLogoutPath(scope: LogoutScope) { + return scope === "all_sessions" ? "/logout/all" : "/logout"; +} + +export async function logoutHandler(opts: LogoutOptions): Promise { + const scope = opts.scope ?? "all_sessions"; + const upstream = await authFetch(`${opts.authServerUrl}${getLogoutPath(scope)}`, { + method: "DELETE", + authorization: opts.authorization, forwardedClientIp: opts.forwardedClientIp, }); return { - status: 204, + status: upstream.ok ? 204 : upstream.status, clearCookies: [ opts.accessCookieName, opts.registrationCookieName, @@ -30,3 +38,11 @@ export async function logoutHandler( ], }; } + +export function logoutCurrentSessionHandler(opts: Omit) { + return logoutHandler({ ...opts, scope: "current_session" }); +} + +export function logoutAllSessionsHandler(opts: Omit) { + return logoutHandler({ ...opts, scope: "all_sessions" }); +} diff --git a/packages/core/tests/logoutHandler.test.js b/packages/core/tests/logoutHandler.test.js new file mode 100644 index 0000000..8225e03 --- /dev/null +++ b/packages/core/tests/logoutHandler.test.js @@ -0,0 +1,59 @@ +import { jest } from "@jest/globals"; + +const authFetchMock = jest.fn(); + +jest.unstable_mockModule("../dist/authFetch.js", () => ({ + authFetch: authFetchMock, +})); + +const baseOptions = { + authServerUrl: "https://auth.example.com", + accessCookieName: "access", + registrationCookieName: "registration", + refreshCookieName: "refresh", + authorization: "Bearer service-token", + forwardedClientIp: "203.0.113.44", +}; + +describe("logoutHandler", () => { + beforeEach(() => authFetchMock.mockReset()); + + it("logs out all sessions by default for backward compatibility", async () => { + const { logoutHandler } = await import("../dist/handlers/logout.js"); + + authFetchMock.mockResolvedValue({ ok: true, status: 200 }); + + const result = await logoutHandler(baseOptions); + + expect(authFetchMock).toHaveBeenCalledWith( + "https://auth.example.com/logout/all", + { + method: "DELETE", + authorization: "Bearer service-token", + forwardedClientIp: "203.0.113.44", + }, + ); + expect(result).toEqual({ + status: 204, + clearCookies: ["access", "registration", "refresh"], + }); + }); + + it("can log out only the current session", async () => { + const { logoutCurrentSessionHandler } = await import( + "../dist/handlers/logout.js" + ); + + authFetchMock.mockResolvedValue({ ok: true, status: 200 }); + + await logoutCurrentSessionHandler(baseOptions); + + expect(authFetchMock).toHaveBeenCalledWith( + "https://auth.example.com/logout", + expect.objectContaining({ + method: "DELETE", + authorization: "Bearer service-token", + }), + ); + }); +}); diff --git a/packages/express/README.md b/packages/express/README.md index b92b3fa..1ecbe87 100644 --- a/packages/express/README.md +++ b/packages/express/README.md @@ -142,7 +142,9 @@ Routes include: - `/auth/step-up/*` - `/auth/registration/*` - `/auth/users/me` -- `/auth/logout` +- `DELETE /auth/logout` for the current session +- `DELETE /auth/logout/all` for every session owned by the current user +- `GET /auth/logout` as a deprecated all-session compatibility route **Options** diff --git a/packages/express/src/createServer.ts b/packages/express/src/createServer.ts index 0fb4c9e..62deeae 100644 --- a/packages/express/src/createServer.ts +++ b/packages/express/src/createServer.ts @@ -317,7 +317,15 @@ export function createSeamlessAuthServer( ); r.get("/users/me", (req, res) => me(req, res, resolvedOpts)); - r.get("/logout", (req, res) => logout(req, res, resolvedOpts)); + r.get("/logout", (req, res) => + logout(req, res, resolvedOpts, "all_sessions"), + ); + r.delete("/logout", (req, res) => + logout(req, res, resolvedOpts, "current_session"), + ); + r.delete("/logout/all", (req, res) => + logout(req, res, resolvedOpts, "all_sessions"), + ); r.get("/organizations", proxyWithIdentity("organizations", "access", "GET")); r.post("/organizations", proxyWithIdentity("organizations", "access")); diff --git a/packages/express/src/handlers/logout.ts b/packages/express/src/handlers/logout.ts index 93b665a..5558d98 100644 --- a/packages/express/src/handlers/logout.ts +++ b/packages/express/src/handlers/logout.ts @@ -1,21 +1,26 @@ import { Request, Response } from "express"; import { logoutHandler } from "@seamless-auth/core/handlers/logout"; +import type { LogoutScope } from "@seamless-auth/core/handlers/logout"; import { clearAllCookies } from "../internal/cookie"; import { buildForwardedClientIp } from "../internal/buildForwardedClientIp"; import { SeamlessAuthServerOptions } from "../createServer"; +import { buildServiceAuthorization } from "../internal/buildAuthorization"; export async function logout( req: Request, res: Response, opts: SeamlessAuthServerOptions, + scope: LogoutScope = "current_session", ) { const result = await logoutHandler({ authServerUrl: opts.authServerUrl, accessCookieName: opts.accessCookieName!, registrationCookieName: opts.registrationCookieName!, refreshCookieName: opts.refreshCookieName!, + authorization: buildServiceAuthorization(req, opts), forwardedClientIp: buildForwardedClientIp(req), - } as any); + scope, + }); clearAllCookies(res, opts.cookieDomain || "", ...result.clearCookies); diff --git a/packages/express/src/internal/buildAuthorization.ts b/packages/express/src/internal/buildAuthorization.ts index bf44866..5bf3bdd 100644 --- a/packages/express/src/internal/buildAuthorization.ts +++ b/packages/express/src/internal/buildAuthorization.ts @@ -7,6 +7,7 @@ export function buildServiceAuthorization( opts: SeamlessAuthServerOptions, ) { const subject = req.cookiePayload?.sub || req.user?.sub; + const sessionId = req.cookiePayload?.sessionId || req.user?.sessionId; if (!subject) { return undefined; @@ -18,6 +19,7 @@ export function buildServiceAuthorization( audience: opts.audience, serviceSecret: opts.serviceSecret, keyId: opts.jwksKid || "dev-main", + ...(sessionId === undefined ? {} : { sessionId }), }); return `Bearer ${token}`; diff --git a/packages/express/tests/logoutRoutes.test.js b/packages/express/tests/logoutRoutes.test.js new file mode 100644 index 0000000..a5d695a --- /dev/null +++ b/packages/express/tests/logoutRoutes.test.js @@ -0,0 +1,108 @@ +import { jest } from "@jest/globals"; +import express from "express"; +import jwt from "jsonwebtoken"; +import request from "supertest"; + +const { default: createSeamlessAuthServer } = await import("../dist/index.js"); + +function createResponse(status = 200) { + return { + ok: status >= 200 && status < 300, + status, + }; +} + +function createAccessCookie(subject = "user-123") { + const token = jwt.sign( + { sub: subject, roles: ["user"], sessionId: "session-123" }, + "cookie-secret", + { + algorithm: "HS256", + expiresIn: "300s", + }, + ); + + return `seamless-access=${token}`; +} + +function createApp() { + const app = express(); + + app.use( + "/auth", + createSeamlessAuthServer({ + authServerUrl: "https://auth.example.com", + cookieSecret: "cookie-secret", + serviceSecret: "service-secret", + issuer: "https://api.example.com", + audience: "https://auth.example.com", + jwksKid: "dev-main", + }), + ); + + return app; +} + +describe("logout routes", () => { + const originalFetch = global.fetch; + + beforeEach(() => { + global.fetch = jest.fn().mockResolvedValue(createResponse()); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("logs out the current session with DELETE /logout", async () => { + const res = await request(createApp()) + .delete("/auth/logout") + .set("Cookie", createAccessCookie()); + + expect(res.status).toBe(204); + expect(global.fetch).toHaveBeenCalledWith( + "https://auth.example.com/logout", + expect.objectContaining({ + method: "DELETE", + headers: expect.objectContaining({ + Authorization: expect.stringMatching(/^Bearer /), + "x-seamless-service-token": expect.stringMatching(/^Bearer /), + }), + }), + ); + + const authorization = + global.fetch.mock.calls[0][1].headers.Authorization.replace("Bearer ", ""); + const decoded = jwt.decode(authorization); + + expect(decoded.sid).toBe("session-123"); + }); + + it("keeps GET /logout as an all-session compatibility route", async () => { + const res = await request(createApp()) + .get("/auth/logout") + .set("Cookie", createAccessCookie()); + + expect(res.status).toBe(204); + expect(global.fetch).toHaveBeenCalledWith( + "https://auth.example.com/logout/all", + expect.objectContaining({ + method: "DELETE", + }), + ); + }); + + it("logs out all sessions with DELETE /logout/all", async () => { + const res = await request(createApp()) + .delete("/auth/logout/all") + .set("Cookie", createAccessCookie()); + + expect(res.status).toBe(204); + expect(global.fetch).toHaveBeenCalledWith( + "https://auth.example.com/logout/all", + expect.objectContaining({ + method: "DELETE", + }), + ); + }); +}); From 049f3f7c0ec6ec8c94e182b640e3206c73b3f8c0 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 30 May 2026 08:41:08 -0400 Subject: [PATCH 2/4] chore: clean up work --- packages/core/src/ensureCookies.ts | 4 ++ packages/core/src/handlers/finishLogin.ts | 1 + packages/core/src/handlers/finishRegister.ts | 1 + packages/core/src/handlers/oauthHandlers.ts | 1 + .../pollMagicLinkConfirmationHandler.ts | 1 + packages/core/src/handlers/register.ts | 2 +- .../src/handlers/switchOrganizationHandler.ts | 1 + .../src/handlers/verifyLoginOtpHandler.ts | 1 + packages/core/tests/ensureCookes.test.js | 6 +++ packages/core/tests/oauthHandlers.test.js | 1 + packages/core/tests/registerHandler.test.js | 54 +++++++++++++++++++ packages/express/package.json | 4 +- .../src/internal/buildAuthorization.ts | 20 ++----- packages/express/tests/loginOtpRoutes.test.js | 44 ++++++++++++--- packages/express/tests/logoutRoutes.test.js | 17 +++--- .../express/tests/messagingDelivery.test.js | 14 +++-- .../express/tests/organizationRoutes.test.js | 11 ++-- packages/express/tests/stepUpProxy.test.js | 32 ++++++----- 18 files changed, 160 insertions(+), 55 deletions(-) create mode 100644 packages/core/tests/registerHandler.test.js diff --git a/packages/core/src/ensureCookies.ts b/packages/core/src/ensureCookies.ts index 8d6b847..a68f540 100644 --- a/packages/core/src/ensureCookies.ts +++ b/packages/core/src/ensureCookies.ts @@ -32,6 +32,7 @@ export interface EnsureCookiesResult { user?: { sub: string; sessionId?: string; + token?: string; roles?: string[]; }; setCookies?: CookieInstruction[]; @@ -224,6 +225,7 @@ export async function ensureCookies( ...(refreshed.sessionId === undefined ? {} : { sessionId: refreshed.sessionId }), + token: refreshed.token, roles: refreshed.roles, }, setCookies: [ @@ -234,6 +236,7 @@ export async function ensureCookies( ...(refreshed.sessionId === undefined ? {} : { sessionId: refreshed.sessionId }), + token: refreshed.token, roles: refreshed.roles, email: refreshed.email, phone: refreshed.phone, @@ -272,6 +275,7 @@ export async function ensureCookies( ...(typeof payload.sessionId === "string" ? { sessionId: payload.sessionId } : {}), + ...(typeof payload.token === "string" ? { token: payload.token } : {}), roles: payload.roles as string[] | undefined, }, }; diff --git a/packages/core/src/handlers/finishLogin.ts b/packages/core/src/handlers/finishLogin.ts index 168f510..6e77a92 100644 --- a/packages/core/src/handlers/finishLogin.ts +++ b/packages/core/src/handlers/finishLogin.ts @@ -74,6 +74,7 @@ export async function finishLoginHandler( value: { sub: data.sub, ...(sessionId === undefined ? {} : { sessionId }), + token: data.token, roles: data.roles, email: data.email, phone: data.phone, diff --git a/packages/core/src/handlers/finishRegister.ts b/packages/core/src/handlers/finishRegister.ts index 6ce9a35..721382f 100644 --- a/packages/core/src/handlers/finishRegister.ts +++ b/packages/core/src/handlers/finishRegister.ts @@ -72,6 +72,7 @@ export async function finishRegisterHandler( value: { sub: data.sub, ...(sessionId === undefined ? {} : { sessionId }), + token: data.token, roles: data.roles, email: data.email, phone: data.phone, diff --git a/packages/core/src/handlers/oauthHandlers.ts b/packages/core/src/handlers/oauthHandlers.ts index bac3164..203c8ca 100644 --- a/packages/core/src/handlers/oauthHandlers.ts +++ b/packages/core/src/handlers/oauthHandlers.ts @@ -112,6 +112,7 @@ export async function finishOAuthLoginHandler( value: { sub: data.sub, ...(sessionId === undefined ? {} : { sessionId }), + token: data.token, roles: data.roles, email: data.email, phone: data.phone, diff --git a/packages/core/src/handlers/pollMagicLinkConfirmationHandler.ts b/packages/core/src/handlers/pollMagicLinkConfirmationHandler.ts index 2497b2f..4e1d765 100644 --- a/packages/core/src/handlers/pollMagicLinkConfirmationHandler.ts +++ b/packages/core/src/handlers/pollMagicLinkConfirmationHandler.ts @@ -89,6 +89,7 @@ export async function pollMagicLinkConfirmationHandler( value: { sub: data.sub, ...(sessionId === undefined ? {} : { sessionId }), + token: data.token, roles: data.roles, email: data.email, phone: data.phone, diff --git a/packages/core/src/handlers/register.ts b/packages/core/src/handlers/register.ts index f0007a0..dc780e8 100644 --- a/packages/core/src/handlers/register.ts +++ b/packages/core/src/handlers/register.ts @@ -59,7 +59,7 @@ export async function registerHandler( setCookies: [ { name: opts.registrationCookieName, - value: { sub: data.sub }, + value: { sub: data.sub, token: data.token }, ttl: data.ttl, domain: opts.cookieDomain, }, diff --git a/packages/core/src/handlers/switchOrganizationHandler.ts b/packages/core/src/handlers/switchOrganizationHandler.ts index 27e7fdd..ba38d44 100644 --- a/packages/core/src/handlers/switchOrganizationHandler.ts +++ b/packages/core/src/handlers/switchOrganizationHandler.ts @@ -82,6 +82,7 @@ export async function switchOrganizationHandler( value: { sub: data.sub, ...(sessionId === undefined ? {} : { sessionId }), + token: data.token, roles: data.roles, email: data.email, phone: data.phone, diff --git a/packages/core/src/handlers/verifyLoginOtpHandler.ts b/packages/core/src/handlers/verifyLoginOtpHandler.ts index ef69426..1dc257c 100644 --- a/packages/core/src/handlers/verifyLoginOtpHandler.ts +++ b/packages/core/src/handlers/verifyLoginOtpHandler.ts @@ -80,6 +80,7 @@ export async function verifyLoginOtpHandler( value: { sub: data.sub, ...(sessionId === undefined ? {} : { sessionId }), + token: data.token, roles: data.roles, email: data.email, phone: data.phone, diff --git a/packages/core/tests/ensureCookes.test.js b/packages/core/tests/ensureCookes.test.js index b079fe9..bd338f8 100644 --- a/packages/core/tests/ensureCookes.test.js +++ b/packages/core/tests/ensureCookes.test.js @@ -107,6 +107,7 @@ describe("ensureCookies", () => { expect(result.type).toBe("ok"); expect(result.user?.sub).toBe("user-123"); expect(result.user?.sessionId).toBe("session-123"); + expect(result.user?.token).toBe("new-access"); expect(result.setCookies).toHaveLength(2); @@ -115,6 +116,7 @@ describe("ensureCookies", () => { expect(accessCookie.value).toEqual({ sub: "user-123", sessionId: "session-123", + token: "new-access", roles: ["user"], email: "test@example.com", phone: "+14155552671", @@ -209,6 +211,7 @@ describe("ensureCookies", () => { verifyCookieJwtMock.mockReturnValue({ sub: "user-123", + token: "access-token", sessionId: "session-123", roles: ["user"], }); @@ -225,6 +228,7 @@ describe("ensureCookies", () => { expect(result.user).toEqual({ sub: "user-123", sessionId: "session-123", + token: "access-token", roles: ["user"], }); }); @@ -234,6 +238,7 @@ describe("ensureCookies", () => { verifyCookieJwtMock.mockReturnValue({ sub: "user-123", + token: "access-token", sessionId: "session-123", roles: ["user"], }); @@ -250,6 +255,7 @@ describe("ensureCookies", () => { expect(result.user).toEqual({ sub: "user-123", sessionId: "session-123", + token: "access-token", roles: ["user"], }); }); diff --git a/packages/core/tests/oauthHandlers.test.js b/packages/core/tests/oauthHandlers.test.js index eba503f..0466d6d 100644 --- a/packages/core/tests/oauthHandlers.test.js +++ b/packages/core/tests/oauthHandlers.test.js @@ -104,6 +104,7 @@ describe("oauthHandlers", () => { value: expect.objectContaining({ sub: "user-123", sessionId: "session-123", + token: "access-token", organizationId: "org-123", }), }), diff --git a/packages/core/tests/registerHandler.test.js b/packages/core/tests/registerHandler.test.js new file mode 100644 index 0000000..c1402b7 --- /dev/null +++ b/packages/core/tests/registerHandler.test.js @@ -0,0 +1,54 @@ +import { jest } from "@jest/globals"; + +function createJsonResponse(status, body) { + return { + ok: status >= 200 && status < 300, + status, + json: async () => body, + }; +} + +describe("registerHandler", () => { + const originalFetch = global.fetch; + + beforeEach(() => { + global.fetch = jest.fn(); + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("stores the upstream ephemeral token in the registration cookie", async () => { + const { registerHandler } = await import("../dist/handlers/register.js"); + + global.fetch.mockResolvedValue( + createJsonResponse(200, { + message: "Success", + sub: "user-123", + token: "ephemeral-token", + ttl: 300, + }), + ); + + const result = await registerHandler( + { + body: { email: "user@example.com", phone: "+14155552671" }, + }, + { + authServerUrl: "https://auth.example.com", + registrationCookieName: "registration", + }, + ); + + expect(result.status).toBe(200); + expect(result.setCookies).toEqual([ + { + name: "registration", + value: { sub: "user-123", token: "ephemeral-token" }, + ttl: 300, + domain: undefined, + }, + ]); + }); +}); diff --git a/packages/express/package.json b/packages/express/package.json index 3670d27..227f38a 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -41,7 +41,7 @@ "express": ">=4.18.0" }, "dependencies": { - "@seamless-auth/core": "workspace:^", + "@seamless-auth/core": "file:../core", "cookie-parser": "^1.4.6", "jsonwebtoken": "^9.0.3" }, @@ -60,4 +60,4 @@ "publishConfig": { "access": "public" } -} +} \ No newline at end of file diff --git a/packages/express/src/internal/buildAuthorization.ts b/packages/express/src/internal/buildAuthorization.ts index 5bf3bdd..ef019ba 100644 --- a/packages/express/src/internal/buildAuthorization.ts +++ b/packages/express/src/internal/buildAuthorization.ts @@ -4,25 +4,11 @@ import { SeamlessAuthServerOptions } from "../createServer"; export function buildServiceAuthorization( req: Request & { cookiePayload?: any }, - opts: SeamlessAuthServerOptions, + _opts?: SeamlessAuthServerOptions, ) { - const subject = req.cookiePayload?.sub || req.user?.sub; - const sessionId = req.cookiePayload?.sessionId || req.user?.sessionId; + const token = req.cookiePayload?.token || req.user?.token; - if (!subject) { - return undefined; - } - - const token = createServiceToken({ - subject, - issuer: opts.issuer, - audience: opts.audience, - serviceSecret: opts.serviceSecret, - keyId: opts.jwksKid || "dev-main", - ...(sessionId === undefined ? {} : { sessionId }), - }); - - return `Bearer ${token}`; + return typeof token === "string" ? `Bearer ${token}` : undefined; } export function buildInternalServiceAuthorization(opts: SeamlessAuthServerOptions) { diff --git a/packages/express/tests/loginOtpRoutes.test.js b/packages/express/tests/loginOtpRoutes.test.js index 7b34277..814068b 100644 --- a/packages/express/tests/loginOtpRoutes.test.js +++ b/packages/express/tests/loginOtpRoutes.test.js @@ -14,10 +14,14 @@ function createJsonResponse(status, body) { } function createPreAuthCookie(subject = "user-123") { - const token = jwt.sign({ sub: subject }, "cookie-secret", { - algorithm: "HS256", - expiresIn: "300s", - }); + const token = jwt.sign( + { sub: subject, token: "ephemeral-token" }, + "cookie-secret", + { + algorithm: "HS256", + expiresIn: "300s", + }, + ); return `seamless-ephemeral=${token}`; } @@ -68,8 +72,36 @@ describe("login OTP routes", () => { expect.objectContaining({ method: "GET", headers: expect.objectContaining({ - Authorization: expect.stringMatching(/^Bearer /), - "x-seamless-service-token": expect.stringMatching(/^Bearer /), + Authorization: "Bearer ephemeral-token", + "x-seamless-service-token": "Bearer ephemeral-token", + }), + }), + ); + }); + + it("proxies registration phone OTP verification with the stored ephemeral token", async () => { + global.fetch.mockResolvedValue( + createJsonResponse(200, { + message: "Phone verified successfully.", + }), + ); + + const body = { verificationToken: "123456" }; + + const res = await request(createApp()) + .post("/auth/otp/verify-phone-otp") + .set("Cookie", createPreAuthCookie()) + .send(body); + + expect(res.status).toBe(200); + expect(global.fetch).toHaveBeenCalledWith( + "https://auth.example.com/otp/verify-phone-otp", + expect.objectContaining({ + method: "POST", + body: JSON.stringify(body), + headers: expect.objectContaining({ + Authorization: "Bearer ephemeral-token", + "x-seamless-service-token": "Bearer ephemeral-token", }), }), ); diff --git a/packages/express/tests/logoutRoutes.test.js b/packages/express/tests/logoutRoutes.test.js index a5d695a..a3c1a31 100644 --- a/packages/express/tests/logoutRoutes.test.js +++ b/packages/express/tests/logoutRoutes.test.js @@ -14,7 +14,12 @@ function createResponse(status = 200) { function createAccessCookie(subject = "user-123") { const token = jwt.sign( - { sub: subject, roles: ["user"], sessionId: "session-123" }, + { + sub: subject, + roles: ["user"], + sessionId: "session-123", + token: "access-token", + }, "cookie-secret", { algorithm: "HS256", @@ -65,17 +70,11 @@ describe("logout routes", () => { expect.objectContaining({ method: "DELETE", headers: expect.objectContaining({ - Authorization: expect.stringMatching(/^Bearer /), - "x-seamless-service-token": expect.stringMatching(/^Bearer /), + Authorization: "Bearer access-token", + "x-seamless-service-token": "Bearer access-token", }), }), ); - - const authorization = - global.fetch.mock.calls[0][1].headers.Authorization.replace("Bearer ", ""); - const decoded = jwt.decode(authorization); - - expect(decoded.sid).toBe("session-123"); }); it("keeps GET /logout as an all-session compatibility route", async () => { diff --git a/packages/express/tests/messagingDelivery.test.js b/packages/express/tests/messagingDelivery.test.js index 6f54a40..328be6c 100644 --- a/packages/express/tests/messagingDelivery.test.js +++ b/packages/express/tests/messagingDelivery.test.js @@ -14,10 +14,14 @@ function createJsonResponse(status, body) { } function createPreAuthCookie(subject = "user-123") { - const token = jwt.sign({ sub: subject }, "cookie-secret", { - algorithm: "HS256", - expiresIn: "300s", - }); + const token = jwt.sign( + { sub: subject, token: "ephemeral-token" }, + "cookie-secret", + { + algorithm: "HS256", + expiresIn: "300s", + }, + ); return `seamless-ephemeral=${token}`; } @@ -96,7 +100,7 @@ describe("messaging delivery routes", () => { headers: expect.objectContaining({ "Content-Type": "application/json", "x-seamless-auth-delivery-mode": "external", - Authorization: expect.stringMatching(/^Bearer /), + Authorization: "Bearer ephemeral-token", "x-seamless-service-token": expect.stringMatching(/^Bearer /), "x-seamless-client-ip": expect.any(String), }), diff --git a/packages/express/tests/organizationRoutes.test.js b/packages/express/tests/organizationRoutes.test.js index c376605..204cf48 100644 --- a/packages/express/tests/organizationRoutes.test.js +++ b/packages/express/tests/organizationRoutes.test.js @@ -15,7 +15,12 @@ function createJsonResponse(status, body) { function createAccessCookie(subject = "user-123") { const token = jwt.sign( - { sub: subject, roles: ["admin"], sessionId: "session-123" }, + { + sub: subject, + roles: ["admin"], + sessionId: "session-123", + token: "access-token", + }, "cookie-secret", { algorithm: "HS256", @@ -77,8 +82,8 @@ describe("organization proxy routes", () => { expect.objectContaining({ method: "GET", headers: expect.objectContaining({ - Authorization: expect.stringMatching(/^Bearer /), - "x-seamless-service-token": expect.stringMatching(/^Bearer /), + Authorization: "Bearer access-token", + "x-seamless-service-token": "Bearer access-token", }), }), ); diff --git a/packages/express/tests/stepUpProxy.test.js b/packages/express/tests/stepUpProxy.test.js index c2e05e7..27488fd 100644 --- a/packages/express/tests/stepUpProxy.test.js +++ b/packages/express/tests/stepUpProxy.test.js @@ -14,19 +14,27 @@ function createJsonResponse(status, body) { } function createAccessCookie(subject = "user-123") { - const token = jwt.sign({ sub: subject, roles: ["user"] }, "cookie-secret", { - algorithm: "HS256", - expiresIn: "300s", - }); + const token = jwt.sign( + { sub: subject, roles: ["user"], token: "access-token" }, + "cookie-secret", + { + algorithm: "HS256", + expiresIn: "300s", + }, + ); return `seamless-access=${token}`; } function createRegistrationCookie(subject = "user-123") { - const token = jwt.sign({ sub: subject, roles: ["user"] }, "cookie-secret", { - algorithm: "HS256", - expiresIn: "300s", - }); + const token = jwt.sign( + { sub: subject, roles: ["user"], token: "ephemeral-token" }, + "cookie-secret", + { + algorithm: "HS256", + expiresIn: "300s", + }, + ); return `seamless-ephemeral=${token}`; } @@ -89,8 +97,8 @@ describe("step-up proxy routes", () => { expect.objectContaining({ method: "GET", headers: expect.objectContaining({ - Authorization: expect.stringMatching(/^Bearer /), - "x-seamless-service-token": expect.stringMatching(/^Bearer /), + Authorization: "Bearer access-token", + "x-seamless-service-token": "Bearer access-token", }), }), ); @@ -131,8 +139,8 @@ describe("step-up proxy routes", () => { method: "POST", body: JSON.stringify(body), headers: expect.objectContaining({ - Authorization: expect.stringMatching(/^Bearer /), - "x-seamless-service-token": expect.stringMatching(/^Bearer /), + Authorization: "Bearer access-token", + "x-seamless-service-token": "Bearer access-token", }), }), ); From f6ca34285621538e7891e72c5fe1b165e83f92fa Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 30 May 2026 08:44:33 -0400 Subject: [PATCH 3/4] chore: changeset --- .changeset/soft-chefs-retire.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/soft-chefs-retire.md diff --git a/.changeset/soft-chefs-retire.md b/.changeset/soft-chefs-retire.md new file mode 100644 index 0000000..9083d56 --- /dev/null +++ b/.changeset/soft-chefs-retire.md @@ -0,0 +1,6 @@ +--- +"@seamless-auth/express": patch +"@seamless-auth/core": patch +--- + +Operational tidy work and extension of the logout functions for future use From 53d83be1713f89ab7bed074aa34efa7b5dfd1d24 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 30 May 2026 08:46:37 -0400 Subject: [PATCH 4/4] fix: undo local core install --- packages/express/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/express/package.json b/packages/express/package.json index 227f38a..8912130 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -41,7 +41,7 @@ "express": ">=4.18.0" }, "dependencies": { - "@seamless-auth/core": "file:../core", + "@seamless-auth/core": "workspace:^", "cookie-parser": "^1.4.6", "jsonwebtoken": "^9.0.3" },