idp-e2e-proof.mjs 5.6 KB

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