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.ts";
import type { Fetch } from "#lib/fetch.ts";
vi.mock("@atcute/oauth-browser-client", async (importOriginal) => {
const mod = await importOriginal<Record<string, unknown>>();
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<Fetch>();
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();
});
});