Files
zpan/server/auth/oauth-par.test.ts
T
Jasper Van 2657f82ef1 feat(auth): add contextual OAuth workspace grants (#550)
* feat(auth): add contextual OAuth workspace grants

* fix(auth): register RFC 9396 detail types

* fix(openapi): restore delegated CLI auth profile

* fix(store): recover paid capacity fulfillment

* fix(auth): close OAuth contract gaps

* fix(store): resume verified x402 settlements

* chore(deps): update zpan cloud sdk to 2.5.2

* test(store): cover verified settlement recovery

* fix(auth): localize standard consent scopes

* fix(docker): include dependency patches before install

* fix(store): harden x402 purchase responses

* test(auth): cover OAuth authorization boundaries

* docs: add PR 550 verification screenshots

* chore: remove temporary verification screenshots

* docs(openapi): document exhausted capacity response
2026-08-02 13:33:15 -04:00

110 lines
3.7 KiB
TypeScript

import { WORKSPACE_AUTHORIZATION_DETAIL_TYPE } from '@shared/oauth'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { oauthPushedAuthorizationRequests, resolvePushedAuthorizationRequest } from './oauth-par'
const getOAuthProviderApi = vi.hoisted(() => vi.fn())
vi.mock('@better-auth/oauth-provider', async (importOriginal) => ({
...(await importOriginal<typeof import('@better-auth/oauth-provider')>()),
getOAuthProviderApi,
}))
const requestUri = 'urn:ietf:params:oauth:request_uri:test'
function pushedRequest(overrides: Record<string, string> = {}) {
return {
client_id: 'client-1',
redirect_uri: 'https://agent.example/callback',
response_type: 'code',
scope: 'openid',
code_challenge_method: 'S256',
code_challenge: 'a'.repeat(43),
authorization_details: JSON.stringify([{ type: WORKSPACE_AUTHORIZATION_DETAIL_TYPE }]),
...overrides,
}
}
function endpointContext(body: Record<string, string>) {
return {
body,
context: { adapter: { create: vi.fn() } },
setHeader: vi.fn(),
} as never
}
async function submit(body: Record<string, string>) {
const endpoint = oauthPushedAuthorizationRequests({ scopes: ['openid'] } as never).endpoints
?.oauth2PushedAuthorizationRequest
if (!endpoint) throw new Error('PAR endpoint is not configured')
return endpoint(endpointContext(body))
}
describe('OAuth pushed authorization requests', () => {
beforeEach(() => {
getOAuthProviderApi.mockReturnValue({
getClient: vi.fn(async () => ({
clientId: 'client-1',
redirectUris: ['https://agent.example/callback'],
responseTypes: ['code'],
grantTypes: ['authorization_code'],
scopes: ['openid'],
public: true,
})),
authenticateClient: vi.fn(async () => ({ clientId: 'client-1' })),
})
})
it.each([
[{ response_type: 'token' }, 'Only the authorization code response type is supported'],
[{ code_challenge: 'short' }, 'A valid S256 PKCE challenge is required'],
[{ authorization_details: 'not-json' }, 'Invalid workspace authorization details'],
[{ authorization_details: '[]' }, 'Exactly one workspace authorization request is required'],
])('rejects invalid pushed request parameters %#', async (overrides, message) => {
await expect(submit(pushedRequest(overrides))).rejects.toMatchObject({
body: expect.objectContaining({ error_description: message }),
})
})
it('rejects clients without the authorization code grant', async () => {
getOAuthProviderApi.mockReturnValue({
getClient: vi.fn(async () => ({
clientId: 'client-1',
redirectUris: ['https://agent.example/callback'],
responseTypes: ['code'],
grantTypes: ['refresh_token'],
scopes: ['openid'],
public: true,
})),
authenticateClient: vi.fn(async () => ({ clientId: 'client-1' })),
})
await expect(submit(pushedRequest())).rejects.toMatchObject({
body: expect.objectContaining({ error_description: 'Client cannot use the authorization code grant' }),
})
})
it('deletes an expired request before rejecting it', async () => {
const adapter = {
findOne: vi.fn(async () => ({
id: 'par-1',
clientId: 'client-1',
parameters: { scope: 'openid' },
expiresAt: new Date(0),
})),
delete: vi.fn(async () => undefined),
}
await expect(
resolvePushedAuthorizationRequest({
requestUri,
clientId: 'client-1',
ctx: { context: { adapter } } as never,
}),
).resolves.toBeNull()
expect(adapter.delete).toHaveBeenCalledWith({
model: 'oauthPushedAuthorizationRequest',
where: [{ field: 'id', value: 'par-1' }],
})
})
})