/** * Tests for the identity-provider boundary. * * These cases pin the trust decisions shared by protected requests and profile * creation. Membership remains deliberately outside this module. */ import { strict as assert } from 'node:assert'; import { generateKeyPairSync, type KeyObject } from 'node:crypto'; import { createServer, type Server } from 'node:http'; import type { AddressInfo } from 'node:net'; import { after, before, describe, it } from 'node:test'; import { exportJWK, SignJWT } from 'jose'; import { createSupabaseAuthProvider, type AuthProvider, } from '../src/lib/auth-provider'; describe('Supabase auth provider', () => { let server: Server; let provider: AuthProvider; let issuer: string; let privateKey: KeyObject; before(async () => { const keys = generateKeyPairSync('rsa', { modulusLength: 2048 }); privateKey = keys.privateKey; const publicJwk = await exportJWK(keys.publicKey); server = createServer((request, response) => { if (request.url !== '/auth/v1/.well-known/jwks.json') { response.writeHead(404).end(); return; } response.setHeader('content-type', 'application/json'); response.end( JSON.stringify({ keys: [{ ...publicJwk, alg: 'RS256', kid: 'test-key', use: 'sig' }], }), ); }); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); }); const address = server.address() as AddressInfo; const supabaseUrl = `http://127.0.0.1:${address.port}`; issuer = `${supabaseUrl}/auth/v1`; provider = createSupabaseAuthProvider(supabaseUrl); }); after( () => new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }), ); async function sign(claims: Record, tokenIssuer = issuer): Promise { return new SignJWT(claims) .setProtectedHeader({ alg: 'RS256', kid: 'test-key' }) .setIssuer(tokenIssuer) .setExpirationTime('5m') .sign(privateKey); } it('returns only the verified external identity claims', async () => { const token = await sign({ sub: 'provider-user-1', email: 'Owner@Example.com' }); assert.deepEqual(await provider.verifyAccessToken(token), { subject: 'provider-user-1', email: 'Owner@Example.com', }); }); it('rejects a correctly signed token issued for a different identity provider', async () => { // Signature validity alone is insufficient: without the issuer check, a // sibling deployment using the same key could authenticate here. const token = await sign({ sub: 'provider-user-1' }, 'https://other.example/auth/v1'); await assert.rejects(provider.verifyAccessToken(token)); }); it('accepts a subject without email for protected requests', async () => { // Existing members are joined by subject. Email is required only by the // invite-gated profile flow, not as an extra condition on every request. const token = await sign({ sub: 'provider-user-2' }); assert.deepEqual(await provider.verifyAccessToken(token), { subject: 'provider-user-2', email: undefined, }); }); it('rejects a token with no stable subject', async () => { const token = await sign({ email: 'owner@example.com' }); await assert.rejects(provider.verifyAccessToken(token)); }); });