This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Authentication-provider boundary.
|
||||
*
|
||||
* Providers prove an external identity. They do not decide whether that
|
||||
* identity belongs to PIG; workspace membership remains a database decision
|
||||
* in the authenticator and signup route.
|
||||
*/
|
||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
||||
import type { Config } from './config';
|
||||
|
||||
export interface VerifiedIdentity {
|
||||
subject: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface AuthProvider {
|
||||
verifyAccessToken(token: string): Promise<VerifiedIdentity>;
|
||||
}
|
||||
|
||||
export function createSupabaseAuthProvider(supabaseUrl: string): AuthProvider {
|
||||
const issuer = `${supabaseUrl}/auth/v1`;
|
||||
// `jose` fetches lazily and caches this set, including safe key rotation.
|
||||
// Sharing one provider instance avoids a remote lookup path per handler.
|
||||
const jwks = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
|
||||
|
||||
return {
|
||||
async verifyAccessToken(token: string): Promise<VerifiedIdentity> {
|
||||
const { payload } = await jwtVerify(token, jwks, { issuer });
|
||||
if (!payload.sub) throw new Error('token has no subject');
|
||||
|
||||
return {
|
||||
subject: payload.sub,
|
||||
email: typeof payload.email === 'string' ? payload.email : undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createConfiguredAuthProvider(
|
||||
config: Pick<Config, 'SUPABASE_URL'>,
|
||||
): AuthProvider | null {
|
||||
return config.SUPABASE_URL ? createSupabaseAuthProvider(config.SUPABASE_URL) : null;
|
||||
}
|
||||
Reference in New Issue
Block a user