idp-live-proof.ts214 lines · main
1/**
2 * End-to-end IdP proof for briven-engine OIDC provider.
3 *
4 * Run inside API container (or local with same env):
5 * bun run scripts/idp-live-proof.ts
6 *
7 * Steps: discovery → create client → user → auth request → code →
8 * token → userinfo → refresh → revoke → introspect
9 */
10
11import { createHash, randomBytes } from 'node:crypto';
12
13const PROJECT_ID =
14 process.env.BRIVEN_IDP_PROOF_PROJECT_ID ??
15 'p_01KW5RC84WZXBF3EE8ZCK9X8EX';
16const API =
17 (process.env.BRIVEN_API_ORIGIN ?? 'https://api.briven.tech').replace(
18 /\/$/,
19 '',
20 );
21
22function fail(msg: string): never {
23 console.error('FAIL:', msg);
24 process.exit(1);
25}
26
27function ok(step: string, detail?: string) {
28 console.log(`OK ${step}${detail ? ` — ${detail}` : ''}`);
29}
30
31async function httpJson(
32 path: string,
33 init?: RequestInit,
34): Promise<{ status: number; body: Record<string, unknown> }> {
35 const res = await fetch(`${API}${path}`, init);
36 const text = await res.text();
37 let body: Record<string, unknown> = {};
38 try {
39 body = text ? (JSON.parse(text) as Record<string, unknown>) : {};
40 } catch {
41 body = { raw: text.slice(0, 200) };
42 }
43 return { status: res.status, body };
44}
45
46async function main() {
47 console.log('IdP live proof');
48 console.log(' API:', API);
49 console.log(' project:', PROJECT_ID);
50
51 // 1. Discovery
52 const disc = await httpJson(
53 '/v1/auth-core/oidc/.well-known/openid-configuration',
54 );
55 if (disc.status !== 200) fail(`discovery HTTP ${disc.status}`);
56 if (!disc.body.authorization_endpoint || !disc.body.token_endpoint) {
57 fail('discovery missing endpoints');
58 }
59 ok('discovery', String(disc.body.issuer));
60
61 // 2. JWKS
62 const jwks = await httpJson('/v1/auth-core/oidc/jwks.json');
63 if (jwks.status !== 200) fail(`jwks HTTP ${jwks.status}`);
64 const keys = (jwks.body.keys as unknown[]) ?? [];
65 if (keys.length < 1) fail('jwks empty');
66 ok('jwks', `${keys.length} key(s)`);
67
68 // 3. In-process service path (same process as API when run via import)
69 // Dynamic import of engine services
70 const { createOidcClient } = await import(
71 '../apps/api/src/services/auth-core/idp-clients.ts'
72 );
73 const {
74 createAuthRequest,
75 issueAuthCodeAndRedirect,
76 exchangeAuthorizationCode,
77 exchangeRefreshToken,
78 buildUserInfo,
79 revokeToken,
80 introspectToken,
81 } = await import('../apps/api/src/services/auth-core/idp-flow.ts');
82 const { signUpEmailPassword } = await import(
83 '../apps/api/src/services/auth-core/emailpassword.ts'
84 );
85 const { bootstrapBrivenEngineSchema } = await import(
86 '../apps/api/src/services/auth-core/schema.ts'
87 );
88 const { openEnginePool } = await import(
89 '../apps/api/src/services/auth-core/db.ts'
90 );
91
92 openEnginePool();
93 await bootstrapBrivenEngineSchema();
94
95 const redirectUri = 'https://localhost:3999/oidc/callback';
96 const created = await createOidcClient({
97 projectId: PROJECT_ID,
98 name: `IdP proof ${new Date().toISOString().slice(0, 19)}`,
99 redirectUris: [redirectUri],
100 logoUrl: 'https://briven.tech/favicon.ico',
101 isPublic: false,
102 createdBy: 'idp-live-proof',
103 });
104 const clientId = created.client.clientId;
105 const clientSecret = created.clientSecret;
106 if (!clientSecret) fail('expected confidential client secret');
107 ok('create client', clientId);
108
109 const email = `idp-proof-${randomBytes(4).toString('hex')}@example.com`;
110 const password = `Proof!${randomBytes(6).toString('hex')}aA1`;
111 const sign = await signUpEmailPassword({
112 email,
113 password,
114 tenantId: `proj-${PROJECT_ID.toLowerCase()}`,
115 });
116 if (sign.status !== 'OK' || !sign.user?.id) {
117 // retry public tenant
118 const sign2 = await signUpEmailPassword({ email, password });
119 if (sign2.status !== 'OK' || !sign2.user?.id) {
120 fail(`signup failed: ${sign.status} / ${sign2.status}`);
121 }
122 var userId = sign2.user.id;
123 } else {
124 var userId = sign.user.id;
125 }
126 ok('signup user', userId);
127
128 // PKCE
129 const verifier = randomBytes(32).toString('base64url');
130 const challenge = createHash('sha256').update(verifier).digest('base64url');
131
132 const authReq = await createAuthRequest({
133 client: created.client,
134 redirectUri,
135 scope: 'openid email profile offline_access',
136 state: 'proof-state',
137 nonce: 'proof-nonce',
138 codeChallenge: challenge,
139 codeChallengeMethod: 'S256',
140 });
141 ok('auth request', authReq.id);
142
143 const { redirectUrl } = await issueAuthCodeAndRedirect(authReq.id, userId);
144 const code = new URL(redirectUrl).searchParams.get('code');
145 if (!code) fail(`no code in redirect: ${redirectUrl}`);
146 ok('auth code', code.slice(0, 12) + '…');
147
148 const tokens = await exchangeAuthorizationCode({
149 code,
150 redirectUri,
151 clientId,
152 clientSecret,
153 codeVerifier: verifier,
154 });
155 if (!tokens.ok) fail(`token: ${tokens.error} ${tokens.error_description}`);
156 ok('token (authorization_code)', `expires_in=${tokens.expires_in}`);
157
158 const ui = await buildUserInfo(tokens.access_token);
159 if (!ui.ok) fail(`userinfo: ${ui.error}`);
160 ok('userinfo', `sub=${ui.body.sub} email=${ui.body.email ?? 'n/a'}`);
161
162 if (!tokens.refresh_token) fail('missing refresh_token');
163 const refreshed = await exchangeRefreshToken({
164 refreshToken: tokens.refresh_token,
165 clientId,
166 clientSecret,
167 });
168 if (!refreshed.ok) {
169 fail(`refresh: ${refreshed.error} ${refreshed.error_description}`);
170 }
171 ok('token (refresh)', `expires_in=${refreshed.expires_in}`);
172
173 const intro = await introspectToken({
174 token: refreshed.access_token,
175 clientId,
176 clientSecret,
177 });
178 if (!intro.active) fail('introspect access not active');
179 ok('introspect', `active=${intro.active}`);
180
181 await revokeToken({
182 token: refreshed.refresh_token ?? tokens.refresh_token,
183 clientId,
184 clientSecret,
185 });
186 ok('revoke');
187
188 // HTTP discovery already proved; authorize without client returns invalid_client
189 const badAuth = await httpJson(
190 `/v1/auth-core/oidc/authorize?client_id=nope&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=openid`,
191 );
192 if (badAuth.status !== 400) fail(`expected 400 for bad client, got ${badAuth.status}`);
193 ok('authorize rejects unknown client');
194
195 console.log('\nALL IdP PROOFS PASSED');
196 console.log(
197 JSON.stringify(
198 {
199 projectId: PROJECT_ID,
200 clientId,
201 userId,
202 email,
203 issuer: disc.body.issuer,
204 },
205 null,
206 2,
207 ),
208 );
209}
210
211main().catch((e) => {
212 console.error(e);
213 process.exit(1);
214});