diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..9e4948a --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,13 @@ +{ + "mode": "pre", + "tag": "beta", + "initialVersions": { + "@labdigital/federated-token-apollo": "2.1.0", + "@labdigital/federated-token": "2.1.0", + "@labdigital/federated-token-express-adapter": "2.1.0", + "@labdigital/federated-token-fastify-adapter": "2.1.0", + "@labdigital/federated-token-react": "2.1.0", + "@labdigital/federated-token-yoga": "2.1.0" + }, + "changesets": [] +} diff --git a/.changeset/slick-rabbits-count.md b/.changeset/slick-rabbits-count.md new file mode 100644 index 0000000..0b02320 --- /dev/null +++ b/.changeset/slick-rabbits-count.md @@ -0,0 +1,7 @@ +--- +"@labdigital/federated-token-express-adapter": minor +"@labdigital/federated-token-fastify-adapter": minor +"@labdigital/federated-token": minor +--- + +Add support for cookie path and refresh token path function diff --git a/packages/core/src/tokensource/cookies-base.test.ts b/packages/core/src/tokensource/cookies-base.test.ts index 1c7d560..fcecafb 100644 --- a/packages/core/src/tokensource/cookies-base.test.ts +++ b/packages/core/src/tokensource/cookies-base.test.ts @@ -13,7 +13,7 @@ import { * support multiple Set-Cookie headers, so we just 'join' them with a comma. */ class TestAdapter implements CookieAdapter { - constructor(private options: BaseCookieSourceOptions) {} + constructor(private options: BaseCookieSourceOptions) {} getCookie(request: Request, name: string): string | undefined { const header = request.headers.get("cookie"); @@ -55,7 +55,7 @@ class TestAdapter implements CookieAdapter { class TestCookieTokenSource extends BaseCookieTokenSource { protected adapter: CookieAdapter; - constructor(options: BaseCookieSourceOptions) { + constructor(options: BaseCookieSourceOptions) { super(options); this.adapter = new TestAdapter(options); } @@ -282,7 +282,10 @@ describe("CookieTokenSource", () => { cookieTokenSource.deleteAccessToken(request, response); const cookies = getCookies(response); - expect(cookies).toEqual([{ userToken: "" }, { guestToken: "" }]); + expect(cookies).toEqual([ + { userToken: "", Path: "/" }, + { guestToken: "", Path: "/" }, + ]); }); // Test for deleting refresh tokens @@ -305,8 +308,8 @@ describe("CookieTokenSource", () => { const cookies = getCookies(response); expect(cookies).toEqual([ { refreshToken: "", Path: "/refresh" }, - { guestRefreshTokenExists: "" }, - { userRefreshTokenExists: "" }, + { guestRefreshTokenExists: "", Path: "/" }, + { userRefreshTokenExists: "", Path: "/" }, ]); }); @@ -330,4 +333,31 @@ describe("CookieTokenSource", () => { const cookies = getCookies(response); expect(cookies).toEqual([{ refreshToken: "", Path: "/refresh" }]); }); + + it("should get the refresh path from the refresh path function", () => { + const request: Request = new Request("http://localhost"); + + const cookieTokenSource = new TestCookieTokenSource({ + secure: true, + sameSite: "strict", + refreshTokenPath: () => "/refresh", + }); + + const result = cookieTokenSource["_getRefreshTokenPath"](request); + expect(result).toBe("/refresh"); + }); + + it("should get the cookiePath from the cookiePath function", () => { + const request: Request = new Request("http://localhost"); + + const cookieTokenSource = new TestCookieTokenSource({ + secure: true, + sameSite: "strict", + refreshTokenPath: "/refresh", + cookiePathFn: () => "/cookie", + }); + + const result = cookieTokenSource["options"].cookiePathFn?.(request); + expect(result).toBe("/cookie"); + }); }); diff --git a/packages/core/src/tokensource/cookies-base.ts b/packages/core/src/tokensource/cookies-base.ts index dd7d028..c600c37 100644 --- a/packages/core/src/tokensource/cookies-base.ts +++ b/packages/core/src/tokensource/cookies-base.ts @@ -63,14 +63,15 @@ type CookieSettings = { expiresIn: number | "session"; }; -export type BaseCookieSourceOptions = { +export type BaseCookieSourceOptions = { secure: boolean; sameSite: "strict" | "lax" | "none" | boolean; - refreshTokenPath: string; + refreshTokenPath: string | ((request: TRequest) => string | undefined); cookieNames?: Partial; guestToken?: CookieSettings; userToken?: CookieSettings; refreshToken?: CookieSettings; + cookiePathFn?: (request: TRequest) => string | undefined; }; export abstract class BaseCookieTokenSource @@ -79,7 +80,7 @@ export abstract class BaseCookieTokenSource protected cookieNames: CookieNames; protected abstract adapter: CookieAdapter; - constructor(protected options: BaseCookieSourceOptions) { + constructor(protected options: BaseCookieSourceOptions) { this.cookieNames = { ...DEFAULT_COOKIE_NAMES, ...(options.cookieNames ?? {}), @@ -96,7 +97,7 @@ export abstract class BaseCookieTokenSource deleteRefreshToken(request: TRequest, response: TResponse): void { this.adapter.clearCookie(request, response, this.cookieNames.refreshToken, { - path: this.options.refreshTokenPath, + path: this._getRefreshTokenPath(request), domain: this.adapter.getPrivateDomain(request), }); @@ -128,6 +129,7 @@ export abstract class BaseCookieTokenSource if (this.adapter.getCookie(request, name)) { this.adapter.clearCookie(request, response, name, { domain: this.adapter.getPublicDomain(request), + path: this.options.cookiePathFn?.(request) ?? "/", }); } } @@ -140,6 +142,7 @@ export abstract class BaseCookieTokenSource if (this.adapter.getCookie(request, name)) { this.adapter.clearCookie(request, response, name, { domain: this.adapter.getPublicDomain(request), + path: this.options.cookiePathFn?.(request) ?? "/", }); } } @@ -180,7 +183,7 @@ export abstract class BaseCookieTokenSource opts.expiresIn === "session" ? undefined : new Date(Date.now() + opts.expiresIn * 1000), - path: "/", + path: this.options.cookiePathFn?.(request) ?? "/", }; if (isAuthenticated) { @@ -244,7 +247,7 @@ export abstract class BaseCookieTokenSource opts.expiresIn === "session" ? undefined : new Date(Date.now() + opts.expiresIn * 1000), - path: "/", + path: this.options.cookiePathFn?.(request) ?? "/", }; if (isAuthenticated) { @@ -299,7 +302,7 @@ export abstract class BaseCookieTokenSource { ...cookieOptions, httpOnly: true, - path: this.options.refreshTokenPath, + path: this._getRefreshTokenPath(request), }, ); @@ -331,4 +334,9 @@ export abstract class BaseCookieTokenSource ); } } + + private _getRefreshTokenPath(req: TRequest): string | undefined { + const path = this.options.refreshTokenPath; + return typeof path === "function" ? path(req) : path; + } } diff --git a/packages/express-adapter/src/cookies.ts b/packages/express-adapter/src/cookies.ts index 0e77239..31dafbf 100644 --- a/packages/express-adapter/src/cookies.ts +++ b/packages/express-adapter/src/cookies.ts @@ -5,9 +5,11 @@ import { } from "@labdigital/federated-token/tokensource"; import type { CookieOptions, Request, Response } from "express"; -type ExpressCookieSourceOptions = BaseCookieSourceOptions & { +type ExpressCookieSourceOptions = BaseCookieSourceOptions & { + refreshTokenPath: string | ((request: Request) => string | undefined); publicDomainFn?: (request: Request) => string | undefined; privateDomainFn?: (request: Request) => string | undefined; + cookiePathFn?: (request: Request) => string | undefined; }; class ExpressCookieAdapter implements CookieAdapter { diff --git a/packages/fastify-adapter/src/cookies.ts b/packages/fastify-adapter/src/cookies.ts index 4f9159b..d16eee7 100644 --- a/packages/fastify-adapter/src/cookies.ts +++ b/packages/fastify-adapter/src/cookies.ts @@ -6,9 +6,11 @@ import { } from "@labdigital/federated-token/tokensource"; import type { FastifyReply, FastifyRequest } from "fastify"; -type FastifyCookieSourceOptions = BaseCookieSourceOptions & { +type FastifyCookieSourceOptions = BaseCookieSourceOptions & { + refreshTokenPath: string | ((request: FastifyRequest) => string | undefined); publicDomainFn?: (request: FastifyRequest) => string | undefined; privateDomainFn?: (request: FastifyRequest) => string | undefined; + cookiePathFn?: (request: FastifyRequest) => string | undefined; }; class FastifyCookieAdapter