@maronn-openid-connect/cli 0.4.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +102 -5
  2. package/dist/features.d.ts +9 -3
  3. package/dist/features.d.ts.map +1 -1
  4. package/dist/features.js +28 -2
  5. package/dist/features.js.map +1 -1
  6. package/dist/frameworks/express/index.d.ts.map +1 -1
  7. package/dist/frameworks/express/index.js +1 -1
  8. package/dist/frameworks/express/index.js.map +1 -1
  9. package/dist/frameworks/fastify/index.d.ts.map +1 -1
  10. package/dist/frameworks/fastify/index.js +1 -1
  11. package/dist/frameworks/fastify/index.js.map +1 -1
  12. package/dist/frameworks/hono/index.d.ts.map +1 -1
  13. package/dist/frameworks/hono/index.js +37 -8
  14. package/dist/frameworks/hono/index.js.map +1 -1
  15. package/dist/frameworks/hono/pages.d.ts +10 -0
  16. package/dist/frameworks/hono/pages.d.ts.map +1 -0
  17. package/dist/frameworks/hono/pages.js +806 -0
  18. package/dist/frameworks/hono/pages.js.map +1 -0
  19. package/dist/frameworks/hono/templates.d.ts +21 -9
  20. package/dist/frameworks/hono/templates.d.ts.map +1 -1
  21. package/dist/frameworks/hono/templates.js +13311 -8856
  22. package/dist/frameworks/hono/templates.js.map +1 -1
  23. package/dist/frameworks/nextjs/index.d.ts.map +1 -1
  24. package/dist/frameworks/nextjs/index.js +1 -1
  25. package/dist/frameworks/nextjs/index.js.map +1 -1
  26. package/dist/frameworks/types.d.ts +1 -0
  27. package/dist/frameworks/types.d.ts.map +1 -1
  28. package/dist/frameworks/web-standard/templates.d.ts +6 -6
  29. package/dist/frameworks/web-standard/templates.d.ts.map +1 -1
  30. package/dist/frameworks/web-standard/templates.js +332 -56
  31. package/dist/frameworks/web-standard/templates.js.map +1 -1
  32. package/dist/generator.d.ts +1 -0
  33. package/dist/generator.d.ts.map +1 -1
  34. package/dist/generator.js +2 -2
  35. package/dist/generator.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +130 -20
  38. package/dist/index.js.map +1 -1
  39. package/dist/scopes.d.ts +5 -0
  40. package/dist/scopes.d.ts.map +1 -0
  41. package/dist/scopes.js +38 -0
  42. package/dist/scopes.js.map +1 -0
  43. package/package.json +4 -3
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_FEATURES } from '../../features.js';
2
- import { authorizationCodeConformanceHelper, authorizeRouteTemplate, configTemplate, conformanceTestClientsBlock, consentDecisionConformanceBlock, consentWithdrawalConformanceBlock, consentRouteTemplate, customViewConformanceTestBlock, deviceAuthorizationConformanceBlock, deviceAuthorizationRouteTemplate, deviceVerificationRouteTemplate, discoveryRouteTemplate, endpointBehaviorConformanceBlock, featureDisabledDiscoveryConformanceTests, idTokenHintConformanceBlock, internalRedirectOriginConformanceBlock, introspectionConformanceBlock, introspectionRouteTemplate, jwksRouteTemplate, loginRouteTemplate, parRouteTemplate, parConformanceBlock, jarmConformanceBlock, jarmConfigTemplate, tokenExchangeConformanceBlock, idJagConformanceBlock, pkceDisabledConformanceBlock, persistentStorageConformanceBlock, requestObjectConformanceBeforeAll, requestObjectConformanceModuleSetup, resolversTemplate, onlineRefreshTokenConformanceBlock, onlineRefreshTokenConformanceStoreImport, reuseFlowConformanceTestBlock, revocationDisabledConformanceBlock, revocationRouteTemplate, scopesSupportedConformanceTest, storeTemplate, tokenEndpointAuthMethodsConformanceBlock, tokenRouteTemplate, transactionBindingConformanceBlock, userinfoRouteTemplate, viewsTemplate, } from '../hono/templates.js';
2
+ import { authorizationCodeConformanceHelper, cibaConformanceBlock, backchannelAuthenticationRouteTemplate, cibaVerificationRouteTemplate, authorizeRouteTemplate, configTemplate, conformanceTestClientsBlock, consentDecisionConformanceBlock, consentWithdrawalConformanceBlock, consentRouteTemplate, customScopeConformanceBlock, customScopesTemplate, customViewConformanceTestBlock, deviceAuthorizationConformanceBlock, deviceAuthorizationRouteTemplate, deviceVerificationRouteTemplate, endSessionRouteTemplate, rpInitiatedLogoutConformanceBlock, discoveryRouteTemplate, endpointBehaviorConformanceBlock, featureDisabledDiscoveryConformanceTests, idTokenHintConformanceBlock, internalRedirectOriginConformanceBlock, introspectionConformanceBlock, introspectionRouteTemplate, jwksRouteTemplate, loginRouteTemplate, parRouteTemplate, parConformanceBlock, jarmConformanceBlock, jarmConfigTemplate, jwtIntrospectionResponseConformanceBlock, googleLoginConformanceBlock, googleLoginConformanceImportsBlock, GOOGLE_LOGIN_PACKAGE, tokenExchangeConformanceBlock, idJagConformanceBlock, pkceDisabledConformanceBlock, persistentStorageConformanceBlock, requestObjectConformanceBeforeAll, requestObjectConformanceModuleSetup, resolversTemplate, onlineRefreshTokenConformanceBlock, onlineRefreshTokenConformanceStoreImport, reuseFlowConformanceTestBlock, revocationDisabledConformanceBlock, revocationRouteTemplate, scopesSupportedConformanceTest, storeTemplate, tokenEndpointAuthMethodsConformanceBlock, tokenRouteTemplate, transactionBindingConformanceBlock, userinfoRouteTemplate, viewsTemplate, } from '../hono/templates.js';
3
+ import { authorizePageTemplate, cibaPageTemplate, consentPageTemplate, devicePageTemplate, errorPageTemplate, loginPageTemplate, logoutPageTemplate, respondTemplate, } from '../hono/pages.js';
3
4
  function toWebRouteTemplate(content) {
4
5
  return content
5
6
  .replace("import { Hono } from 'hono';", "import { WebRouter } from '../web-router.js';")
@@ -379,14 +380,14 @@ export function webAppTemplate(corePkg, features = DEFAULT_FEATURES) {
379
380
  : '';
380
381
  const deviceImport = features.deviceAuthorizationGrant
381
382
  ? `import { deviceAuthorizationApp } from './routes/device-authorization.js';
382
- import { deviceApp } from './routes/device.js';\n`
383
+ import { devicePage } from './pages/device.js';\n`
383
384
  : '';
384
385
  const deviceCors = features.deviceAuthorizationGrant
385
386
  ? ` app.use('/device_authorization', protectedCors);\n`
386
387
  : '';
387
388
  const deviceMount = features.deviceAuthorizationGrant
388
389
  ? ` app.route('/device_authorization', deviceAuthorizationApp);
389
- app.route('/device', deviceApp);\n`
390
+ app.route('/device', devicePage);\n`
390
391
  : '';
391
392
  const deviceStorageContext = features.deviceAuthorizationGrant
392
393
  ? ` c.set('deviceAuthorizationStore', deviceAuthorizationStore);\n`
@@ -394,6 +395,78 @@ import { deviceApp } from './routes/device.js';\n`
394
395
  const deviceStoreImport = features.deviceAuthorizationGrant
395
396
  ? ` deviceAuthorizationStore,\n`
396
397
  : '';
398
+ const cibaImport = features.ciba
399
+ ? `import { backchannelAuthenticationApp } from './routes/backchannel-authentication.js';
400
+ import { cibaPage } from './pages/ciba.js';\n`
401
+ : '';
402
+ const cibaCors = features.ciba
403
+ ? ` app.use('/backchannel_authentication', protectedCors);\n`
404
+ : '';
405
+ const cibaMount = features.ciba
406
+ ? ` app.route('/backchannel_authentication', backchannelAuthenticationApp);
407
+ app.route('/ciba', cibaPage);\n`
408
+ : '';
409
+ const cibaStorageContext = features.ciba
410
+ ? ` c.set('cibaAuthenticationRequestStore', cibaAuthenticationRequestStore);
411
+ c.set('cibaLoginTransactionStore', cibaLoginTransactionStore);
412
+ c.set('cibaUserResolver', options.cibaUserResolver ?? (async (loginHint: string) => {
413
+ const claims = await stores.userStore.getClaims(loginHint);
414
+ return claims ? { subject: claims.sub } : null;
415
+ }));\n`
416
+ : '';
417
+ const cibaStoreImport = features.ciba
418
+ ? ` cibaAuthenticationRequestStore,
419
+ cibaLoginTransactionStore,\n`
420
+ : '';
421
+ const cibaOptionsField = features.ciba
422
+ ? ` /**
423
+ * EXPERIMENTAL (CIBA Core 1.0 §7.1): resolve a login_hint to the subject the
424
+ * authentication request is for. Defaults to treating the hint as a username
425
+ * of the configured user store. Return null when no user matches.
426
+ */
427
+ cibaUserResolver?: (
428
+ loginHint: string,
429
+ ) => Promise<{ subject: string } | null> | { subject: string } | null;
430
+ `
431
+ : '';
432
+ const logoutImport = features.rpInitiatedLogout
433
+ ? `import { logoutPage } from './pages/logout.js';\n`
434
+ : '';
435
+ const logoutMount = features.rpInitiatedLogout
436
+ ? ` app.route('/logout', logoutPage);\n`
437
+ : '';
438
+ const googleLoginImport = features.googleLogin
439
+ ? `import {
440
+ getDefaultGoogleIdTokenVerifier,
441
+ type GoogleAccountResolver,
442
+ type GoogleIdTokenPayload,
443
+ type GoogleIdTokenVerifier,
444
+ } from '${GOOGLE_LOGIN_PACKAGE}';\n`
445
+ : '';
446
+ const googleLoginStorageContext = features.googleLogin
447
+ ? ` c.set('googleLoginNonceStore', stores.googleLoginNonceStore);
448
+ c.set('googleIdTokenVerifier', options.googleIdTokenVerifier ?? getDefaultGoogleIdTokenVerifier());
449
+ c.set('googleAccountResolver', options.googleAccountResolver ?? {
450
+ resolveSubject: async (account: GoogleIdTokenPayload) =>
451
+ (await stores.userStore.linkGoogleAccount(account)).sub,
452
+ });\n`
453
+ : '';
454
+ const googleLoginOptionsFields = features.googleLogin
455
+ ? ` /**
456
+ * EXTENSION (google-login): verifier for the ID token Google posts to
457
+ * /login/google. Defaults to google-auth-library (OAuth2Client.verifyIdToken)
458
+ * with a process-wide certificate cache; inject a custom one for tests or a
459
+ * proxied environment.
460
+ */
461
+ googleIdTokenVerifier?: GoogleIdTokenVerifier;
462
+ /**
463
+ * EXTENSION (google-login): map a verified Google account to the OP subject.
464
+ * Defaults to just-in-time provisioning through the user store
465
+ * (userStore.linkGoogleAccount), keyed by the Google \`sub\`.
466
+ */
467
+ googleAccountResolver?: GoogleAccountResolver;
468
+ `
469
+ : '';
397
470
  const refreshStorageContext = features.refreshToken
398
471
  ? ` c.set('refreshTokenResolver', storeResolvers.refreshTokenResolver);
399
472
  c.set('authenticationSessionResolver', storeResolvers.authenticationSessionResolver);\n`
@@ -406,13 +479,13 @@ import { deviceApp } from './routes/device.js';\n`
406
479
  ? ` c.set('revocationResolvers', storeResolvers.revocationResolvers);\n`
407
480
  : '';
408
481
  return `import { WebRouter, type WebMiddleware } from './web-router.js';
409
- import { authorizeApp } from './routes/authorize.js';
482
+ import { authorizePage } from './pages/authorize.js';
410
483
  import { tokenApp } from './routes/token.js';
411
484
  import { userinfoApp } from './routes/userinfo.js';
412
- ${introspectionImport}${revocationImport}${parImport}${deviceImport}import { jwksApp } from './routes/jwks.js';
485
+ ${introspectionImport}${revocationImport}${parImport}${deviceImport}${cibaImport}${logoutImport}import { jwksApp } from './routes/jwks.js';
413
486
  import { discoveryApp } from './routes/discovery.js';
414
- import { loginApp } from './routes/login.js';
415
- import { consentApp } from './routes/consent.js';
487
+ import { loginPage } from './pages/login.js';
488
+ import { consentPage } from './pages/consent.js';
416
489
  import {
417
490
  createInMemoryClientResolver,
418
491
  createProviderConfig,
@@ -423,10 +496,10 @@ import {
423
496
  } from './resolvers.js';
424
497
  import {
425
498
  defaultProviderStores,
426
- ${parStoreImport}${deviceStoreImport} type ProviderStores,
499
+ ${parStoreImport}${deviceStoreImport}${cibaStoreImport} type ProviderStores,
427
500
  } from './store.js';
428
501
  import { createViews, type Views } from './views.js';
429
- import {
502
+ ${googleLoginImport}import {
430
503
  assertHasRs256Key,
431
504
  assertKeyStrength,
432
505
  assertKidStrategyConsistent,
@@ -459,7 +532,7 @@ export interface OidcProviderOptions {
459
532
  storage?: ProviderStores;
460
533
  acrResolver?: AcrResolver;
461
534
  jwksProvider?: () => Promise<JwkSet> | JwkSet;
462
- corsOrigins?: CorsOrigins;
535
+ ${cibaOptionsField}${googleLoginOptionsFields} corsOrigins?: CorsOrigins;
463
536
  /**
464
537
  * Custom UI for the login / consent / error pages.
465
538
  * Provide any subset; omitted pages fall back to the default views.
@@ -497,7 +570,7 @@ export function createApp(options: OidcProviderOptions): WebRouter {
497
570
  });
498
571
  app.use('/token', protectedCors);
499
572
  app.use('/userinfo', protectedCors);
500
- ${introspectionCors}${revocationCors}${parCors}${deviceCors} app.use('/.well-known/openid-configuration', publicCors);
573
+ ${introspectionCors}${revocationCors}${parCors}${deviceCors}${cibaCors} app.use('/.well-known/openid-configuration', publicCors);
501
574
  app.use('/.well-known/jwks.json', publicCors);
502
575
 
503
576
  app.use('*', async (c, next) => {
@@ -555,7 +628,7 @@ ${introspectionCors}${revocationCors}${parCors}${deviceCors} app.use('/.well-kn
555
628
  c.set('authCodeResolver', storeResolvers.authorizationCodeResolver);
556
629
  c.set('accessTokenResolver', storeResolvers.accessTokenResolver);
557
630
  c.set('userClaimsResolver', storeResolvers.userClaimsResolver);
558
- ${refreshStorageContext}${introspectionStorageContext}${revocationStorageContext}${parStorageContext}${deviceStorageContext}
631
+ ${refreshStorageContext}${introspectionStorageContext}${revocationStorageContext}${parStorageContext}${deviceStorageContext}${cibaStorageContext}${googleLoginStorageContext}
559
632
  if (options.acrResolver) {
560
633
  c.set('acrResolver', options.acrResolver);
561
634
  }
@@ -570,13 +643,15 @@ ${refreshStorageContext}${introspectionStorageContext}${revocationStorageContext
570
643
  await next();
571
644
  });
572
645
 
573
- app.route('/authorize', authorizeApp);
646
+ // Browser-facing surfaces are mounted from pages/: every GET and POST of a
647
+ // screen lives there, and the logic they call is in routes/.
648
+ app.route('/authorize', authorizePage);
574
649
  app.route('/token', tokenApp);
575
650
  app.route('/userinfo', userinfoApp);
576
- ${introspectionMount}${revocationMount}${parMount}${deviceMount} app.route('/.well-known/jwks.json', jwksApp);
651
+ ${introspectionMount}${revocationMount}${parMount}${deviceMount}${cibaMount}${logoutMount} app.route('/.well-known/jwks.json', jwksApp);
577
652
  app.route('/.well-known/openid-configuration', discoveryApp);
578
- app.route('/login', loginApp);
579
- app.route('/consent', consentApp);
653
+ app.route('/login', loginPage);
654
+ app.route('/consent', consentPage);
580
655
 
581
656
  return app;
582
657
  }
@@ -629,6 +704,10 @@ export function expressApplyTemplate(features = DEFAULT_FEATURES) {
629
704
  ? ` '/device_authorization',
630
705
  '/device',\n`
631
706
  : '';
707
+ const cibaEndpoints = features.ciba
708
+ ? ` '/backchannel_authentication',
709
+ '/ciba',\n`
710
+ : '';
632
711
  return `import type { Express } from 'express';
633
712
  import type { Request, Response, NextFunction } from 'express';
634
713
  import { createApp, type OidcProviderOptions } from './app.js';
@@ -640,7 +719,7 @@ const OIDC_ENDPOINTS = [
640
719
  '/authorize',
641
720
  '/token',
642
721
  '/userinfo',
643
- ${introspectionEndpoint}${revocationEndpoint}${parEndpoint}${deviceEndpoints} '/.well-known/jwks.json',
722
+ ${introspectionEndpoint}${revocationEndpoint}${parEndpoint}${deviceEndpoints}${cibaEndpoints} '/.well-known/jwks.json',
644
723
  '/.well-known/openid-configuration',
645
724
  '/login',
646
725
  '/consent',
@@ -682,6 +761,15 @@ export function fastifyApplyTemplate(features = DEFAULT_FEATURES) {
682
761
  app.route({ method: ['POST'], url: '/device/login', handler: handle });
683
762
  app.route({ method: ['POST'], url: '/device/approve', handler: handle });\n`
684
763
  : '';
764
+ const cibaRoutes = features.ciba
765
+ ? ` app.route({ method: ['POST', 'OPTIONS'], url: '/backchannel_authentication', handler: handle });
766
+ app.route({ method: ['GET'], url: '/ciba', handler: handle });
767
+ app.route({ method: ['POST'], url: '/ciba/login', handler: handle });
768
+ app.route({ method: ['POST'], url: '/ciba/approve', handler: handle });\n`
769
+ : '';
770
+ const googleLoginRoute = features.googleLogin
771
+ ? ` app.route({ method: ['POST'], url: '/login/google', handler: handle });\n`
772
+ : '';
685
773
  return `import type { FastifyInstance } from 'fastify';
686
774
  import type { FastifyReply, FastifyRequest } from 'fastify';
687
775
  import { createApp, type OidcProviderOptions } from './app.js';
@@ -720,10 +808,10 @@ export async function applyOidc(app: FastifyInstance, options: ApplyOidcOptions)
720
808
  app.route({ method: ['GET', 'POST', 'OPTIONS'], url: '/authorize', handler: handle });
721
809
  app.route({ method: ['POST', 'OPTIONS'], url: '/token', handler: handle });
722
810
  app.route({ method: ['GET', 'POST', 'OPTIONS'], url: '/userinfo', handler: handle });
723
- ${introspectionRoute}${revocationRoute}${parRoute}${deviceRoutes} app.route({ method: ['GET', 'OPTIONS'], url: '/.well-known/jwks.json', handler: handle });
811
+ ${introspectionRoute}${revocationRoute}${parRoute}${deviceRoutes}${cibaRoutes} app.route({ method: ['GET', 'OPTIONS'], url: '/.well-known/jwks.json', handler: handle });
724
812
  app.route({ method: ['GET', 'OPTIONS'], url: '/.well-known/openid-configuration', handler: handle });
725
813
  app.route({ method: ['GET', 'POST'], url: '/login', handler: handle });
726
- app.route({ method: ['GET', 'POST'], url: '/consent', handler: handle });
814
+ ${googleLoginRoute} app.route({ method: ['GET', 'POST'], url: '/consent', handler: handle });
727
815
  }
728
816
 
729
817
  async function toFastifyReply(reply: FastifyReply, response: Response): Promise<void> {
@@ -979,14 +1067,33 @@ function readEnv(name: string): string | undefined {
979
1067
  }
980
1068
  `;
981
1069
  }
982
- export function nextJsRuntimeTemplate(corePkg) {
1070
+ export function nextJsRuntimeTemplate(corePkg, features = DEFAULT_FEATURES) {
1071
+ const googleLoginConfigTypeImport = features.googleLogin ? 'type GoogleLoginConfig, ' : '';
1072
+ const googleLoginRuntimeConfig = features.googleLogin
1073
+ ? `
1074
+ // EXTENSION (google-login): set GOOGLE_CLIENT_ID (the OAuth client ID from
1075
+ // the Google Cloud console) to render the "Sign in with Google" button.
1076
+ // Register <issuer>/login/google as an authorized redirect URI there.
1077
+ // GOOGLE_HOSTED_DOMAIN optionally restricts sign-in to one Workspace domain.
1078
+ googleLogin: readGoogleLoginConfig(),`
1079
+ : '';
1080
+ const googleLoginRuntimeReader = features.googleLogin
1081
+ ? `
1082
+ function readGoogleLoginConfig(): GoogleLoginConfig | undefined {
1083
+ const clientId = readEnv('GOOGLE_CLIENT_ID');
1084
+ if (!clientId) return undefined;
1085
+ const hostedDomain = readEnv('GOOGLE_HOSTED_DOMAIN');
1086
+ return hostedDomain ? { clientId, hostedDomain } : { clientId };
1087
+ }
1088
+ `
1089
+ : '';
983
1090
  return `import {
984
1091
  createCachedSigningKeyProvider,
985
1092
  type AcrResolver,
986
1093
  type SigningKey,
987
1094
  type SigningKeyProvider,
988
1095
  } from '${corePkg}';
989
- import { createInMemoryClientResolver, type RegisteredClient } from './config';
1096
+ import { createInMemoryClientResolver, ${googleLoginConfigTypeImport}type RegisteredClient } from './config';
990
1097
  import { createOidcRouteHandlers } from './next';
991
1098
  import { createNextJsProviderStores } from './storage-backend';
992
1099
  import type { OidcProviderOptions } from './app';
@@ -1039,7 +1146,7 @@ export function createOidcProviderOptions(): OidcProviderOptions {
1039
1146
  // /oidc-error, which renders them via the App Router error boundary
1040
1147
  // (app/oidc-error/error.tsx) — consistent with login/consent being real
1041
1148
  // pages rather than HTML strings from the route handler.
1042
- authorizationErrorRedirectPath: '/oidc-error',
1149
+ authorizationErrorRedirectPath: '/oidc-error',${googleLoginRuntimeConfig}
1043
1150
  },
1044
1151
  signingKeyProvider,
1045
1152
  clientResolver,
@@ -1120,7 +1227,7 @@ function readEnv(name: string): string | undefined {
1120
1227
  if (typeof process === 'undefined') return undefined;
1121
1228
  return process.env[name];
1122
1229
  }
1123
-
1230
+ ${googleLoginRuntimeReader}
1124
1231
  function createEphemeralRs256KeyProvider(): SigningKeyProvider {
1125
1232
  const keyPromise = generateSigningKey();
1126
1233
  return {
@@ -1222,13 +1329,70 @@ async function isBoundToThisBrowser(
1222
1329
  }
1223
1330
  `
1224
1331
  : '';
1332
+ const googleLoginImports = features.googleLogin
1333
+ ? `
1334
+ import Script from 'next/script';
1335
+ import { issueGoogleLoginNonce } from '${GOOGLE_LOGIN_PACKAGE}';
1336
+ import {
1337
+ buildGoogleSignInAttributes,
1338
+ GOOGLE_GSI_CLIENT_SCRIPT_URL,
1339
+ } from '${GOOGLE_LOGIN_PACKAGE}/sign-in';`
1340
+ : '';
1341
+ const loginPageStores = features.googleLogin
1342
+ ? `const { transactionStore, googleLoginNonceStore } =
1343
+ oidcProviderOptions.storage ?? defaultProviderStores;`
1344
+ : `const transactionStore =
1345
+ (oidcProviderOptions.storage ?? defaultProviderStores).transactionStore;`;
1346
+ const googleSignInRender = features.googleLogin
1347
+ ? `
1348
+ // EXTENSION (google-login): the GIS configuration (g_id_onload attributes),
1349
+ // built only when config.googleLogin is set. Each render issues a fresh nonce
1350
+ // bound to this transaction; Google echoes it in the ID token, which is how
1351
+ // login/google/route.ts finds the transaction. The JSX below owns the markup.
1352
+ const googleLogin = oidcProviderOptions.config?.googleLogin;
1353
+ const googleSignIn = googleLogin
1354
+ ? buildGoogleSignInAttributes({
1355
+ clientId: googleLogin.clientId,
1356
+ // Must equal an authorized redirect URI of the Google OAuth client.
1357
+ loginUri: new URL(
1358
+ '/login/google',
1359
+ oidcProviderOptions.config?.issuer ?? 'http://localhost:3000',
1360
+ ).toString(),
1361
+ nonce: await issueGoogleLoginNonce({
1362
+ transactionId,
1363
+ expiresAt: transaction.expiresAt,
1364
+ store: googleLoginNonceStore,
1365
+ }),
1366
+ loginHint: transaction.loginHint,
1367
+ hostedDomain:
1368
+ typeof googleLogin.hostedDomain === 'string' ? googleLogin.hostedDomain : undefined,
1369
+ })
1370
+ : undefined;
1371
+ `
1372
+ : '';
1373
+ const googleSignInJsx = features.googleLogin
1374
+ ? `
1375
+ {/*
1376
+ EXTENSION (google-login): the three elements GIS needs for redirect mode.
1377
+ googleSignIn holds the g_id_onload attributes (data-ux_mode="redirect",
1378
+ data-login_uri, data-nonce, ...) and spreads straight onto the element;
1379
+ GIS replaces .g_id_signin with the button — style it through the GIS
1380
+ button attributes (data-theme, data-size, data-text, ...).
1381
+ */}
1382
+ {googleSignIn ? (
1383
+ <section aria-label="Sign in with Google">
1384
+ <Script src={GOOGLE_GSI_CLIENT_SCRIPT_URL} strategy="afterInteractive" />
1385
+ <div {...googleSignIn} />
1386
+ <div className="g_id_signin" data-type="standard" />
1387
+ </section>
1388
+ ) : null}`
1389
+ : '';
1225
1390
  return `${bindingImports}
1226
1391
  import { oidcProviderOptions } from '../_oidc-provider/runtime';
1227
- ${bindingStoreImport}
1392
+ ${bindingStoreImport}${googleLoginImports}
1228
1393
  import { loginAction } from './actions';
1229
1394
 
1230
- const transactionStore =
1231
- (oidcProviderOptions.storage ?? defaultProviderStores).transactionStore;
1395
+ ${loginPageStores}
1232
1396
 
1233
1397
  // Authorization redirects here with a per-request transaction_id, so the page
1234
1398
  // must always render dynamically (never statically cached).
@@ -1273,7 +1437,7 @@ export default async function LoginPage({ searchParams }: LoginPageProps) {
1273
1437
  }
1274
1438
 
1275
1439
  const transaction = await getAuthTransaction(transactionId, transactionStore);
1276
- ${bindingCheck}
1440
+ ${bindingCheck}${googleSignInRender}
1277
1441
  const errorMessage =
1278
1442
  error === 'invalid_credentials'
1279
1443
  ? \`Invalid credentials\${remaining ? \`. Attempts remaining: \${remaining}\` : ''}\`
@@ -1299,7 +1463,7 @@ ${bindingCheck}
1299
1463
  <input type="password" id="password" name="password" required />
1300
1464
  </div>
1301
1465
  <button type="submit">Login</button>
1302
- </form>
1466
+ </form>${googleSignInJsx}
1303
1467
  </main>
1304
1468
  );
1305
1469
  }
@@ -1481,7 +1645,28 @@ ${bindingCheck} validateCsrfToken(transaction, csrfToken);
1481
1645
  }
1482
1646
  `;
1483
1647
  }
1484
- export function nextJsConsentPageTemplate(corePkg, features = DEFAULT_FEATURES) {
1648
+ export function nextJsConsentPageTemplate(corePkg, features = DEFAULT_FEATURES, scopes = []) {
1649
+ const customScopesDeclared = scopes.length > 0;
1650
+ const customScopeImport = customScopesDeclared
1651
+ ? `
1652
+ import { resolveGrantableScopes } from '../_oidc-provider/scopes';`
1653
+ : '';
1654
+ const consentPageScopes = customScopesDeclared
1655
+ ? ` // The subject comes from the auth session that the login step stored for this
1656
+ // transaction; without one there is nothing to apply the scope policy to, so
1657
+ // the request is shown as-is and the Server Action stops on the same missing
1658
+ // session.
1659
+ const consentSession = await authSessionStore.get(transactionId);
1660
+ const requestedScopes = transaction.scope.split(' ').filter(Boolean);
1661
+ const scopes = consentSession
1662
+ ? await resolveGrantableScopes(requestedScopes, consentSession.subject)
1663
+ : requestedScopes;`
1664
+ : ` const scopes = transaction.scope.split(' ').filter(Boolean);`;
1665
+ const consentPageStores = customScopesDeclared
1666
+ ? `
1667
+ const authSessionStore =
1668
+ (oidcProviderOptions.storage ?? defaultProviderStores).authSessionStore;`
1669
+ : '';
1485
1670
  const bindingImports = features.transactionBinding
1486
1671
  ? `import { cookies } from 'next/headers';
1487
1672
  import { getAuthTransaction, validateTransactionBinding } from '${corePkg}';`
@@ -1534,10 +1719,10 @@ async function isBoundToThisBrowser(
1534
1719
  return `${bindingImports}
1535
1720
  import { oidcProviderOptions } from '../_oidc-provider/runtime';
1536
1721
  ${bindingStoreImport}
1537
- import { consentAction } from './actions';
1722
+ import { consentAction } from './actions';${customScopeImport}
1538
1723
 
1539
1724
  const transactionStore =
1540
- (oidcProviderOptions.storage ?? defaultProviderStores).transactionStore;
1725
+ (oidcProviderOptions.storage ?? defaultProviderStores).transactionStore;${consentPageStores}
1541
1726
 
1542
1727
  export const dynamic = 'force-dynamic';
1543
1728
 
@@ -1564,7 +1749,7 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) {
1564
1749
  }
1565
1750
 
1566
1751
  const transaction = await getAuthTransaction(transactionId, transactionStore);
1567
- ${bindingCheck} const scopes = transaction.scope.split(' ').filter(Boolean);
1752
+ ${bindingCheck}${consentPageScopes}
1568
1753
 
1569
1754
  return (
1570
1755
  <main>
@@ -1600,7 +1785,23 @@ ${bindingCheck} const scopes = transaction.scope.split(' ').filter(Boolean);
1600
1785
  }
1601
1786
  `;
1602
1787
  }
1603
- export function nextJsConsentActionTemplate(corePkg, features = DEFAULT_FEATURES) {
1788
+ export function nextJsConsentActionTemplate(corePkg, features = DEFAULT_FEATURES, scopes = []) {
1789
+ const customScopesDeclared = scopes.length > 0;
1790
+ const customScopeImport = customScopesDeclared
1791
+ ? `
1792
+ import { resolveGrantableScopes } from '../_oidc-provider/scopes';`
1793
+ : '';
1794
+ const consentGrantedScope = customScopesDeclared
1795
+ ? `
1796
+ // Apply the scope policy (resolveGrantableScopes in _oidc-provider/scopes.ts —
1797
+ // the place to write per-user filtering). A dropped scope narrows the grant
1798
+ // rather than failing the request: RFC 6749 §3.3 lets the authorization server
1799
+ // issue a narrower scope, and the token response reports what was granted.
1800
+ const grantedScope = await resolveGrantableScopes(
1801
+ transaction.scope.split(' ').filter(Boolean),
1802
+ session.subject,
1803
+ );`
1804
+ : ` const grantedScope = transaction.scope.split(' ').filter(Boolean);`;
1604
1805
  const bindingCookiesImport = features.transactionBinding
1605
1806
  ? `
1606
1807
  import { cookies } from 'next/headers';`
@@ -1653,7 +1854,7 @@ import {
1653
1854
  } from '${corePkg}';
1654
1855
  import { oidcProviderOptions } from '../_oidc-provider/runtime';
1655
1856
  import { createStoreResolvers } from '../_oidc-provider/resolvers';
1656
- ${bindingStoreImport}
1857
+ ${bindingStoreImport}${customScopeImport}
1657
1858
 
1658
1859
  const providerStores = oidcProviderOptions.storage ?? defaultProviderStores;
1659
1860
  const { transactionStore, authCodeStore, authSessionStore } = providerStores;
@@ -1722,7 +1923,7 @@ ${clearBindingCookie} redirect(denyUrl.toString());
1722
1923
  // transaction.scope は認可リクエスト検証時に applyOfflineAccessPolicy を通した後の値。
1723
1924
  // offline_access の可否(OIDC Core 1.0 §11 の prompt=consent と、クライアント登録
1724
1925
  // grant_types に refresh_token があるか)はそこで確定しているので再フィルタしない。
1725
- const grantedScope = transaction.scope.split(' ').filter(Boolean);
1926
+ ${consentGrantedScope}
1726
1927
 
1727
1928
  // OIDC Core 1.0 Section 3.1.3.1: TTL is configurable via ProviderConfig.
1728
1929
  const authCodeData = await createAuthorizationCode({
@@ -1756,8 +1957,12 @@ ${clearBindingCookieOnSuccess} const successUrl = new URL(responseParams.redire
1756
1957
  }
1757
1958
  `;
1758
1959
  }
1759
- export function webConformanceTestTemplate(corePkg, errorPageMode = 'html', features = DEFAULT_FEATURES, includeNodeAdapterContract = false, jarmConsentResponseMode = 'jwt') {
1960
+ export function webConformanceTestTemplate(corePkg, errorPageMode = 'html', features = DEFAULT_FEATURES, includeNodeAdapterContract = false, jarmConsentResponseMode = 'jwt', scopes = []) {
1760
1961
  const usesRedirect = errorPageMode === 'redirect';
1962
+ const customScopeConformanceImport = scopes.length > 0
1963
+ ? `
1964
+ import { RESTRICTED_SCOPE_SUBJECTS } from './scopes.js';`
1965
+ : '';
1761
1966
  const createAppConfig = usesRedirect
1762
1967
  ? `
1763
1968
  config: { authorizationErrorRedirectPath: '/oidc-error' },`
@@ -1885,14 +2090,27 @@ import { tokenExchangeConfig } from './routes/token.js';`
1885
2090
  ? `
1886
2091
  import { idJagConfig } from './routes/token.js';`
1887
2092
  : '';
1888
- return `import { describe, it, expect, beforeAll } from 'vitest';
2093
+ const rpInitiatedLogoutConformanceImports = features.rpInitiatedLogout
2094
+ ? `
2095
+ import { rpInitiatedLogoutConfig } from './routes/logout.js';`
2096
+ : '';
2097
+ const cibaConformanceImports = features.ciba
2098
+ ? `
2099
+ import { cibaAuthenticationRequestStore } from './store.js';`
2100
+ : '';
2101
+ const vitestNames = features.ciba
2102
+ ? 'describe, it, expect, beforeAll, afterEach'
2103
+ : 'describe, it, expect, beforeAll';
2104
+ const googleLoginConformanceImports = googleLoginConformanceImportsBlock(features);
2105
+ const googleLoginConfigTypeImport = features.googleLogin ? ', type GoogleLoginConfig' : '';
2106
+ return `import { ${vitestNames} } from 'vitest';
1889
2107
  import type { SigningKeyProvider, SigningKey } from '${corePkg}';
1890
2108
  ${exportPublicJwkImport}import { createApp, validateSigningKeySet } from './app.js';
1891
- import { createInMemoryClientResolver, type RegisteredClient } from './config.js';
2109
+ import { createInMemoryClientResolver, type RegisteredClient${googleLoginConfigTypeImport} } from './config.js';
1892
2110
  import { accessTokenStore, authSessionStore, consentStore, createJsonProviderStores,${onlineRefreshTokenConformanceStoreImport(features)} refreshTokenStore, transactionStore, type JsonStoreBackend } from './store.js';
1893
2111
  import { consentResolver } from './resolvers.js';
1894
2112
  import { defaultViews } from './views.js';
1895
- import { renderView } from './views.js';${parConformanceImports}${tokenExchangeConformanceImports}${idJagConformanceImports}
2113
+ import { renderView } from './views.js';${parConformanceImports}${tokenExchangeConformanceImports}${idJagConformanceImports}${cibaConformanceImports}${rpInitiatedLogoutConformanceImports}${googleLoginConformanceImports}${customScopeConformanceImport}
1896
2114
  ${nodeAdapterImport}
1897
2115
 
1898
2116
  const REDIRECT_URI = 'http://localhost:3000/callback';
@@ -2068,7 +2286,7 @@ ${responseModesSupportedExpectation}
2068
2286
  });
2069
2287
  });
2070
2288
 
2071
- ${scopesSupportedConformanceTest(features)}
2289
+ ${scopesSupportedConformanceTest(features, scopes)}
2072
2290
  // OIDC Core 1.0 §2 / §3.1.3.6 + Discovery 1.0 §3: claims_supported advertises
2073
2291
  // the claims the OP can supply, including the ID Token protocol claims
2074
2292
  // (auth_time/nonce/acr/amr/azp/at_hash). The full list is pinned so dropping
@@ -2303,15 +2521,18 @@ ${nonRedirectErrorTest}
2303
2521
  });
2304
2522
  });
2305
2523
  });
2306
- ${transactionBindingConformanceBlock(features)}${customViewConformanceTestBlock()}${internalRedirectOriginConformanceBlock()}${endpointBehaviorConformanceBlock(features)}${idTokenHintConformanceBlock()}${consentWithdrawalConformanceBlock(features)}${reuseFlowConformanceTestBlock(features)}${onlineRefreshTokenConformanceBlock(features)}${revocationDisabledConformanceBlock(features)}${tokenEndpointAuthMethodsConformanceBlock()}${pkceDisabledConformanceBlock(features)}${parConformanceBlock(features)}${tokenExchangeConformanceBlock(features)}${idJagConformanceBlock(features)}${deviceAuthorizationConformanceBlock(features)}${jarmConformanceBlock(features, jarmConsentResponseMode)}${consentDecisionConformanceBlock()}});
2524
+ ${transactionBindingConformanceBlock(features)}${customViewConformanceTestBlock()}${internalRedirectOriginConformanceBlock()}${endpointBehaviorConformanceBlock(features)}${idTokenHintConformanceBlock()}${consentWithdrawalConformanceBlock(features)}${reuseFlowConformanceTestBlock(features, errorPageMode)}${onlineRefreshTokenConformanceBlock(features)}${revocationDisabledConformanceBlock(features)}${tokenEndpointAuthMethodsConformanceBlock()}${pkceDisabledConformanceBlock(features)}${parConformanceBlock(features)}${tokenExchangeConformanceBlock(features)}${idJagConformanceBlock(features)}${deviceAuthorizationConformanceBlock(features)}${cibaConformanceBlock(features)}${jarmConformanceBlock(features, jarmConsentResponseMode)}${jwtIntrospectionResponseConformanceBlock(features)}${rpInitiatedLogoutConformanceBlock(features)}${googleLoginConformanceBlock(features)}${consentDecisionConformanceBlock()}${customScopeConformanceBlock(scopes)}});
2307
2525
  `;
2308
2526
  }
2309
- function webCoreGeneratedFiles(corePkg, errorPageMode = 'html', features = DEFAULT_FEATURES, includeNodeAdapterContract = false, jarmConsentResponseMode = 'jwt') {
2527
+ function webCoreGeneratedFiles(corePkg, errorPageMode = 'html', features = DEFAULT_FEATURES, includeNodeAdapterContract = false, jarmConsentResponseMode = 'jwt', scopes = []) {
2310
2528
  const consentFeatures = jarmConsentResponseMode === 'plain' ? { ...features, jarm: false } : features;
2311
2529
  return [
2312
2530
  { path: 'app.ts', content: webAppTemplate(corePkg, features) },
2313
2531
  { path: 'web-router.ts', content: webRouterTemplate() },
2314
2532
  { path: 'config.ts', content: configTemplate(corePkg, features) },
2533
+ ...(scopes.length > 0
2534
+ ? [{ path: 'scopes.ts', content: customScopesTemplate(scopes, features) }]
2535
+ : []),
2315
2536
  {
2316
2537
  path: 'store.ts',
2317
2538
  content: storeTemplate(corePkg, features),
@@ -2321,11 +2542,23 @@ function webCoreGeneratedFiles(corePkg, errorPageMode = 'html', features = DEFAU
2321
2542
  content: resolversTemplate(corePkg, features).replace('through Hono context', 'through the generated request context'),
2322
2543
  },
2323
2544
  { path: 'views.ts', content: viewsTemplate(features) },
2324
- { path: 'routes/authorize.ts', content: toWebRouteTemplate(authorizeRouteTemplate(corePkg, features)) },
2545
+ { path: 'pages/respond.ts', content: respondTemplate() },
2546
+ { path: 'pages/errors.ts', content: errorPageTemplate() },
2547
+ { path: 'pages/authorize.ts', content: toWebRouteTemplate(authorizePageTemplate()) },
2548
+ { path: 'pages/login.ts', content: toWebRouteTemplate(loginPageTemplate(features)) },
2549
+ { path: 'pages/consent.ts', content: toWebRouteTemplate(consentPageTemplate()) },
2550
+ ...(features.deviceAuthorizationGrant
2551
+ ? [{ path: 'pages/device.ts', content: toWebRouteTemplate(devicePageTemplate()) }]
2552
+ : []),
2553
+ ...(features.ciba ? [{ path: 'pages/ciba.ts', content: toWebRouteTemplate(cibaPageTemplate()) }] : []),
2554
+ ...(features.rpInitiatedLogout
2555
+ ? [{ path: 'pages/logout.ts', content: toWebRouteTemplate(logoutPageTemplate()) }]
2556
+ : []),
2557
+ { path: 'routes/authorize.ts', content: toWebRouteTemplate(authorizeRouteTemplate(corePkg, features, scopes)) },
2325
2558
  { path: 'routes/token.ts', content: toWebRouteTemplate(tokenRouteTemplate(corePkg, features)) },
2326
2559
  { path: 'routes/userinfo.ts', content: toWebRouteTemplate(userinfoRouteTemplate(corePkg)) },
2327
2560
  ...(features.introspection
2328
- ? [{ path: 'routes/introspection.ts', content: toWebRouteTemplate(introspectionRouteTemplate(corePkg)) }]
2561
+ ? [{ path: 'routes/introspection.ts', content: toWebRouteTemplate(introspectionRouteTemplate(corePkg, features)) }]
2329
2562
  : []),
2330
2563
  ...(features.revocation
2331
2564
  ? [{ path: 'routes/revocation.ts', content: toWebRouteTemplate(revocationRouteTemplate(corePkg)) }]
@@ -2337,39 +2570,54 @@ function webCoreGeneratedFiles(corePkg, errorPageMode = 'html', features = DEFAU
2337
2570
  ? [
2338
2571
  {
2339
2572
  path: 'routes/device-authorization.ts',
2340
- content: toWebRouteTemplate(deviceAuthorizationRouteTemplate(corePkg, features)),
2573
+ content: toWebRouteTemplate(deviceAuthorizationRouteTemplate(corePkg, features, scopes)),
2341
2574
  },
2342
2575
  {
2343
2576
  path: 'routes/device.ts',
2344
- content: toWebRouteTemplate(deviceVerificationRouteTemplate(corePkg)),
2577
+ content: toWebRouteTemplate(deviceVerificationRouteTemplate(corePkg, scopes)),
2578
+ },
2579
+ ]
2580
+ : []),
2581
+ ...(features.ciba
2582
+ ? [
2583
+ {
2584
+ path: 'routes/backchannel-authentication.ts',
2585
+ content: toWebRouteTemplate(backchannelAuthenticationRouteTemplate(corePkg, features, scopes)),
2586
+ },
2587
+ {
2588
+ path: 'routes/ciba-verification.ts',
2589
+ content: toWebRouteTemplate(cibaVerificationRouteTemplate(corePkg, scopes)),
2345
2590
  },
2346
2591
  ]
2347
2592
  : []),
2593
+ ...(features.rpInitiatedLogout
2594
+ ? [{ path: 'routes/logout.ts', content: toWebRouteTemplate(endSessionRouteTemplate(corePkg)) }]
2595
+ : []),
2348
2596
  ...(features.jarm
2349
2597
  ? [{ path: 'routes/jarm.ts', content: jarmConfigTemplate() }]
2350
2598
  : []),
2351
2599
  { path: 'routes/jwks.ts', content: toWebRouteTemplate(jwksRouteTemplate(corePkg)) },
2352
- { path: 'routes/discovery.ts', content: toWebRouteTemplate(discoveryRouteTemplate(corePkg, features)) },
2600
+ { path: 'routes/discovery.ts', content: toWebRouteTemplate(discoveryRouteTemplate(corePkg, features, scopes)) },
2353
2601
  { path: 'routes/login.ts', content: toWebRouteTemplate(loginRouteTemplate(corePkg, features)) },
2354
- { path: 'routes/consent.ts', content: toWebRouteTemplate(consentRouteTemplate(corePkg, consentFeatures)) },
2602
+ { path: 'routes/consent.ts', content: toWebRouteTemplate(consentRouteTemplate(corePkg, consentFeatures, scopes)) },
2355
2603
  {
2356
2604
  path: 'conformance.test.ts',
2357
- content: webConformanceTestTemplate(corePkg, errorPageMode, features, includeNodeAdapterContract, jarmConsentResponseMode),
2605
+ content: webConformanceTestTemplate(corePkg, errorPageMode, features, includeNodeAdapterContract, jarmConsentResponseMode, scopes),
2358
2606
  },
2359
2607
  ];
2360
2608
  }
2361
2609
  function toNextJsModuleImports(content) {
2362
2610
  return content.replaceAll(/(from\s+['"](?:\.{1,2}\/[^'"]+))\.js(['"])/g, '$1$2');
2363
2611
  }
2364
- export function webGeneratedFiles(corePkg, applyTemplate, features = DEFAULT_FEATURES) {
2612
+ export function webGeneratedFiles(corePkg, applyTemplate, features = DEFAULT_FEATURES, scopes = []) {
2365
2613
  return [
2366
- ...webCoreGeneratedFiles(corePkg, 'html', features, true),
2614
+ ...webCoreGeneratedFiles(corePkg, 'html', features, true, 'jwt', scopes),
2367
2615
  { path: 'apply.ts', content: applyTemplate },
2368
2616
  { path: 'node-adapter.ts', content: nodeAdapterTemplate() },
2369
2617
  ];
2370
2618
  }
2371
- export function nextJsGeneratedFiles(corePkg, features = DEFAULT_FEATURES) {
2372
- const internalFiles = webCoreGeneratedFiles(corePkg, 'redirect', features, false, 'plain').map((file) => ({
2619
+ export function nextJsGeneratedFiles(corePkg, features = DEFAULT_FEATURES, scopes = []) {
2620
+ const internalFiles = webCoreGeneratedFiles(corePkg, 'redirect', features, false, 'plain', scopes).map((file) => ({
2373
2621
  path: `_oidc-provider/${file.path}`,
2374
2622
  content: toNextJsModuleImports(file.content),
2375
2623
  }));
@@ -2377,7 +2625,7 @@ export function nextJsGeneratedFiles(corePkg, features = DEFAULT_FEATURES) {
2377
2625
  ...internalFiles,
2378
2626
  { path: '_oidc-provider/next.ts', content: nextJsRouteHandlerTemplate() },
2379
2627
  { path: '_oidc-provider/storage-backend.ts', content: nextJsStorageBackendTemplate() },
2380
- { path: '_oidc-provider/runtime.ts', content: nextJsRuntimeTemplate(corePkg) },
2628
+ { path: '_oidc-provider/runtime.ts', content: nextJsRuntimeTemplate(corePkg, features) },
2381
2629
  {
2382
2630
  path: 'authorize/route.ts',
2383
2631
  content: nextJsEndpointRouteTemplate('../_oidc-provider/runtime', [
@@ -2442,6 +2690,26 @@ export function nextJsGeneratedFiles(corePkg, features = DEFAULT_FEATURES) {
2442
2690
  },
2443
2691
  ]
2444
2692
  : []),
2693
+ ...(features.ciba
2694
+ ? [
2695
+ {
2696
+ path: 'backchannel_authentication/route.ts',
2697
+ content: nextJsEndpointRouteTemplate('../_oidc-provider/runtime', ['POST', 'OPTIONS']),
2698
+ },
2699
+ {
2700
+ path: 'ciba/route.ts',
2701
+ content: nextJsEndpointRouteTemplate('../_oidc-provider/runtime', ['GET']),
2702
+ },
2703
+ {
2704
+ path: 'ciba/login/route.ts',
2705
+ content: nextJsEndpointRouteTemplate('../../_oidc-provider/runtime', ['POST']),
2706
+ },
2707
+ {
2708
+ path: 'ciba/approve/route.ts',
2709
+ content: nextJsEndpointRouteTemplate('../../_oidc-provider/runtime', ['POST']),
2710
+ },
2711
+ ]
2712
+ : []),
2445
2713
  {
2446
2714
  path: '.well-known/jwks.json/route.ts',
2447
2715
  content: nextJsEndpointRouteTemplate('../../_oidc-provider/runtime', [
@@ -2458,8 +2726,16 @@ export function nextJsGeneratedFiles(corePkg, features = DEFAULT_FEATURES) {
2458
2726
  },
2459
2727
  { path: 'login/page.tsx', content: nextJsLoginPageTemplate(corePkg, features) },
2460
2728
  { path: 'login/actions.ts', content: nextJsLoginActionTemplate(corePkg, features) },
2461
- { path: 'consent/page.tsx', content: nextJsConsentPageTemplate(corePkg, features) },
2462
- { path: 'consent/actions.ts', content: nextJsConsentActionTemplate(corePkg, features) },
2729
+ ...(features.googleLogin
2730
+ ? [
2731
+ {
2732
+ path: 'login/google/route.ts',
2733
+ content: nextJsEndpointRouteTemplate('../../_oidc-provider/runtime', ['POST']),
2734
+ },
2735
+ ]
2736
+ : []),
2737
+ { path: 'consent/page.tsx', content: nextJsConsentPageTemplate(corePkg, features, scopes) },
2738
+ { path: 'consent/actions.ts', content: nextJsConsentActionTemplate(corePkg, features, scopes) },
2463
2739
  { path: 'oidc-error/page.tsx', content: nextJsAuthorizationErrorPageTemplate() },
2464
2740
  { path: 'oidc-error/error.tsx', content: nextJsAuthorizationErrorBoundaryTemplate() },
2465
2741
  ];