idp-flow.ts776 lines · main
1/**
2 * OIDC authorization code + PKCE + refresh flow (production IdP).
3 */
4
5import { createHash, randomBytes } from 'node:crypto';
6
7import { SignJWT, jwtVerify, importJWK, type JWTPayload } from 'jose';
8
9import { env } from '../../env.js';
10import { getEnginePool } from './db.js';
11import { ensureOidcSigningKey } from './idp-signing.js';
12import {
13 getOidcClientByClientId,
14 redirectUriAllowed,
15 type OidcClient,
16 verifyOidcClientSecret,
17} from './idp-clients.js';
18import { recordBrivenEngineAudit } from './audit.js';
19
20export const ACCESS_TOKEN_TTL = 3600; // 1h
21export const REFRESH_TOKEN_TTL_DAYS = 30;
22export const AUTH_CODE_TTL_SEC = 600; // 10m
23export const AUTH_REQUEST_TTL_SEC = 900; // 15m
24
25function hash(value: string): string {
26 return createHash('sha256').update(value).digest('hex');
27}
28
29function sha256Base64Url(input: string): string {
30 return createHash('sha256').update(input).digest('base64url');
31}
32
33export function oidcIssuer(): string {
34 return `${env.BRIVEN_API_ORIGIN.replace(/\/$/, '')}/v1/auth-core/oidc`;
35}
36
37export function webOrigin(): string {
38 return env.BRIVEN_WEB_ORIGIN.replace(/\/$/, '');
39}
40
41export type AuthRequest = {
42 id: string;
43 clientId: string;
44 projectId: string;
45 redirectUri: string;
46 scope: string;
47 state: string | null;
48 nonce: string | null;
49 codeChallenge: string | null;
50 codeChallengeMethod: string | null;
51 userId: string | null;
52 consentedAt: string | null;
53 expiresAt: string;
54};
55
56function mapAuthReq(r: Record<string, unknown>): AuthRequest {
57 return {
58 id: String(r.id),
59 clientId: String(r.client_id),
60 projectId: String(r.project_id),
61 redirectUri: String(r.redirect_uri),
62 scope: String(r.scope),
63 state: r.state ? String(r.state) : null,
64 nonce: r.nonce ? String(r.nonce) : null,
65 codeChallenge: r.code_challenge ? String(r.code_challenge) : null,
66 codeChallengeMethod: r.code_challenge_method
67 ? String(r.code_challenge_method)
68 : null,
69 userId: r.user_id ? String(r.user_id) : null,
70 consentedAt: r.consented_at
71 ? r.consented_at instanceof Date
72 ? r.consented_at.toISOString()
73 : String(r.consented_at)
74 : null,
75 expiresAt:
76 r.expires_at instanceof Date
77 ? r.expires_at.toISOString()
78 : String(r.expires_at),
79 };
80}
81
82export async function createAuthRequest(input: {
83 client: OidcClient;
84 redirectUri: string;
85 scope: string;
86 state?: string | null;
87 nonce?: string | null;
88 codeChallenge?: string | null;
89 codeChallengeMethod?: string | null;
90}): Promise<AuthRequest> {
91 if (input.client.revokedAt) throw new Error('client_revoked');
92 if (!redirectUriAllowed(input.client, input.redirectUri)) {
93 throw new Error('invalid_redirect_uri');
94 }
95 if (input.client.isPublic) {
96 if (!input.codeChallenge) throw new Error('pkce_required');
97 const method = (input.codeChallengeMethod ?? 'S256').toUpperCase();
98 if (method !== 'S256' && method !== 'PLAIN') {
99 throw new Error('unsupported_code_challenge_method');
100 }
101 }
102
103 const scopes = input.scope.split(/\s+/).filter(Boolean);
104 if (!scopes.includes('openid')) {
105 throw new Error('openid_scope_required');
106 }
107 for (const s of scopes) {
108 if (!input.client.scopes.includes(s) && s !== 'openid') {
109 // allow openid always; others must be registered on client
110 if (!['profile', 'email', 'offline_access'].includes(s)) {
111 throw new Error(`invalid_scope:${s}`);
112 }
113 }
114 }
115
116 const id = `oar_${randomBytes(16).toString('hex')}`;
117 const expiresAt = new Date(Date.now() + AUTH_REQUEST_TTL_SEC * 1000);
118 const pool = getEnginePool();
119 await pool.query(
120 `INSERT INTO be_oidc_auth_requests
121 (id, client_id, project_id, redirect_uri, scope, state, nonce,
122 code_challenge, code_challenge_method, response_type, expires_at)
123 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,'code',$10)`,
124 [
125 id,
126 input.client.clientId,
127 input.client.projectId,
128 input.redirectUri,
129 scopes.join(' '),
130 input.state ?? null,
131 input.nonce ?? null,
132 input.codeChallenge ?? null,
133 input.codeChallengeMethod ?? null,
134 expiresAt.toISOString(),
135 ],
136 );
137 const req = await getAuthRequest(id);
138 if (!req) throw new Error('auth_request_create_failed');
139 return req;
140}
141
142export async function getAuthRequest(id: string): Promise<AuthRequest | null> {
143 const pool = getEnginePool();
144 const res = await pool.query(
145 `SELECT * FROM be_oidc_auth_requests WHERE id = $1 LIMIT 1`,
146 [id],
147 );
148 const row = res.rows[0] as Record<string, unknown> | undefined;
149 if (!row) return null;
150 const req = mapAuthReq(row);
151 if (new Date(req.expiresAt).getTime() < Date.now()) return null;
152 return req;
153}
154
155export async function attachUserToAuthRequest(
156 id: string,
157 userId: string,
158): Promise<AuthRequest | null> {
159 const pool = getEnginePool();
160 await pool.query(
161 `UPDATE be_oidc_auth_requests SET user_id = $2 WHERE id = $1 AND user_id IS NULL`,
162 [id, userId],
163 );
164 return getAuthRequest(id);
165}
166
167export async function hasConsent(
168 userId: string,
169 clientId: string,
170 scope: string,
171): Promise<boolean> {
172 const pool = getEnginePool();
173 const res = await pool.query(
174 `SELECT scope FROM be_oidc_consents WHERE user_id = $1 AND client_id = $2 LIMIT 1`,
175 [userId, clientId],
176 );
177 const row = res.rows[0] as { scope?: string } | undefined;
178 if (!row?.scope) return false;
179 const granted = new Set(row.scope.split(/\s+/));
180 return scope.split(/\s+/).every((s) => granted.has(s));
181}
182
183export async function grantConsent(
184 userId: string,
185 clientId: string,
186 scope: string,
187): Promise<void> {
188 const pool = getEnginePool();
189 await pool.query(
190 `INSERT INTO be_oidc_consents (user_id, client_id, scope, granted_at)
191 VALUES ($1, $2, $3, NOW())
192 ON CONFLICT (user_id, client_id) DO UPDATE SET scope = $3, granted_at = NOW()`,
193 [userId, clientId, scope],
194 );
195 void recordBrivenEngineAudit({
196 action: 'oidc.consent.granted',
197 userId,
198 metadata: { clientId, scope },
199 });
200}
201
202/** Issue authorization code after consent; returns redirect URL. */
203export async function issueAuthCodeAndRedirect(
204 requestId: string,
205 userId: string,
206): Promise<{ redirectUrl: string }> {
207 const req = await getAuthRequest(requestId);
208 if (!req) throw new Error('invalid_request');
209 if (req.userId && req.userId !== userId) throw new Error('user_mismatch');
210
211 const client = await getOidcClientByClientId(req.clientId);
212 if (!client || client.revokedAt) throw new Error('invalid_client');
213
214 await grantConsent(userId, req.clientId, req.scope);
215
216 const code = `ac_${randomBytes(24).toString('base64url')}`;
217 const codeHash = hash(code);
218 const expiresAt = new Date(Date.now() + AUTH_CODE_TTL_SEC * 1000);
219 const pool = getEnginePool();
220
221 await pool.query(
222 `INSERT INTO be_oidc_auth_codes
223 (code_hash, client_id, project_id, user_id, redirect_uri, scope, nonce,
224 code_challenge, code_challenge_method, expires_at)
225 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`,
226 [
227 codeHash,
228 req.clientId,
229 req.projectId,
230 userId,
231 req.redirectUri,
232 req.scope,
233 req.nonce,
234 req.codeChallenge,
235 req.codeChallengeMethod,
236 expiresAt.toISOString(),
237 ],
238 );
239 await pool.query(
240 `UPDATE be_oidc_auth_requests SET user_id = $2, consented_at = NOW() WHERE id = $1`,
241 [requestId, userId],
242 );
243
244 void recordBrivenEngineAudit({
245 action: 'oidc.code.issued',
246 projectId: req.projectId,
247 userId,
248 metadata: { clientId: req.clientId },
249 });
250
251 const url = new URL(req.redirectUri);
252 url.searchParams.set('code', code);
253 if (req.state) url.searchParams.set('state', req.state);
254 return { redirectUrl: url.toString() };
255}
256
257export async function denyAuthRequest(
258 requestId: string,
259): Promise<{ redirectUrl: string }> {
260 const req = await getAuthRequest(requestId);
261 if (!req) throw new Error('invalid_request');
262 const url = new URL(req.redirectUri);
263 url.searchParams.set('error', 'access_denied');
264 url.searchParams.set('error_description', 'user denied consent');
265 if (req.state) url.searchParams.set('state', req.state);
266 void recordBrivenEngineAudit({
267 action: 'oidc.consent.denied',
268 projectId: req.projectId,
269 metadata: { clientId: req.clientId },
270 });
271 return { redirectUrl: url.toString() };
272}
273
274async function loadUserClaims(userId: string): Promise<{
275 sub: string;
276 email?: string;
277 email_verified?: boolean;
278 name?: string;
279}> {
280 const pool = getEnginePool();
281 const res = await pool.query(
282 `SELECT id, email, email_verified, metadata_json FROM be_users WHERE id = $1 LIMIT 1`,
283 [userId],
284 );
285 const row = res.rows[0] as
286 | {
287 id: string;
288 email?: string | null;
289 email_verified?: boolean;
290 metadata_json?: string;
291 }
292 | undefined;
293 if (!row) return { sub: userId };
294 let name: string | undefined;
295 try {
296 const meta = JSON.parse(row.metadata_json ?? '{}') as { name?: string };
297 if (meta.name) name = meta.name;
298 } catch {
299 /* ignore */
300 }
301 return {
302 sub: row.id,
303 email: row.email ?? undefined,
304 email_verified: Boolean(row.email_verified),
305 name,
306 };
307}
308
309async function signAccessAndIdToken(input: {
310 client: OidcClient;
311 userId: string;
312 scope: string;
313 nonce?: string | null;
314}): Promise<{ accessToken: string; idToken: string; expiresIn: number }> {
315 const key = await ensureOidcSigningKey();
316 const claims = await loadUserClaims(input.userId);
317 const issuer = oidcIssuer();
318 const now = Math.floor(Date.now() / 1000);
319
320 const accessToken = await new SignJWT({
321 scope: input.scope,
322 client_id: input.client.clientId,
323 project_id: input.client.projectId,
324 token_use: 'access',
325 })
326 .setProtectedHeader({ alg: 'RS256', kid: key.kid, typ: 'JWT' })
327 .setIssuer(issuer)
328 .setAudience(input.client.clientId)
329 .setSubject(input.userId)
330 .setIssuedAt(now)
331 .setExpirationTime(now + ACCESS_TOKEN_TTL)
332 .setJti(`at_${randomBytes(8).toString('hex')}`)
333 .sign(key.privateKey);
334
335 const idPayload: Record<string, unknown> = {
336 token_use: 'id',
337 };
338 if (input.scope.includes('email') && claims.email) {
339 idPayload.email = claims.email;
340 idPayload.email_verified = claims.email_verified ?? false;
341 }
342 if (input.scope.includes('profile') && claims.name) {
343 idPayload.name = claims.name;
344 }
345 if (input.nonce) idPayload.nonce = input.nonce;
346
347 const idToken = await new SignJWT(idPayload)
348 .setProtectedHeader({ alg: 'RS256', kid: key.kid, typ: 'JWT' })
349 .setIssuer(issuer)
350 .setAudience(input.client.clientId)
351 .setSubject(input.userId)
352 .setIssuedAt(now)
353 .setExpirationTime(now + ACCESS_TOKEN_TTL)
354 .sign(key.privateKey);
355
356 return { accessToken, idToken, expiresIn: ACCESS_TOKEN_TTL };
357}
358
359async function mintRefreshToken(input: {
360 clientId: string;
361 projectId: string;
362 userId: string;
363 scope: string;
364}): Promise<string | null> {
365 if (!input.scope.includes('offline_access')) return null;
366 const raw = `rt_${randomBytes(32).toString('base64url')}`;
367 const expiresAt = new Date(
368 Date.now() + REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000,
369 );
370 const pool = getEnginePool();
371 await pool.query(
372 `INSERT INTO be_oidc_refresh_tokens
373 (token_hash, client_id, project_id, user_id, scope, expires_at)
374 VALUES ($1,$2,$3,$4,$5,$6)`,
375 [
376 hash(raw),
377 input.clientId,
378 input.projectId,
379 input.userId,
380 input.scope,
381 expiresAt.toISOString(),
382 ],
383 );
384 return raw;
385}
386
387function verifyPkce(
388 method: string | null,
389 challenge: string | null,
390 verifier: string | null | undefined,
391): boolean {
392 if (!challenge) return true; // confidential may omit
393 if (!verifier) return false;
394 const m = (method ?? 'S256').toUpperCase();
395 if (m === 'PLAIN') return verifier === challenge;
396 if (m === 'S256') return sha256Base64Url(verifier) === challenge;
397 return false;
398}
399
400export async function exchangeAuthorizationCode(input: {
401 code: string;
402 redirectUri: string;
403 clientId: string;
404 clientSecret?: string | null;
405 codeVerifier?: string | null;
406}): Promise<
407 | {
408 ok: true;
409 access_token: string;
410 id_token: string;
411 refresh_token?: string;
412 token_type: 'Bearer';
413 expires_in: number;
414 scope: string;
415 }
416 | { ok: false; error: string; error_description: string }
417> {
418 const client = await getOidcClientByClientId(input.clientId);
419 if (!client || client.revokedAt) {
420 return {
421 ok: false,
422 error: 'invalid_client',
423 error_description: 'unknown or revoked client',
424 };
425 }
426 if (!(await verifyOidcClientSecret(client, input.clientSecret))) {
427 return {
428 ok: false,
429 error: 'invalid_client',
430 error_description: 'client authentication failed',
431 };
432 }
433
434 const pool = getEnginePool();
435 const codeHash = hash(input.code);
436 const res = await pool.query(
437 `SELECT * FROM be_oidc_auth_codes WHERE code_hash = $1 LIMIT 1`,
438 [codeHash],
439 );
440 const row = res.rows[0] as Record<string, unknown> | undefined;
441 if (!row || row.used_at) {
442 return {
443 ok: false,
444 error: 'invalid_grant',
445 error_description: 'code invalid or already used',
446 };
447 }
448 if (String(row.client_id) !== input.clientId) {
449 return {
450 ok: false,
451 error: 'invalid_grant',
452 error_description: 'code client mismatch',
453 };
454 }
455 if (String(row.redirect_uri) !== input.redirectUri) {
456 return {
457 ok: false,
458 error: 'invalid_grant',
459 error_description: 'redirect_uri mismatch',
460 };
461 }
462 const exp = new Date(row.expires_at as string | Date);
463 if (exp.getTime() < Date.now()) {
464 return {
465 ok: false,
466 error: 'invalid_grant',
467 error_description: 'code expired',
468 };
469 }
470
471 const challenge = row.code_challenge ? String(row.code_challenge) : null;
472 const method = row.code_challenge_method
473 ? String(row.code_challenge_method)
474 : null;
475 if (client.isPublic || challenge) {
476 if (!verifyPkce(method, challenge, input.codeVerifier)) {
477 return {
478 ok: false,
479 error: 'invalid_grant',
480 error_description: 'pkce verification failed',
481 };
482 }
483 }
484
485 await pool.query(
486 `UPDATE be_oidc_auth_codes SET used_at = NOW() WHERE code_hash = $1`,
487 [codeHash],
488 );
489
490 const userId = String(row.user_id);
491 const scope = String(row.scope);
492 const nonce = row.nonce ? String(row.nonce) : null;
493
494 const tokens = await signAccessAndIdToken({
495 client,
496 userId,
497 scope,
498 nonce,
499 });
500 const refresh = await mintRefreshToken({
501 clientId: client.clientId,
502 projectId: client.projectId,
503 userId,
504 scope,
505 });
506
507 void recordBrivenEngineAudit({
508 action: 'oidc.token.issued',
509 projectId: client.projectId,
510 userId,
511 metadata: { clientId: client.clientId, grant: 'authorization_code' },
512 });
513
514 return {
515 ok: true,
516 access_token: tokens.accessToken,
517 id_token: tokens.idToken,
518 refresh_token: refresh ?? undefined,
519 token_type: 'Bearer',
520 expires_in: tokens.expiresIn,
521 scope,
522 };
523}
524
525export async function exchangeRefreshToken(input: {
526 refreshToken: string;
527 clientId: string;
528 clientSecret?: string | null;
529}): Promise<
530 | {
531 ok: true;
532 access_token: string;
533 id_token: string;
534 refresh_token?: string;
535 token_type: 'Bearer';
536 expires_in: number;
537 scope: string;
538 }
539 | { ok: false; error: string; error_description: string }
540> {
541 const client = await getOidcClientByClientId(input.clientId);
542 if (!client || client.revokedAt) {
543 return {
544 ok: false,
545 error: 'invalid_client',
546 error_description: 'unknown or revoked client',
547 };
548 }
549 if (!(await verifyOidcClientSecret(client, input.clientSecret))) {
550 return {
551 ok: false,
552 error: 'invalid_client',
553 error_description: 'client authentication failed',
554 };
555 }
556
557 const pool = getEnginePool();
558 const th = hash(input.refreshToken);
559 const res = await pool.query(
560 `SELECT * FROM be_oidc_refresh_tokens WHERE token_hash = $1 LIMIT 1`,
561 [th],
562 );
563 const row = res.rows[0] as Record<string, unknown> | undefined;
564 if (!row || row.revoked_at) {
565 return {
566 ok: false,
567 error: 'invalid_grant',
568 error_description: 'refresh token invalid',
569 };
570 }
571 if (String(row.client_id) !== input.clientId) {
572 return {
573 ok: false,
574 error: 'invalid_grant',
575 error_description: 'refresh client mismatch',
576 };
577 }
578 if (new Date(row.expires_at as string | Date).getTime() < Date.now()) {
579 return {
580 ok: false,
581 error: 'invalid_grant',
582 error_description: 'refresh token expired',
583 };
584 }
585
586 // Rotate refresh token
587 await pool.query(
588 `UPDATE be_oidc_refresh_tokens SET revoked_at = NOW() WHERE token_hash = $1`,
589 [th],
590 );
591
592 const userId = String(row.user_id);
593 const scope = String(row.scope);
594 const tokens = await signAccessAndIdToken({ client, userId, scope });
595 const refresh = await mintRefreshToken({
596 clientId: client.clientId,
597 projectId: client.projectId,
598 userId,
599 scope,
600 });
601
602 void recordBrivenEngineAudit({
603 action: 'oidc.token.issued',
604 projectId: client.projectId,
605 userId,
606 metadata: { clientId: client.clientId, grant: 'refresh_token' },
607 });
608
609 return {
610 ok: true,
611 access_token: tokens.accessToken,
612 id_token: tokens.idToken,
613 refresh_token: refresh ?? undefined,
614 token_type: 'Bearer',
615 expires_in: tokens.expiresIn,
616 scope,
617 };
618}
619
620export type AccessTokenPayload = JWTPayload & {
621 scope?: string;
622 client_id?: string;
623 project_id?: string;
624 token_use?: string;
625 sub: string;
626};
627
628export async function verifyOidcAccessToken(
629 token: string,
630): Promise<AccessTokenPayload> {
631 const key = await ensureOidcSigningKey();
632 const pub = await importJWK(key.publicJwk, 'RS256');
633 const { payload } = await jwtVerify(token, pub, { issuer: oidcIssuer() });
634 if (payload.token_use && payload.token_use !== 'access') {
635 throw new Error('not_access_token');
636 }
637 if (typeof payload.sub !== 'string') throw new Error('missing_sub');
638 return payload as AccessTokenPayload;
639}
640
641export async function buildUserInfo(accessToken: string): Promise<
642 | { ok: true; body: Record<string, unknown> }
643 | { ok: false; status: number; error: string }
644> {
645 try {
646 const payload = await verifyOidcAccessToken(accessToken);
647 const claims = await loadUserClaims(payload.sub);
648 const scope = String(payload.scope ?? '');
649 const body: Record<string, unknown> = { sub: claims.sub };
650 if (scope.includes('email') && claims.email) {
651 body.email = claims.email;
652 body.email_verified = claims.email_verified ?? false;
653 }
654 if (scope.includes('profile') && claims.name) {
655 body.name = claims.name;
656 }
657 return { ok: true, body };
658 } catch {
659 return { ok: false, status: 401, error: 'invalid_token' };
660 }
661}
662
663export async function revokeToken(input: {
664 token: string;
665 clientId: string;
666 clientSecret?: string | null;
667}): Promise<{ ok: true }> {
668 const client = await getOidcClientByClientId(input.clientId);
669 if (!client) return { ok: true }; // RFC 7009: always 200
670 if (!(await verifyOidcClientSecret(client, input.clientSecret))) {
671 return { ok: true };
672 }
673 const pool = getEnginePool();
674 await pool.query(
675 `UPDATE be_oidc_refresh_tokens SET revoked_at = NOW()
676 WHERE token_hash = $1 AND client_id = $2`,
677 [hash(input.token), input.clientId],
678 );
679 void recordBrivenEngineAudit({
680 action: 'oidc.token.revoked',
681 projectId: client.projectId,
682 metadata: { clientId: input.clientId },
683 });
684 return { ok: true };
685}
686
687export async function introspectToken(input: {
688 token: string;
689 clientId: string;
690 clientSecret?: string | null;
691}): Promise<Record<string, unknown>> {
692 const client = await getOidcClientByClientId(input.clientId);
693 if (!client || !(await verifyOidcClientSecret(client, input.clientSecret))) {
694 return { active: false };
695 }
696
697 // Try as refresh
698 const pool = getEnginePool();
699 const th = hash(input.token);
700 const rt = await pool.query(
701 `SELECT * FROM be_oidc_refresh_tokens WHERE token_hash = $1 LIMIT 1`,
702 [th],
703 );
704 const rrow = rt.rows[0] as Record<string, unknown> | undefined;
705 if (rrow && !rrow.revoked_at) {
706 const exp = new Date(rrow.expires_at as string | Date);
707 if (exp.getTime() > Date.now() && String(rrow.client_id) === input.clientId) {
708 return {
709 active: true,
710 token_type: 'refresh_token',
711 client_id: input.clientId,
712 sub: String(rrow.user_id),
713 scope: String(rrow.scope),
714 exp: Math.floor(exp.getTime() / 1000),
715 };
716 }
717 }
718
719 try {
720 const payload = await verifyOidcAccessToken(input.token);
721 if (payload.client_id && payload.client_id !== input.clientId) {
722 return { active: false };
723 }
724 return {
725 active: true,
726 token_type: 'access_token',
727 client_id: payload.client_id ?? input.clientId,
728 sub: payload.sub,
729 scope: payload.scope,
730 exp: payload.exp,
731 iat: payload.iat,
732 iss: payload.iss,
733 };
734 } catch {
735 return { active: false };
736 }
737}
738
739export function discoveryDocument(): Record<string, unknown> {
740 const iss = oidcIssuer();
741 return {
742 issuer: iss,
743 authorization_endpoint: `${iss}/authorize`,
744 token_endpoint: `${iss}/token`,
745 userinfo_endpoint: `${iss}/userinfo`,
746 jwks_uri: `${iss}/jwks.json`,
747 revocation_endpoint: `${iss}/revoke`,
748 introspection_endpoint: `${iss}/introspect`,
749 end_session_endpoint: `${iss}/end_session`,
750 response_types_supported: ['code'],
751 grant_types_supported: ['authorization_code', 'refresh_token'],
752 subject_types_supported: ['public'],
753 id_token_signing_alg_values_supported: ['RS256'],
754 token_endpoint_auth_methods_supported: [
755 'client_secret_post',
756 'client_secret_basic',
757 'none',
758 ],
759 code_challenge_methods_supported: ['S256', 'plain'],
760 scopes_supported: ['openid', 'profile', 'email', 'offline_access'],
761 claims_supported: [
762 'sub',
763 'iss',
764 'aud',
765 'exp',
766 'iat',
767 'email',
768 'email_verified',
769 'name',
770 'nonce',
771 ],
772 request_parameter_supported: false,
773 engine: 'briven-engine',
774 };
775}
776