idp-e2e-proof.mjs183 lines · main
1/**
2 * Briven Auth OIDC IdP E2E proof (service-level, Doltgres).
3 *
4 * Confidential client + public PKCE client:
5 * create client → auth request → consent/code → token → userinfo → refresh → revoke → introspect
6 *
7 * cd apps/api
8 * BRIVEN_ENGINE_DATABASE_URL=... BRIVEN_DATA_PLANE_URL=... \
9 * BRIVEN_AUTH_CORE_ENABLED=true BRIVEN_ENV=development \
10 * BRIVEN_API_ORIGIN=https://api.briven.tech BRIVEN_WEB_ORIGIN=https://briven.tech \
11 * BRIVEN_BETTER_AUTH_SECRET=dev-secret-at-least-32-chars-long!! \
12 * bun scripts/idp-e2e-proof.mjs
13 */
14
15process.env.BRIVEN_AUTH_CORE_ENABLED = 'true';
16process.env.BRIVEN_ENV = process.env.BRIVEN_ENV ?? 'development';
17process.env.BRIVEN_ENGINE_DATABASE_URL =
18 process.env.BRIVEN_ENGINE_DATABASE_URL ??
19 'postgres://postgres:devpass@127.0.0.1:5434/briven_engine?sslmode=disable';
20process.env.BRIVEN_DATA_PLANE_URL =
21 process.env.BRIVEN_DATA_PLANE_URL ??
22 'postgres://postgres:devpass@127.0.0.1:5434/postgres?sslmode=disable';
23process.env.BRIVEN_API_ORIGIN =
24 process.env.BRIVEN_API_ORIGIN ?? 'https://api.briven.tech';
25process.env.BRIVEN_WEB_ORIGIN =
26 process.env.BRIVEN_WEB_ORIGIN ?? 'https://briven.tech';
27process.env.BRIVEN_BETTER_AUTH_SECRET =
28 process.env.BRIVEN_BETTER_AUTH_SECRET ?? 'dev-secret-at-least-32-chars-long!!';
29
30import { createHash, randomBytes } from 'node:crypto';
31
32const { ensureBrivenEngineDatabase } = await import(
33 '../src/services/auth-core/ensure-db.ts'
34);
35const { initAuthCoreSdk } = await import('../src/services/auth-core/engine.ts');
36const { signUpEmailPassword } = await import(
37 '../src/services/auth-core/emailpassword.ts'
38);
39const { createOidcClient } = await import(
40 '../src/services/auth-core/idp-clients.ts'
41);
42const {
43 createAuthRequest,
44 issueAuthCodeAndRedirect,
45 exchangeAuthorizationCode,
46 exchangeRefreshToken,
47 buildUserInfo,
48 revokeToken,
49 introspectToken,
50 discoveryDocument,
51} = await import('../src/services/auth-core/idp-flow.ts');
52const { getOidcJwks } = await import('../src/services/auth-core/idp-signing.ts');
53
54function fail(msg, extra) {
55 console.error('FAIL', msg, extra ?? '');
56 process.exit(1);
57}
58function ok(msg) {
59 console.log('ok', msg);
60}
61
62console.log('=== IdP E2E proof (briven-engine OIDC) ===');
63
64const db = await ensureBrivenEngineDatabase();
65if (!db.ok) fail('ensure db', db);
66if (!(await initAuthCoreSdk())) fail('init sdk');
67
68const doc = discoveryDocument();
69if (!doc.authorization_endpoint || !doc.token_endpoint || !doc.jwks_uri) {
70 fail('discovery missing endpoints', doc);
71}
72ok('discovery shape');
73
74const jwks = await getOidcJwks();
75if (!jwks.keys?.length) fail('jwks empty');
76ok(`jwks keys=${jwks.keys.length}`);
77
78const projectId = `p_idp_${Date.now().toString(36)}`;
79const email = `idp_${Date.now()}@example.com`;
80const su = await signUpEmailPassword({
81 email,
82 password: 'IdpProof!99xx',
83 projectId,
84});
85if (su.status !== 'OK') fail('signup', su);
86const userId = su.user.id;
87ok(`user ${userId}`);
88
89const redirect = 'http://localhost:9999/cb';
90const conf = await createOidcClient({
91 projectId,
92 name: 'E2E Confidential',
93 redirectUris: [redirect],
94 isPublic: false,
95});
96if (!conf.clientSecret) fail('confidential secret missing');
97ok(`confidential client ${conf.client.clientId}`);
98
99const authReq = await createAuthRequest({
100 client: conf.client,
101 redirectUri: redirect,
102 scope: 'openid profile email offline_access',
103 state: 'st1',
104 nonce: 'n1',
105});
106const { redirectUrl } = await issueAuthCodeAndRedirect(authReq.id, userId);
107const code = new URL(redirectUrl).searchParams.get('code');
108if (!code) fail('no code in redirect', redirectUrl);
109ok('authorization code issued');
110
111const tok = await exchangeAuthorizationCode({
112 code,
113 redirectUri: redirect,
114 clientId: conf.client.clientId,
115 clientSecret: conf.clientSecret,
116});
117if (!tok.ok) fail('token exchange', tok);
118if (!tok.access_token || !tok.id_token) fail('missing tokens', tok);
119ok('token exchange (confidential)');
120
121const info = await buildUserInfo(tok.access_token);
122if (!info.ok) fail('userinfo', info);
123const sub = info.body?.sub;
124if (sub !== userId) fail('userinfo sub mismatch', info);
125ok(`userinfo sub=${sub}`);
126
127if (!tok.refresh_token) fail('expected refresh_token with offline_access');
128const refreshed = await exchangeRefreshToken({
129 refreshToken: tok.refresh_token,
130 clientId: conf.client.clientId,
131 clientSecret: conf.clientSecret,
132});
133if (!refreshed.ok) fail('refresh', refreshed);
134ok('refresh token');
135
136const intro = await introspectToken({
137 token: refreshed.access_token,
138 clientId: conf.client.clientId,
139 clientSecret: conf.clientSecret,
140});
141if (!intro.active) fail('introspect inactive', intro);
142ok('introspect active');
143
144const rev = await revokeToken({
145 token: tok.refresh_token,
146 clientId: conf.client.clientId,
147 clientSecret: conf.clientSecret,
148});
149if (!rev.ok) fail('revoke', rev);
150ok('revoke');
151
152// Public + PKCE
153const verifier = randomBytes(32).toString('base64url');
154const challenge = createHash('sha256').update(verifier).digest('base64url');
155const pub = await createOidcClient({
156 projectId,
157 name: 'E2E Public PKCE',
158 redirectUris: [redirect],
159 isPublic: true,
160});
161const authReq2 = await createAuthRequest({
162 client: pub.client,
163 redirectUri: redirect,
164 scope: 'openid email',
165 codeChallenge: challenge,
166 codeChallengeMethod: 'S256',
167});
168const { redirectUrl: redir2 } = await issueAuthCodeAndRedirect(
169 authReq2.id,
170 userId,
171);
172const code2 = new URL(redir2).searchParams.get('code');
173const tok2 = await exchangeAuthorizationCode({
174 code: code2,
175 redirectUri: redirect,
176 clientId: pub.client.clientId,
177 codeVerifier: verifier,
178});
179if (!tok2.ok) fail('pkce token', tok2);
180ok('public client + PKCE');
181
182console.log('=== IdP E2E proof PASSED ===');
183process.exit(0);