import { env } from "cloudflare:workers"; import { beforeAll, describe, expect, it } from "vitest"; import { DEFAULT_PASSWORD, DEFAULT_REDIRECT_URI, ISSUER, SELF, authorizeWithPkce, createOAuthClientAsAdmin, exchangeAuthorizationCode, signInForCookie, signUpAdmin, type JsonErrorBody, type OAuthTokenResponse, type TestCredential, } from "./helpers"; // /api/auth/oauth2/token must reject grant_type=client_credentials. // oauthProvider's default grantTypes include client_credentials, so a // confidential client could mint non-user bearer tokens via its secret // without ever signing a human in. tessera sets grantTypes: // ["authorization_code"] to remove that surface; this test pins the // behavior so a future config drift can't reopen it silently. describe("token endpoint grant types", () => { const adminCred = { email: "token-admin@example.com", password: DEFAULT_PASSWORD, name: "Token Admin", } satisfies TestCredential; let clientId: string; let clientSecret: string; beforeAll(async () => { await signUpAdmin(adminCred); const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.70.0.1"); const created = await createOAuthClientAsAdmin(cookie, { name: "token grant test client", redirectUris: [DEFAULT_REDIRECT_URI], }); clientId = created.client_id; clientSecret = created.client_secret; }); it("rejects grant_type=client_credentials with unsupported_grant_type", async () => { const basic = btoa(`${clientId}:${clientSecret}`); const res = await SELF.fetch(`${ISSUER}/api/auth/oauth2/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${basic}`, }, body: new URLSearchParams({ grant_type: "client_credentials", scope: "openid" }).toString(), }); expect(res.status).toBe(400); const body = (await res.json()) as JsonErrorBody; expect(body.error).toBe("unsupported_grant_type"); }); it("rejects grant_type=refresh_token (refresh disabled in tessera config)", async () => { const basic = btoa(`${clientId}:${clientSecret}`); const res = await SELF.fetch(`${ISSUER}/api/auth/oauth2/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${basic}`, }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: "anything" }).toString(), }); expect(res.status).toBe(400); const body = (await res.json()) as JsonErrorBody; expect(body.error).toBe("unsupported_grant_type"); }); // The `resource` parameter triggers oauth-provider's stateless JWT // access token path (createJwtAccessToken), which bypasses the D1 token // rows the ban kill-switch deletes. tessera rejects `resource` at the // token endpoint so every issued token remains revocable through D1 // state, including after Better Auth 1.7's resource-indicator hardening. // These tests gate that invariant against config drift. it("rejects token requests with a string `resource` parameter", async () => { const basic = btoa(`${clientId}:${clientSecret}`); const res = await SELF.fetch(`${ISSUER}/api/auth/oauth2/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${basic}`, }, body: new URLSearchParams({ grant_type: "authorization_code", code: "no-such-code", resource: "https://api.example.com", }).toString(), }); expect(res.status).toBe(400); const body = (await res.json()) as JsonErrorBody; expect(body.error).toBe("invalid_request"); expect(body.code).toBe("RESOURCE_NOT_SUPPORTED"); }); it("rejects token requests with multiple `resource` parameters (form array)", async () => { // OAuth 2.0 resource indicators (RFC 8707) allow repeated `resource` // parameters in form-encoded bodies; URLSearchParams represents this // by appending the same key twice. Better Auth's body parser converts // repeats into an array so the hook sees `body.resource` as a string[]. const basic = btoa(`${clientId}:${clientSecret}`); const params = new URLSearchParams(); params.append("grant_type", "authorization_code"); params.append("code", "no-such-code"); params.append("resource", "https://api.example.com"); params.append("resource", "https://other.example.com"); const res = await SELF.fetch(`${ISSUER}/api/auth/oauth2/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${basic}`, }, body: params.toString(), }); expect(res.status).toBe(400); const body = (await res.json()) as JsonErrorBody; expect(body.error).toBe("invalid_request"); expect(body.code).toBe("RESOURCE_NOT_SUPPORTED"); }); it("supports Post after a metadata-only correction without rotating the secret or revoking existing tokens", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.70.0.2"); const client = await createOAuthClientAsAdmin(cookie, { name: "legacy Post client", redirectUris: [DEFAULT_REDIRECT_URI], skipConsent: true, }); const first = await authorizeWithPkce({ clientId: client.client_id, cookie }); expect(first.code).toBeTruthy(); const firstResponse = await exchangeAuthorizationCode({ code: first.code!, verifier: first.verifier, clientId: client.client_id, clientSecret: client.client_secret, }); expect(firstResponse.status).toBe(200); const firstTokens = (await firstResponse.json()) as OAuthTokenResponse; const before = await env.DB.prepare("SELECT client_secret FROM oauth_clients WHERE client_id = ?") .bind(client.client_id) .first<{ client_secret: string }>(); await env.DB.prepare( "UPDATE oauth_clients SET token_endpoint_auth_method = 'client_secret_post' WHERE client_id = ?", ) .bind(client.client_id) .run(); const after = await env.DB.prepare("SELECT client_secret FROM oauth_clients WHERE client_id = ?") .bind(client.client_id) .first<{ client_secret: string }>(); expect(Boolean(before?.client_secret && before.client_secret === after?.client_secret)).toBe(true); const existingToken = await SELF.fetch(`${ISSUER}/api/auth/oauth2/userinfo`, { headers: { authorization: `Bearer ${firstTokens.access_token}` }, }); expect(existingToken.status).toBe(200); const next = await authorizeWithPkce({ clientId: client.client_id, cookie }); expect(next.code).toBeTruthy(); const response = await SELF.fetch(`${ISSUER}/api/auth/oauth2/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: next.code!, code_verifier: next.verifier, redirect_uri: DEFAULT_REDIRECT_URI, client_id: client.client_id, client_secret: client.client_secret, }), }); expect(response.status).toBe(200); expect(Boolean(((await response.json()) as OAuthTokenResponse).id_token)).toBe(true); }); });