idp-live-proof.ts 6.3 KB

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