import type { Did } from "@atcute/lexicons/syntax"; import { OAuthResponseError, OAuthUserAgent, TokenRefreshError, createAuthorizationUrl, deleteStoredSession, finalizeAuthorization, getSession, type Session } from "@atcute/oauth-browser-client"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createDelegatedSession, delegatedSessionController, getOrCreateDelegatedSession } from "$lib/api/actAs"; vi.mock("@atcute/oauth-browser-client", async (importOriginal) => { const mod = await importOriginal>(); class MockOAuthUserAgent { readonly sub: string; constructor(readonly session: Session) { this.sub = session.info.sub; } } return { ...mod, OAuthUserAgent: MockOAuthUserAgent, createAuthorizationUrl: vi.fn(), finalizeAuthorization: vi.fn(), getSession: vi.fn(), deleteStoredSession: vi.fn() }; }); const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); const controllerDid = "did:plc:controller" as Did; const delegatedDid = "did:plc:org" as Did; const requestUri = "urn:ietf:params:oauth:request_uri:request-1"; const sessionWithClaims = (claims: unknown): Session => { const payload = btoa(JSON.stringify(claims)) .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); return { token: { access: `header.${payload}.signature` } } as Session; }; // a session on pds.example for the org, minted by `controller` const orgSession = (controller: string): Session => ({ info: { sub: delegatedDid, aud: "https://pds.example" }, token: { access: sessionWithClaims({ act: { sub: controller } }).token.access, scope: "atproto" }, dpopKey: {} }) as Session; const controllerAgentOn = (pds: string): OAuthUserAgent => ({ sub: controllerDid, session: { info: { aud: pds }, token: { scope: "atproto repo:sh.tangled.repo" } }, handle: vi.fn(async (_path: string, _init: RequestInit) => Response.json({ token: "service-token" }, { status: 200 }) ) }) as unknown as OAuthUserAgent; // the headless flow on pds.example, answered the way tranquil would const answerHeadlessFlow = ( minted: Session, consent: unknown = { scopes: [{ scope: "atproto" }] } ) => { vi.mocked(createAuthorizationUrl).mockImplementation(async () => { const url = new URL("https://pds.example/oauth/authorize"); url.searchParams.set("request_uri", requestUri); return url; }); fetchMock .mockResolvedValueOnce(Response.json({ success: true })) .mockResolvedValueOnce(Response.json(consent)) .mockResolvedValueOnce( Response.json({ redirect_uri: "https://pds.example/oauth/authorize/redirect?code=code-1&state=state-1" }) ); vi.mocked(finalizeAuthorization).mockResolvedValue({ session: minted, state: null }); }; beforeEach(() => { vi.clearAllMocks(); fetchMock.mockReset(); }); describe("delegatedSessionController", () => { it("returns the controller DID from the access token act claim", () => { expect(delegatedSessionController(sessionWithClaims({ act: { sub: controllerDid } }))).toBe( controllerDid ); }); it("rejects sessions without a valid controller claim", () => { expect(delegatedSessionController(sessionWithClaims({ sub: "did:plc:org" }))).toBeNull(); expect(delegatedSessionController({ token: { access: "opaque" } } as Session)).toBeNull(); }); }); describe("createDelegatedSession", () => { it.each(["https://pds.example", "https://remote.example"])( "authorizes headlessly for a controller on %s", async (controllerPds) => { const controllerAgent = controllerAgentOn(controllerPds); const session = orgSession(controllerDid); answerHeadlessFlow(session, { scopes: [ { scope: "atproto" }, { scope: "repo:allowed" }, { scope: "repo:restricted", restricted: true } ], permission_sets: [{ include_scope: "include:example.permissions" }] }); const agent = await createDelegatedSession(controllerAgent, delegatedDid); expect(agent).toBeInstanceOf(OAuthUserAgent); expect(agent.session).toBe(session); const handle = vi.mocked(controllerAgent.handle); expect(handle).toHaveBeenCalledTimes(1); const authParams = new URL(String(handle.mock.calls[0][0]), controllerPds).searchParams; expect(authParams.get("aud")).toBe("did:web:pds.example"); expect(authParams.get("lxm")).toBe("farm.tranquil.delegation.authorize"); expect(String(fetchMock.mock.calls[0][0])).toBe( "https://pds.example/oauth/delegation/auth-token" ); expect(fetchMock.mock.calls[0][1]).toMatchObject({ headers: { authorization: "Bearer service-token" }, body: JSON.stringify({ request_uri: requestUri, delegated_did: delegatedDid }) }); expect(String(fetchMock.mock.calls[1][0])).toContain( "https://pds.example/oauth/authorize/consent?" ); expect(fetchMock).toHaveBeenCalledTimes(3); const approval = fetchMock.mock.calls[2][1] as RequestInit; expect(JSON.parse(String(approval.body))).toEqual({ request_uri: requestUri, approved_scopes: ["atproto", "repo:allowed", "include:example.permissions"], remember: false }); expect(finalizeAuthorization).toHaveBeenCalledTimes(1); const params = vi.mocked(finalizeAuthorization).mock.calls[0][0]; expect(Object.fromEntries(params)).toEqual({ iss: "https://pds.example", code: "code-1", state: "state-1" }); } ); }); describe("getOrCreateDelegatedSession", () => { const controllerAgent = controllerAgentOn("https://pds.example"); it("keeps the personal session when the owner is the controller", async () => { const agent = await getOrCreateDelegatedSession(controllerAgent, controllerDid); expect(agent).toBe(controllerAgent); expect(getSession).not.toHaveBeenCalled(); expect(deleteStoredSession).not.toHaveBeenCalled(); expect(createAuthorizationUrl).not.toHaveBeenCalled(); }); it("reuses a stored session minted by this controller", async () => { const stored = orgSession(controllerDid); vi.mocked(getSession).mockResolvedValue(stored); const agent = await getOrCreateDelegatedSession(controllerAgent, delegatedDid); expect(agent.session).toBe(stored); expect(createAuthorizationUrl).not.toHaveBeenCalled(); expect(deleteStoredSession).not.toHaveBeenCalled(); }); it.each([ ["a revoked refresh token", new TokenRefreshError(delegatedDid, "session was revoked")], [ "an invalid_token answer from the token endpoint", new OAuthResponseError(new Response(null, { status: 400 }), { error: "invalid_token", error_description: '"exp" claim timestamp check failed' }) ] ])("remints after %s", async (_label, cause) => { const minted = orgSession(controllerDid); vi.mocked(getSession).mockRejectedValue(cause); answerHeadlessFlow(minted); const agent = await getOrCreateDelegatedSession(controllerAgent, delegatedDid); expect(finalizeAuthorization).toHaveBeenCalledTimes(1); expect(agent.session).toBe(minted); }); it("remints when the stored session was minted by another controller", async () => { vi.mocked(getSession).mockResolvedValue(orgSession("did:plc:other")); answerHeadlessFlow(orgSession(controllerDid)); await getOrCreateDelegatedSession(controllerAgent, delegatedDid); expect(deleteStoredSession).toHaveBeenCalledWith(delegatedDid); expect(finalizeAuthorization).toHaveBeenCalledTimes(1); }); it("keeps a session that only failed transiently", async () => { const cause = new TypeError("fetch failed"); vi.mocked(getSession).mockRejectedValue(cause); await expect(getOrCreateDelegatedSession(controllerAgent, delegatedDid)).rejects.toBe( cause ); expect(finalizeAuthorization).not.toHaveBeenCalled(); }); });