@maronn-openid-connect/cli 0.7.0 → 0.8.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 (67) hide show
  1. package/README.md +83 -7
  2. package/dist/frameworks/hono/index.d.ts.map +1 -1
  3. package/dist/frameworks/hono/index.js +18 -9
  4. package/dist/frameworks/hono/index.js.map +1 -1
  5. package/dist/frameworks/hono/pages.d.ts +7 -6
  6. package/dist/frameworks/hono/pages.d.ts.map +1 -1
  7. package/dist/frameworks/hono/pages.js +62 -44
  8. package/dist/frameworks/hono/pages.js.map +1 -1
  9. package/dist/frameworks/hono/templates.d.ts +19 -6
  10. package/dist/frameworks/hono/templates.d.ts.map +1 -1
  11. package/dist/frameworks/hono/templates.js +439 -304
  12. package/dist/frameworks/hono/templates.js.map +1 -1
  13. package/dist/frameworks/hono/views.d.ts +3 -0
  14. package/dist/frameworks/hono/views.d.ts.map +1 -0
  15. package/dist/frameworks/hono/views.js +458 -0
  16. package/dist/frameworks/hono/views.js.map +1 -0
  17. package/dist/frameworks/nextjs/authorize.d.ts +3 -0
  18. package/dist/frameworks/nextjs/authorize.d.ts.map +1 -0
  19. package/dist/frameworks/nextjs/authorize.js +757 -0
  20. package/dist/frameworks/nextjs/authorize.js.map +1 -0
  21. package/dist/frameworks/nextjs/ciba.d.ts +8 -0
  22. package/dist/frameworks/nextjs/ciba.d.ts.map +1 -0
  23. package/dist/frameworks/nextjs/ciba.js +591 -0
  24. package/dist/frameworks/nextjs/ciba.js.map +1 -0
  25. package/dist/frameworks/nextjs/conformance.d.ts +3 -0
  26. package/dist/frameworks/nextjs/conformance.d.ts.map +1 -0
  27. package/dist/frameworks/nextjs/conformance.js +2447 -0
  28. package/dist/frameworks/nextjs/conformance.js.map +1 -0
  29. package/dist/frameworks/nextjs/device.d.ts +8 -0
  30. package/dist/frameworks/nextjs/device.d.ts.map +1 -0
  31. package/dist/frameworks/nextjs/device.js +662 -0
  32. package/dist/frameworks/nextjs/device.js.map +1 -0
  33. package/dist/frameworks/nextjs/endpoints.d.ts +7 -0
  34. package/dist/frameworks/nextjs/endpoints.d.ts.map +1 -0
  35. package/dist/frameworks/nextjs/endpoints.js +673 -0
  36. package/dist/frameworks/nextjs/endpoints.js.map +1 -0
  37. package/dist/frameworks/nextjs/index.d.ts.map +1 -1
  38. package/dist/frameworks/nextjs/index.js +135 -2
  39. package/dist/frameworks/nextjs/index.js.map +1 -1
  40. package/dist/frameworks/nextjs/interaction.d.ts +13 -0
  41. package/dist/frameworks/nextjs/interaction.d.ts.map +1 -0
  42. package/dist/frameworks/nextjs/interaction.js +864 -0
  43. package/dist/frameworks/nextjs/interaction.js.map +1 -0
  44. package/dist/frameworks/nextjs/logout.d.ts +5 -0
  45. package/dist/frameworks/nextjs/logout.d.ts.map +1 -0
  46. package/dist/frameworks/nextjs/logout.js +271 -0
  47. package/dist/frameworks/nextjs/logout.js.map +1 -0
  48. package/dist/frameworks/nextjs/metadata.d.ts +6 -0
  49. package/dist/frameworks/nextjs/metadata.d.ts.map +1 -0
  50. package/dist/frameworks/nextjs/metadata.js +339 -0
  51. package/dist/frameworks/nextjs/metadata.js.map +1 -0
  52. package/dist/frameworks/nextjs/provider.d.ts +6 -0
  53. package/dist/frameworks/nextjs/provider.d.ts.map +1 -0
  54. package/dist/frameworks/nextjs/provider.js +797 -0
  55. package/dist/frameworks/nextjs/provider.js.map +1 -0
  56. package/dist/frameworks/nextjs/token.d.ts +7 -0
  57. package/dist/frameworks/nextjs/token.d.ts.map +1 -0
  58. package/dist/frameworks/nextjs/token.js +1337 -0
  59. package/dist/frameworks/nextjs/token.js.map +1 -0
  60. package/dist/frameworks/web-standard/templates.d.ts +1 -13
  61. package/dist/frameworks/web-standard/templates.d.ts.map +1 -1
  62. package/dist/frameworks/web-standard/templates.js +12 -1296
  63. package/dist/frameworks/web-standard/templates.js.map +1 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +38 -8
  66. package/dist/index.js.map +1 -1
  67. package/package.json +1 -1
@@ -414,7 +414,13 @@ async function resolveProviderStores(
414
414
  }
415
415
  `;
416
416
  }
417
- export function customScopesTemplate(scopes, features = DEFAULT_FEATURES) {
417
+ const DEFAULT_SCOPE_POLICY_FILE_REFS = {
418
+ userinfo: 'routes/userinfo.ts',
419
+ consent: 'routes/consent.ts',
420
+ authorize: 'routes/authorize.ts',
421
+ approvals: 'routes/device.ts / routes/ciba-verification.ts',
422
+ };
423
+ export function customScopesTemplate(scopes, features = DEFAULT_FEATURES, fileRefs = DEFAULT_SCOPE_POLICY_FILE_REFS) {
418
424
  const toListLiteral = (values) => values.length === 0 ? '[]' : `[${values.map((value) => `'${singleQuoted(value)}'`).join(', ')}]`;
419
425
  const standardScopes = [
420
426
  'openid',
@@ -444,7 +450,7 @@ export function customScopesTemplate(scopes, features = DEFAULT_FEATURES) {
444
450
  * Custom scopes carry no UserInfo claims of their own: OIDC Core 1.0 §5.4 defines
445
451
  * claims for profile / email / address / phone only, so \`filterClaimsByScope()\`
446
452
  * ignores them. Return your own claims for a custom scope by editing
447
- * routes/userinfo.ts.
453
+ * ${fileRefs.userinfo}.
448
454
  */
449
455
 
450
456
  /**
@@ -482,12 +488,12 @@ export const RESTRICTED_SCOPE_SUBJECTS: Record<string, readonly string[]> = {
482
488
  * **This is the per-user scope filtering seam.** It runs once the End-User is
483
489
  * known, and every step that decides a grant already awaits it:
484
490
  *
485
- * - routes/consent.ts — the consent screen (what is displayed) and the approval
491
+ * - ${fileRefs.consent} — the consent screen (what is displayed) and the approval
486
492
  * (what is granted)
487
- * - routes/authorize.ts — the SSO fast path and prompt=none, which grant without
493
+ * - ${fileRefs.authorize} — the SSO fast path and prompt=none, which grant without
488
494
  * showing consent. Both narrow BEFORE looking up stored consent, because a
489
495
  * consent lookup for a scope the subject can never hold would never match.
490
- * - routes/device.ts / routes/ciba-verification.ts — the device and CIBA approval
496
+ * - ${fileRefs.approvals} — the device and CIBA approval
491
497
  * steps, when those features are generated
492
498
  *
493
499
  * It is async so a database / KV lookup can be dropped in without touching any
@@ -518,7 +524,21 @@ export function findUnsupportedScopes(requested: readonly string[]): string[] {
518
524
  }
519
525
  `;
520
526
  }
521
- export function configTemplate(corePkg, features = DEFAULT_FEATURES) {
527
+ export function configTemplate(corePkg, features = DEFAULT_FEATURES, options = {}) {
528
+ const authorizationErrorRedirectPathField = options.authorizationErrorRedirectPath === false
529
+ ? ''
530
+ : ` /**
531
+ * 任意。client redirect が禁止される非リダイレクト型の authorization error
532
+ * (未知 client_id / 未登録 redirect_uri / fragment 付き redirect_uri など、
533
+ * OIDC Core 1.0 §3.1.2.2)の HTML フォールバックを、views.errorPage() で直接
534
+ * 返す代わりに OP 内部のエラーページパスへ 303 リダイレクトしたいときに設定する。
535
+ * Next.js の error.tsx のような framework-native なエラー画面へ委ねるためのフック。
536
+ * 未設定なら従来どおり views.errorPage() を c.html で返す(express/fastify/hono の
537
+ * デフォルト)。なお Accept: application/json の programmatic caller には、この設定の
538
+ * 有無に関わらず常に 400 の OAuth error JSON を返す。
539
+ */
540
+ authorizationErrorRedirectPath?: string;
541
+ `;
522
542
  const refreshTokenLifetimeField = features.refreshToken
523
543
  ? ` /**
524
544
  * Refresh token の absolute lifetime(秒)。初回発行時刻からの絶対的な有効期限。
@@ -700,18 +720,7 @@ ${refreshTokenLifetimeField} /**
700
720
  * 不正なPKCE値やpublic clientの非PKCE requestは拒否する。
701
721
  */
702
722
  allowNonPkceAuthorizationCodeFlow: boolean;
703
- ${allowUnsignedField} /**
704
- * 任意。client redirect が禁止される非リダイレクト型の authorization error
705
- * (未知 client_id / 未登録 redirect_uri / fragment 付き redirect_uri など、
706
- * OIDC Core 1.0 §3.1.2.2)の HTML フォールバックを、views.errorPage() で直接
707
- * 返す代わりに OP 内部のエラーページパスへ 303 リダイレクトしたいときに設定する。
708
- * Next.js の error.tsx のような framework-native なエラー画面へ委ねるためのフック。
709
- * 未設定なら従来どおり views.errorPage() を c.html で返す(express/fastify/hono の
710
- * デフォルト)。なお Accept: application/json の programmatic caller には、この設定の
711
- * 有無に関わらず常に 400 の OAuth error JSON を返す。
712
- */
713
- authorizationErrorRedirectPath?: string;
714
- ${googleLoginConfigField}}
723
+ ${allowUnsignedField}${authorizationErrorRedirectPathField}${googleLoginConfigField}}
715
724
 
716
725
  /**
717
726
  * Optional defaults for quick local testing.
@@ -2283,7 +2292,7 @@ export const consentStore = defaultProviderStores.consentStore;
2283
2292
  export const userStore = defaultProviderStores.userStore;${googleLoginStoreExport}
2284
2293
  ${parStoreImplementation}${deviceStoreImplementation}${cibaStoreImplementation}`;
2285
2294
  }
2286
- export function resolversTemplate(corePkg, features = DEFAULT_FEATURES) {
2295
+ export function resolversTemplate(corePkg, features = DEFAULT_FEATURES, clientResolverInjectionNote = 'Project integrations should inject a D1/KV/env-backed resolver through Hono context.') {
2287
2296
  const refreshTypeImports = features.refreshToken
2288
2297
  ? ` RefreshTokenResolver,
2289
2298
  RefreshTokenInfo,
@@ -2419,7 +2428,7 @@ import {
2419
2428
 
2420
2429
  /**
2421
2430
  * Default in-memory client resolver for quick local testing.
2422
- * Project integrations should inject a D1/KV/env-backed resolver through Hono context.
2431
+ * ${clientResolverInjectionNote}
2423
2432
  */
2424
2433
  export const clientResolver: ClientResolver & TokenClientResolver =
2425
2434
  createInMemoryClientResolver();
@@ -8725,7 +8734,7 @@ revocationApp.post('/', async (c) => {
8725
8734
  });
8726
8735
  `;
8727
8736
  }
8728
- export function viewsTemplate(features = DEFAULT_FEATURES) {
8737
+ export function viewParamTypesTemplate(features = DEFAULT_FEATURES) {
8729
8738
  const deviceParamTypes = features.deviceAuthorizationGrant
8730
8739
  ? `
8731
8740
  export interface DeviceVerificationPageParams {
@@ -8778,6 +8787,120 @@ export interface DeviceCompletedPageParams {
8778
8787
  }
8779
8788
  `
8780
8789
  : '';
8790
+ const cibaParamTypes = features.ciba
8791
+ ? `
8792
+ export interface CibaLoginPageParams {
8793
+ /** Login transaction id; carried through as a hidden field. */
8794
+ loginTransactionId: string;
8795
+ /** CSRF token (must be included as hidden form field) */
8796
+ csrfToken: string;
8797
+ /** Error message from a previous failed attempt */
8798
+ error?: string;
8799
+ /** Number of remaining login attempts for this login transaction */
8800
+ remainingAttempts?: number;
8801
+ }
8802
+
8803
+ export interface CibaPendingRequestParams {
8804
+ /** auth_req_id; carried through as a hidden field of the decision form. */
8805
+ authReqId: string;
8806
+ /** Client that asked for the backchannel authentication */
8807
+ clientId: string;
8808
+ /** Scopes the client asked for */
8809
+ scopes: string[];
8810
+ /**
8811
+ * CIBA Core 1.0 §7.1 binding_message: shown so the user can compare it with
8812
+ * the message on the consumption device — the visual check that they are
8813
+ * approving THEIR transaction and not someone else's. Client-supplied text:
8814
+ * it MUST be escaped before rendering.
8815
+ */
8816
+ bindingMessage?: string;
8817
+ /** Seconds until this request expires */
8818
+ expiresInSeconds: number;
8819
+ /** Per-record CSRF token (must be included as hidden form field) */
8820
+ csrfToken: string;
8821
+ }
8822
+
8823
+ export interface CibaPendingRequestsPageParams {
8824
+ /** Pending backchannel authentication requests addressed to the signed-in user */
8825
+ requests: CibaPendingRequestParams[];
8826
+ }
8827
+
8828
+ export interface CibaCompletedPageParams {
8829
+ /** true when the user approved, false when they denied */
8830
+ approved: boolean;
8831
+ /** Client the decision applied to */
8832
+ clientId: string;
8833
+ }
8834
+ `
8835
+ : '';
8836
+ const rpInitiatedLogoutParamTypes = features.rpInitiatedLogout
8837
+ ? `
8838
+ export interface LogoutConfirmationPageParams {
8839
+ /** CSRF token (must be included as hidden form field of the approve POST) */
8840
+ csrfToken: string;
8841
+ }
8842
+
8843
+ /**
8844
+ * Parameters of the logged-out page. Deliberately empty: the completed screen
8845
+ * shows no End-User or client identifier (whoever sees the screen learns
8846
+ * nothing), and its wording never depends on whether anything was actually
8847
+ * deleted — varying it would make the page a session-existence oracle.
8848
+ */
8849
+ export interface LogoutCompletedPageParams {}
8850
+ `
8851
+ : '';
8852
+ const googleLoginPageParam = features.googleLogin
8853
+ ? ` /**
8854
+ * EXTENSION (google-login): GIS configuration for "Sign in with Google"
8855
+ * (redirect mode) — the g_id_onload attributes built by
8856
+ * buildGoogleSignInAttributes(): client ID, data-ux_mode="redirect", the
8857
+ * login_uri Google posts the ID token to, and the nonce bound to this
8858
+ * transaction. The view owns the markup (see defaultLoginPage). Undefined
8859
+ * when Google login is not configured; only the password form is shown then.
8860
+ */
8861
+ googleSignIn?: GoogleSignInAttributes;
8862
+ `
8863
+ : '';
8864
+ return `export interface LoginPageParams {
8865
+ /** Transaction ID for the auth flow */
8866
+ transactionId: string;
8867
+ /** CSRF token (must be included as hidden form field) */
8868
+ csrfToken: string;
8869
+ /** Error message from a previous failed attempt */
8870
+ error?: string;
8871
+ /** Number of remaining login attempts */
8872
+ remainingAttempts?: number;
8873
+ /**
8874
+ * OIDC Core 1.0 §3.1.2.1 login_hint: untrusted external value the OP MAY use to
8875
+ * pre-fill the login form. Treated as a hint only (initial display); it MUST be
8876
+ * HTML-attribute escaped before rendering since it is unauthenticated input.
8877
+ */
8878
+ loginHint?: string;
8879
+ ${googleLoginPageParam}}
8880
+
8881
+ export interface ConsentPageParams {
8882
+ /** Transaction ID for the auth flow */
8883
+ transactionId: string;
8884
+ /** CSRF token (must be included as hidden form field) */
8885
+ csrfToken: string;
8886
+ /** Scopes requested by the client */
8887
+ scopes: string[];
8888
+ /** Client ID requesting authorization */
8889
+ clientId: string;
8890
+ }
8891
+
8892
+ export interface ErrorPageParams {
8893
+ /** Error message to display (OAuth error code for authorization errors) */
8894
+ error: string;
8895
+ /** Optional human-readable detail (OAuth error_description) */
8896
+ errorDescription?: string;
8897
+ /** HTTP status code */
8898
+ statusCode: number;
8899
+ }
8900
+ ${deviceParamTypes}${cibaParamTypes}${rpInitiatedLogoutParamTypes}
8901
+ `;
8902
+ }
8903
+ export function viewsInterfaceTemplate(features = DEFAULT_FEATURES) {
8781
8904
  const deviceViewsMembers = features.deviceAuthorizationGrant
8782
8905
  ? ` /** EXPERIMENTAL (RFC 8628 §3.3): render the user_code entry form */
8783
8906
  deviceVerificationPage(params: DeviceVerificationPageParams): ViewResult;
@@ -8789,6 +8912,33 @@ export interface DeviceCompletedPageParams {
8789
8912
  deviceCompletedPage(params: DeviceCompletedPageParams): ViewResult;
8790
8913
  `
8791
8914
  : '';
8915
+ const cibaViewsMembers = features.ciba
8916
+ ? ` /** EXPERIMENTAL (CIBA Core 1.0): render the sign-in form of the authentication device UI */
8917
+ cibaLoginPage(params: CibaLoginPageParams): ViewResult;
8918
+ /** EXPERIMENTAL (CIBA Core 1.0): render the pending-requests approval screen */
8919
+ cibaPendingRequestsPage(params: CibaPendingRequestsPageParams): ViewResult;
8920
+ /** EXPERIMENTAL (CIBA Core 1.0): render the decision-recorded screen */
8921
+ cibaCompletedPage(params: CibaCompletedPageParams): ViewResult;
8922
+ `
8923
+ : '';
8924
+ const rpInitiatedLogoutViewsMembers = features.rpInitiatedLogout
8925
+ ? ` /** EXPERIMENTAL (RP-Initiated Logout 1.0 §2): render the logout confirmation screen */
8926
+ logoutConfirmationPage(params: LogoutConfirmationPageParams): ViewResult;
8927
+ /** EXPERIMENTAL (RP-Initiated Logout 1.0): render the logged-out screen */
8928
+ logoutCompletedPage(params: LogoutCompletedPageParams): ViewResult;
8929
+ `
8930
+ : '';
8931
+ return `export interface Views {
8932
+ /** Render the login page (and login error page when error is set) */
8933
+ loginPage(params: LoginPageParams): ViewResult;
8934
+ /** Render the consent/authorization page */
8935
+ consentPage(params: ConsentPageParams): ViewResult;
8936
+ /** Render a generic error page */
8937
+ errorPage(params: ErrorPageParams): ViewResult;
8938
+ ${deviceViewsMembers}${cibaViewsMembers}${rpInitiatedLogoutViewsMembers}}
8939
+ `;
8940
+ }
8941
+ export function viewsTemplate(features = DEFAULT_FEATURES) {
8792
8942
  const deviceDefaultViews = features.deviceAuthorizationGrant
8793
8943
  ? `function defaultDeviceVerificationPage(params: DeviceVerificationPageParams): string {
8794
8944
  const errorHtml = params.error
@@ -8897,61 +9047,6 @@ function defaultDeviceCompletedPage(params: DeviceCompletedPageParams): string {
8897
9047
  deviceLoginPage: defaultDeviceLoginPage,
8898
9048
  deviceApprovalPage: defaultDeviceApprovalPage,
8899
9049
  deviceCompletedPage: defaultDeviceCompletedPage,
8900
- `
8901
- : '';
8902
- const cibaParamTypes = features.ciba
8903
- ? `
8904
- export interface CibaLoginPageParams {
8905
- /** Login transaction id; carried through as a hidden field. */
8906
- loginTransactionId: string;
8907
- /** CSRF token (must be included as hidden form field) */
8908
- csrfToken: string;
8909
- /** Error message from a previous failed attempt */
8910
- error?: string;
8911
- /** Number of remaining login attempts for this login transaction */
8912
- remainingAttempts?: number;
8913
- }
8914
-
8915
- export interface CibaPendingRequestParams {
8916
- /** auth_req_id; carried through as a hidden field of the decision form. */
8917
- authReqId: string;
8918
- /** Client that asked for the backchannel authentication */
8919
- clientId: string;
8920
- /** Scopes the client asked for */
8921
- scopes: string[];
8922
- /**
8923
- * CIBA Core 1.0 §7.1 binding_message: shown so the user can compare it with
8924
- * the message on the consumption device — the visual check that they are
8925
- * approving THEIR transaction and not someone else's. Client-supplied text:
8926
- * it MUST be escaped before rendering.
8927
- */
8928
- bindingMessage?: string;
8929
- /** Seconds until this request expires */
8930
- expiresInSeconds: number;
8931
- /** Per-record CSRF token (must be included as hidden form field) */
8932
- csrfToken: string;
8933
- }
8934
-
8935
- export interface CibaPendingRequestsPageParams {
8936
- /** Pending backchannel authentication requests addressed to the signed-in user */
8937
- requests: CibaPendingRequestParams[];
8938
- }
8939
-
8940
- export interface CibaCompletedPageParams {
8941
- /** true when the user approved, false when they denied */
8942
- approved: boolean;
8943
- /** Client the decision applied to */
8944
- clientId: string;
8945
- }
8946
- `
8947
- : '';
8948
- const cibaViewsMembers = features.ciba
8949
- ? ` /** EXPERIMENTAL (CIBA Core 1.0): render the sign-in form of the authentication device UI */
8950
- cibaLoginPage(params: CibaLoginPageParams): ViewResult;
8951
- /** EXPERIMENTAL (CIBA Core 1.0): render the pending-requests approval screen */
8952
- cibaPendingRequestsPage(params: CibaPendingRequestsPageParams): ViewResult;
8953
- /** EXPERIMENTAL (CIBA Core 1.0): render the decision-recorded screen */
8954
- cibaCompletedPage(params: CibaCompletedPageParams): ViewResult;
8955
9050
  `
8956
9051
  : '';
8957
9052
  const cibaDefaultViews = features.ciba
@@ -9060,29 +9155,6 @@ function defaultCibaCompletedPage(params: CibaCompletedPageParams): string {
9060
9155
  ? ` cibaLoginPage: defaultCibaLoginPage,
9061
9156
  cibaPendingRequestsPage: defaultCibaPendingRequestsPage,
9062
9157
  cibaCompletedPage: defaultCibaCompletedPage,
9063
- `
9064
- : '';
9065
- const rpInitiatedLogoutParamTypes = features.rpInitiatedLogout
9066
- ? `
9067
- export interface LogoutConfirmationPageParams {
9068
- /** CSRF token (must be included as hidden form field of the approve POST) */
9069
- csrfToken: string;
9070
- }
9071
-
9072
- /**
9073
- * Parameters of the logged-out page. Deliberately empty: the completed screen
9074
- * shows no End-User or client identifier (whoever sees the screen learns
9075
- * nothing), and its wording never depends on whether anything was actually
9076
- * deleted — varying it would make the page a session-existence oracle.
9077
- */
9078
- export interface LogoutCompletedPageParams {}
9079
- `
9080
- : '';
9081
- const rpInitiatedLogoutViewsMembers = features.rpInitiatedLogout
9082
- ? ` /** EXPERIMENTAL (RP-Initiated Logout 1.0 §2): render the logout confirmation screen */
9083
- logoutConfirmationPage(params: LogoutConfirmationPageParams): ViewResult;
9084
- /** EXPERIMENTAL (RP-Initiated Logout 1.0): render the logged-out screen */
9085
- logoutCompletedPage(params: LogoutCompletedPageParams): ViewResult;
9086
9158
  `
9087
9159
  : '';
9088
9160
  const rpInitiatedLogoutDefaultViews = features.rpInitiatedLogout
@@ -9135,18 +9207,6 @@ import {
9135
9207
  type GoogleSignInAttributes,
9136
9208
  } from '${GOOGLE_LOGIN_PACKAGE}/sign-in';
9137
9209
 
9138
- `
9139
- : '';
9140
- const googleLoginPageParam = features.googleLogin
9141
- ? ` /**
9142
- * EXTENSION (google-login): GIS configuration for "Sign in with Google"
9143
- * (redirect mode) — the g_id_onload attributes built by
9144
- * buildGoogleSignInAttributes(): client ID, data-ux_mode="redirect", the
9145
- * login_uri Google posts the ID token to, and the nonce bound to this
9146
- * transaction. The view owns the markup (see defaultLoginPage). Undefined
9147
- * when Google login is not configured; only the password form is shown then.
9148
- */
9149
- googleSignIn?: GoogleSignInAttributes;
9150
9210
  `
9151
9211
  : '';
9152
9212
  const googleSignInSnippet = features.googleLogin
@@ -9182,44 +9242,7 @@ ${googleViewsImport}// =========================================================
9182
9242
  // View Parameter Types
9183
9243
  // ============================================================
9184
9244
 
9185
- export interface LoginPageParams {
9186
- /** Transaction ID for the auth flow */
9187
- transactionId: string;
9188
- /** CSRF token (must be included as hidden form field) */
9189
- csrfToken: string;
9190
- /** Error message from a previous failed attempt */
9191
- error?: string;
9192
- /** Number of remaining login attempts */
9193
- remainingAttempts?: number;
9194
- /**
9195
- * OIDC Core 1.0 §3.1.2.1 login_hint: untrusted external value the OP MAY use to
9196
- * pre-fill the login form. Treated as a hint only (initial display); it MUST be
9197
- * HTML-attribute escaped before rendering since it is unauthenticated input.
9198
- */
9199
- loginHint?: string;
9200
- ${googleLoginPageParam}}
9201
-
9202
- export interface ConsentPageParams {
9203
- /** Transaction ID for the auth flow */
9204
- transactionId: string;
9205
- /** CSRF token (must be included as hidden form field) */
9206
- csrfToken: string;
9207
- /** Scopes requested by the client */
9208
- scopes: string[];
9209
- /** Client ID requesting authorization */
9210
- clientId: string;
9211
- }
9212
-
9213
- export interface ErrorPageParams {
9214
- /** Error message to display (OAuth error code for authorization errors) */
9215
- error: string;
9216
- /** Optional human-readable detail (OAuth error_description) */
9217
- errorDescription?: string;
9218
- /** HTTP status code */
9219
- statusCode: number;
9220
- }
9221
- ${deviceParamTypes}${cibaParamTypes}${rpInitiatedLogoutParamTypes}
9222
- // ============================================================
9245
+ ${viewParamTypesTemplate(features)}// ============================================================
9223
9246
  // Views Interface
9224
9247
  // ============================================================
9225
9248
 
@@ -9230,15 +9253,7 @@ ${deviceParamTypes}${cibaParamTypes}${rpInitiatedLogoutParamTypes}
9230
9253
  */
9231
9254
  export type ViewResult = string | Response;
9232
9255
 
9233
- export interface Views {
9234
- /** Render the login page (and login error page when error is set) */
9235
- loginPage(params: LoginPageParams): ViewResult;
9236
- /** Render the consent/authorization page */
9237
- consentPage(params: ConsentPageParams): ViewResult;
9238
- /** Render a generic error page */
9239
- errorPage(params: ErrorPageParams): ViewResult;
9240
- ${deviceViewsMembers}${cibaViewsMembers}${rpInitiatedLogoutViewsMembers}}
9241
-
9256
+ ${viewsInterfaceTemplate(features)}
9242
9257
  /** Options applied when renderView wraps an HTML string into a Response. */
9243
9258
  export interface RenderViewInit {
9244
9259
  /** HTTP status code for the generated Response (defaults to 200). */
@@ -9464,9 +9479,26 @@ export function requestObjectConformanceBeforeAll(features = DEFAULT_FEATURES) {
9464
9479
  );
9465
9480
  `;
9466
9481
  }
9467
- export function reuseFlowConformanceTestBlock(features = DEFAULT_FEATURES, errorPageMode = 'html') {
9482
+ export function reuseFlowConformanceTestBlock(features = DEFAULT_FEATURES, viewMarkup = 'string') {
9468
9483
  return (reuseCascadeConformanceBlock(features) +
9469
- requestObjectValueConformanceBlock(features, errorPageMode));
9484
+ requestObjectValueConformanceBlock(features, viewMarkup));
9485
+ }
9486
+ export function defaultErrorPageBodyExpectation(error, description, viewMarkup) {
9487
+ if (viewMarkup === 'jsx') {
9488
+ return ` '<!DOCTYPE html><html><head><title>Error</title></head><body>' +
9489
+ '<h1>Error</h1><p>${error}</p><p>${description}</p></body></html>'`;
9490
+ }
9491
+ return ` [
9492
+ '<!DOCTYPE html>',
9493
+ '<html>',
9494
+ '<head><title>Error</title></head>',
9495
+ '<body>',
9496
+ ' <h1>Error</h1>',
9497
+ ' <p>${error}</p>',
9498
+ ' <p>${description}</p>',
9499
+ '</body>',
9500
+ '</html>',
9501
+ ].join('\\n')`;
9470
9502
  }
9471
9503
  function reuseCascadeConformanceBlock(features) {
9472
9504
  if (!features.refreshToken) {
@@ -9939,17 +9971,8 @@ function reuseCascadeConformanceBlock(features) {
9939
9971
  });
9940
9972
  `;
9941
9973
  }
9942
- function requestObjectValueConformanceBlock(features, errorPageMode = 'html') {
9943
- const brokenRequestObjectExpectation = errorPageMode === 'redirect'
9944
- ? ` // OIDC Core 1.0 §6.3: invalid_request_object (not the generic
9945
- // invalid_request). This provider sets authorizationErrorRedirectPath, so
9946
- // the browser is 303-redirected to the OP's OWN error page — never to the
9947
- // redirect_uri of the broken Request Object.
9948
- expect(res.status).toBe(303);
9949
- expect(res.headers.get('Location')).toBe(
9950
- '/oidc-error?error=invalid_request_object&error_description=request+object+is+not+a+JWS+compact+serialization',
9951
- );`
9952
- : ` // OIDC Core 1.0 §6.3: the request parameter contains an invalid Request
9974
+ function requestObjectValueConformanceBlock(features, viewMarkup = 'string') {
9975
+ const brokenRequestObjectExpectation = ` // OIDC Core 1.0 §6.3: the request parameter contains an invalid Request
9953
9976
  // Object, so the OP reports invalid_request_object (not the generic
9954
9977
  // invalid_request). A redirect_uri carried inside a broken Request Object
9955
9978
  // cannot be trusted, so the error stays on the OP: HTTP 400, no redirect,
@@ -9960,17 +9983,7 @@ function requestObjectValueConformanceBlock(features, errorPageMode = 'html') {
9960
9983
  expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
9961
9984
  const body = await res.text();
9962
9985
  expect(body).toBe(
9963
- [
9964
- '<!DOCTYPE html>',
9965
- '<html>',
9966
- '<head><title>Error</title></head>',
9967
- '<body>',
9968
- ' <h1>Error</h1>',
9969
- ' <p>invalid_request_object</p>',
9970
- ' <p>request object is not a JWS compact serialization</p>',
9971
- '</body>',
9972
- '</html>',
9973
- ].join('\\n'),
9986
+ ${defaultErrorPageBodyExpectation('invalid_request_object', 'request object is not a JWS compact serialization', viewMarkup)},
9974
9987
  );`;
9975
9988
  if (!features.requestObject) {
9976
9989
  return `
@@ -10445,6 +10458,79 @@ function transactionBindingDisabledConformanceBlock() {
10445
10458
  });
10446
10459
  `;
10447
10460
  }
10461
+ function loginPageDeliveryConformanceTest() {
10462
+ return ` // End-to-end: the login page (pages/login.ts) returns its view via
10463
+ // renderView, so the login page is delivered as a text/html Response through
10464
+ // the framework at runtime.
10465
+ it('should deliver the login page through renderView as a text/html Response', async () => {
10466
+ // RFC 7636 Appendix B example challenge so authorize is accepted and mints a
10467
+ // transaction (302 -> /login); the verifier is never needed here.
10468
+ const PKCE_CHALLENGE_S256 = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';
10469
+ const authorizeUrl =
10470
+ '/authorize?response_type=code&client_id=c-conf' +
10471
+ '&redirect_uri=' + encodeURIComponent(REDIRECT_URI) +
10472
+ '&scope=' + encodeURIComponent('openid') +
10473
+ '&state=view-xyz' +
10474
+ '&code_challenge=' + PKCE_CHALLENGE_S256 + '&code_challenge_method=S256';
10475
+ const authorizeRes = await app.request(authorizeUrl);
10476
+ const loginUrl = new URL(authorizeRes.headers.get('Location') ?? '', 'http://localhost');
10477
+ // Carry forward whatever cookie /authorize set, exactly as a browser would.
10478
+ // With --enable transaction-binding this is the per-transaction binding
10479
+ // secret the later steps require; without it this is '' and the OP ignores
10480
+ // it, so the same flow works in both builds.
10481
+ const bindingCookie = (authorizeRes.headers.get('Set-Cookie') ?? '').split(';')[0] ?? '';
10482
+
10483
+ const res = await app.request(loginUrl.pathname + loginUrl.search, { headers: { Cookie: bindingCookie } });
10484
+
10485
+ // The login body carries a dynamic transaction_id / csrf_token, so the
10486
+ // status + content type pin that renderView delivered a text/html Response
10487
+ // at runtime; the exact-body wrapping is pinned by the renderView unit tests.
10488
+ expect(res.status).toBe(200);
10489
+ expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
10490
+ });
10491
+ `;
10492
+ }
10493
+ export function jsxViewConformanceTestBlock() {
10494
+ return `
10495
+ describe('JSX view rendering (ViewResult / renderView)', () => {
10496
+ // A view returns a hono/jsx element; renderView serializes it into a
10497
+ // text/html Response.
10498
+ it('should serialize a JSX view into a text/html Response', async () => {
10499
+ const res = renderView(defaultViews.errorPage({ error: 'jsx-view', statusCode: 400 }));
10500
+
10501
+ expect(res.status).toBe(200);
10502
+ expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
10503
+ expect(await res.text()).toBe(
10504
+ '<!DOCTYPE html><html><head><title>Error</title></head><body>' +
10505
+ '<h1>Error</h1><p>jsx-view</p></body></html>',
10506
+ );
10507
+ });
10508
+
10509
+ // The caller-provided status is applied (e.g. the 429 rate-limit error page).
10510
+ it('should apply the provided status to a JSX view', async () => {
10511
+ const res = renderView(
10512
+ defaultViews.errorPage({ error: 'too many', statusCode: 429 }),
10513
+ { status: 429 },
10514
+ );
10515
+
10516
+ expect(res.status).toBe(429);
10517
+ expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
10518
+ });
10519
+
10520
+ // A view that renders asynchronously (an async component, or hono/html
10521
+ // with a Promise inside) is streamed once its HTML resolves; the Response
10522
+ // itself is built synchronously so a page can still attach cookies to it.
10523
+ it('should stream a view that renders asynchronously', async () => {
10524
+ const res = renderView(html\`<p>\${Promise.resolve('async-view')}</p>\`);
10525
+
10526
+ expect(res.status).toBe(200);
10527
+ expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
10528
+ expect(await res.text()).toBe('<p>async-view</p>');
10529
+ });
10530
+
10531
+ ${loginPageDeliveryConformanceTest()} });
10532
+ `;
10533
+ }
10448
10534
  export function customViewConformanceTestBlock() {
10449
10535
  return `
10450
10536
  describe('custom view rendering (ViewResult / renderView)', () => {
@@ -10481,36 +10567,7 @@ export function customViewConformanceTestBlock() {
10481
10567
  expect(await res.text()).toBe('<h1>custom-view-response</h1>');
10482
10568
  });
10483
10569
 
10484
- // End-to-end: the login page (pages/login.ts) returns its view via
10485
- // renderView, so the login page is delivered as a text/html Response through
10486
- // the framework at runtime.
10487
- it('should deliver the login page through renderView as a text/html Response', async () => {
10488
- // RFC 7636 Appendix B example challenge so authorize is accepted and mints a
10489
- // transaction (302 -> /login); the verifier is never needed here.
10490
- const PKCE_CHALLENGE_S256 = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM';
10491
- const authorizeUrl =
10492
- '/authorize?response_type=code&client_id=c-conf' +
10493
- '&redirect_uri=' + encodeURIComponent(REDIRECT_URI) +
10494
- '&scope=' + encodeURIComponent('openid') +
10495
- '&state=view-xyz' +
10496
- '&code_challenge=' + PKCE_CHALLENGE_S256 + '&code_challenge_method=S256';
10497
- const authorizeRes = await app.request(authorizeUrl);
10498
- const loginUrl = new URL(authorizeRes.headers.get('Location') ?? '', 'http://localhost');
10499
- // Carry forward whatever cookie /authorize set, exactly as a browser would.
10500
- // With --enable transaction-binding this is the per-transaction binding
10501
- // secret the later steps require; without it this is '' and the OP ignores
10502
- // it, so the same flow works in both builds.
10503
- const bindingCookie = (authorizeRes.headers.get('Set-Cookie') ?? '').split(';')[0] ?? '';
10504
-
10505
- const res = await app.request(loginUrl.pathname + loginUrl.search, { headers: { Cookie: bindingCookie } });
10506
-
10507
- // The login body carries a dynamic transaction_id / csrf_token, so the
10508
- // status + content type pin that renderView delivered a text/html Response
10509
- // at runtime; the exact-body wrapping is pinned by the renderView unit tests.
10510
- expect(res.status).toBe(200);
10511
- expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
10512
- });
10513
- });
10570
+ ${loginPageDeliveryConformanceTest()} });
10514
10571
  `;
10515
10572
  }
10516
10573
  function deviceAuthorizationConformanceClients(features) {
@@ -11913,6 +11970,161 @@ export function tokenEndpointAuthMethodsConformanceBlock() {
11913
11970
  const tokenBody = await tokenRes.json();
11914
11971
  expect(tokenBody.error).toBe('invalid_request');
11915
11972
  });
11973
+
11974
+ // RFC 6749 §3.2: "Parameters sent without a value MUST be treated as if they
11975
+ // were omitted from the request." A public client whose HTTP stack serializes
11976
+ // an unset secret as client_secret= must still authenticate as method 'none'.
11977
+ it('should treat an empty client_secret field as absent for a public client', async () => {
11978
+ const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
11979
+ const authorizeRes = await app.request(
11980
+ '/authorize?response_type=code&client_id=c-public' +
11981
+ '&redirect_uri=' + encodeURIComponent(REDIRECT_URI) +
11982
+ '&scope=openid&state=public-empty-secret' +
11983
+ '&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256',
11984
+ );
11985
+ const loginPath = relativeLocation(authorizeRes.headers.get('Location'));
11986
+ // Carry forward whatever cookie /authorize set, exactly as a browser would.
11987
+ // With --enable transaction-binding this is the per-transaction binding
11988
+ // secret the later steps require; without it this is '' and the OP ignores
11989
+ // it, so the same flow works in both builds.
11990
+ const bindingCookie = (authorizeRes.headers.get('Set-Cookie') ?? '').split(';')[0] ?? '';
11991
+ const transactionId =
11992
+ new URL(loginPath, 'http://localhost').searchParams.get('transaction_id') ?? '';
11993
+ const loginGet = await app.request(loginPath, { headers: { Cookie: bindingCookie } });
11994
+ const loginRes = await app.request('/login', {
11995
+ method: 'POST',
11996
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: bindingCookie },
11997
+ body: new URLSearchParams({
11998
+ transaction_id: transactionId,
11999
+ csrf_token: csrfTokenFrom(await loginGet.text()),
12000
+ username: 'testuser',
12001
+ password: 'password',
12002
+ }).toString(),
12003
+ });
12004
+ const consentPath = relativeLocation(loginRes.headers.get('Location'));
12005
+ const consentGet = await app.request(consentPath, { headers: { Cookie: bindingCookie } });
12006
+ const consentRes = await app.request('/consent', {
12007
+ method: 'POST',
12008
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: bindingCookie },
12009
+ body: new URLSearchParams({
12010
+ transaction_id: transactionId,
12011
+ csrf_token: csrfTokenFrom(await consentGet.text()),
12012
+ action: 'approve',
12013
+ }).toString(),
12014
+ });
12015
+ const callback = new URL(consentRes.headers.get('Location') ?? '', 'http://localhost');
12016
+ const tokenRes = await app.request('/token', {
12017
+ method: 'POST',
12018
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
12019
+ body: new URLSearchParams({
12020
+ grant_type: 'authorization_code',
12021
+ code: callback.searchParams.get('code') ?? '',
12022
+ redirect_uri: REDIRECT_URI,
12023
+ code_verifier: verifier,
12024
+ client_id: 'c-public',
12025
+ // A valueless form field (client_secret=) carries no credential.
12026
+ client_secret: '',
12027
+ }).toString(),
12028
+ });
12029
+
12030
+ expect(consentRes.status).toBe(302);
12031
+ expect(tokenRes.status).toBe(200);
12032
+ const tokenBody = await tokenRes.json();
12033
+ expect(tokenBody.token_type).toBe('Bearer');
12034
+ expect(tokenBody.scope).toBe('openid');
12035
+ expect((tokenBody.access_token as string).split('.')).toHaveLength(3);
12036
+ expect((tokenBody.id_token as string).split('.')).toHaveLength(3);
12037
+ });
12038
+
12039
+ // RFC 6749 §2.3 / §3.2: an empty client_secret field carries no credential,
12040
+ // so it is not a second authentication method next to Authorization: Basic.
12041
+ it('should authenticate a client_secret_basic request that also sends an empty client_secret field', async () => {
12042
+ const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk';
12043
+ const authorizeRes = await app.request(
12044
+ '/authorize?response_type=code&client_id=c-conf-basic' +
12045
+ '&redirect_uri=' + encodeURIComponent(REDIRECT_URI) +
12046
+ '&scope=openid&state=basic-empty-secret' +
12047
+ '&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256',
12048
+ );
12049
+ const loginPath = relativeLocation(authorizeRes.headers.get('Location'));
12050
+ // Carry forward whatever cookie /authorize set, exactly as a browser would.
12051
+ // With --enable transaction-binding this is the per-transaction binding
12052
+ // secret the later steps require; without it this is '' and the OP ignores
12053
+ // it, so the same flow works in both builds.
12054
+ const bindingCookie = (authorizeRes.headers.get('Set-Cookie') ?? '').split(';')[0] ?? '';
12055
+ const transactionId =
12056
+ new URL(loginPath, 'http://localhost').searchParams.get('transaction_id') ?? '';
12057
+ const loginGet = await app.request(loginPath, { headers: { Cookie: bindingCookie } });
12058
+ const loginRes = await app.request('/login', {
12059
+ method: 'POST',
12060
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: bindingCookie },
12061
+ body: new URLSearchParams({
12062
+ transaction_id: transactionId,
12063
+ csrf_token: csrfTokenFrom(await loginGet.text()),
12064
+ username: 'testuser',
12065
+ password: 'password',
12066
+ }).toString(),
12067
+ });
12068
+ const consentPath = relativeLocation(loginRes.headers.get('Location'));
12069
+ const consentGet = await app.request(consentPath, { headers: { Cookie: bindingCookie } });
12070
+ const consentRes = await app.request('/consent', {
12071
+ method: 'POST',
12072
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Cookie: bindingCookie },
12073
+ body: new URLSearchParams({
12074
+ transaction_id: transactionId,
12075
+ csrf_token: csrfTokenFrom(await consentGet.text()),
12076
+ action: 'approve',
12077
+ }).toString(),
12078
+ });
12079
+ const callback = new URL(consentRes.headers.get('Location') ?? '', 'http://localhost');
12080
+ const tokenRes = await app.request('/token', {
12081
+ method: 'POST',
12082
+ headers: {
12083
+ 'Content-Type': 'application/x-www-form-urlencoded',
12084
+ Authorization: 'Basic ' + btoa('c-conf-basic:s'),
12085
+ },
12086
+ body: new URLSearchParams({
12087
+ grant_type: 'authorization_code',
12088
+ code: callback.searchParams.get('code') ?? '',
12089
+ redirect_uri: REDIRECT_URI,
12090
+ code_verifier: verifier,
12091
+ client_id: 'c-conf-basic',
12092
+ // A valueless form field left behind by the client's HTTP stack.
12093
+ client_secret: '',
12094
+ }).toString(),
12095
+ });
12096
+
12097
+ expect(consentRes.status).toBe(302);
12098
+ expect(tokenRes.status).toBe(200);
12099
+ const tokenBody = await tokenRes.json();
12100
+ expect(tokenBody.token_type).toBe('Bearer');
12101
+ expect(tokenBody.scope).toBe('openid');
12102
+ expect((tokenBody.access_token as string).split('.')).toHaveLength(3);
12103
+ expect((tokenBody.id_token as string).split('.')).toHaveLength(3);
12104
+ });
12105
+
12106
+ // A confidential client sending only an empty client_secret presents no
12107
+ // credential at all, so the rejection reason is 'authentication required',
12108
+ // not a method mismatch. Client authentication runs before code validation,
12109
+ // so no authorization code flow is needed here.
12110
+ it('should still require authentication when a confidential client sends an empty client_secret', async () => {
12111
+ const tokenRes = await app.request('/token', {
12112
+ method: 'POST',
12113
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
12114
+ body: new URLSearchParams({
12115
+ grant_type: 'authorization_code',
12116
+ code: 'irrelevant-code',
12117
+ redirect_uri: REDIRECT_URI,
12118
+ client_id: 'c-conf-basic',
12119
+ client_secret: '',
12120
+ }).toString(),
12121
+ });
12122
+
12123
+ expect(tokenRes.status).toBe(401);
12124
+ const tokenBody = await tokenRes.json();
12125
+ expect(tokenBody.error).toBe('invalid_client');
12126
+ expect(tokenBody.error_description).toBe('Client authentication required');
12127
+ });
11916
12128
  });
11917
12129
 
11918
12130
  `;
@@ -15139,59 +15351,6 @@ function jarmInteractiveConsentJwtBlock() {
15139
15351
  ${jarmPromptNoneErrorTest()} });
15140
15352
  `;
15141
15353
  }
15142
- function jarmInteractiveConsentPlainBlock() {
15143
- return ` describe('Interactive flow response (Next.js Server Action limitation)', () => {
15144
- // On this target the consent step runs as a Next.js Server Action, which is
15145
- // bundled apart from the Route Handlers and holds its own signing key
15146
- // provider instance. A response JWT signed there would carry the same kid as
15147
- // /.well-known/jwks.json but different key material, so every client would
15148
- // fail signature verification. The Server Action therefore keeps the plain
15149
- // query response, and these tests pin that so the limitation stays visible.
15150
- it('should return the plain query response after login and consent', async () => {
15151
- const { location } = await interactiveFlow(authorizeUrl({ response_mode: 'query.jwt' }));
15152
-
15153
- // RFC 9207 Section 2: the plain response carries iss, because no JWT iss
15154
- // claim is available to identify the issuer here.
15155
- expect([...queryOf(location).keys()].sort()).toEqual(['code', 'iss', 'state']);
15156
- expect(queryOf(location).get('state')).toBe('jarm-state');
15157
- expect(queryOf(location).get('iss')).toBe('http://localhost:3000');
15158
- });
15159
-
15160
- it('should return the plain query error when the End-User denies consent', async () => {
15161
- const { location } = await interactiveFlow(
15162
- authorizeUrl({ response_mode: 'query.jwt' }),
15163
- 'deny',
15164
- );
15165
-
15166
- expect([...queryOf(location).keys()].sort()).toEqual(['error', 'iss', 'state']);
15167
- expect(queryOf(location).get('error')).toBe('access_denied');
15168
- expect(queryOf(location).get('state')).toBe('jarm-state');
15169
- });
15170
-
15171
- it('should exchange the plainly delivered code for tokens', async () => {
15172
- const { location } = await interactiveFlow(authorizeUrl({ response_mode: 'query.jwt' }));
15173
- const res = await app.request('/token', {
15174
- method: 'POST',
15175
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
15176
- body: new URLSearchParams({
15177
- grant_type: 'authorization_code',
15178
- code: queryOf(location).get('code') ?? '',
15179
- redirect_uri: REDIRECT_URI,
15180
- client_id: 'c-conf',
15181
- client_secret: 's',
15182
- code_verifier: PKCE_VERIFIER,
15183
- }).toString(),
15184
- });
15185
-
15186
- expect(res.status).toBe(200);
15187
- expect((await res.json()).token_type).toBe('Bearer');
15188
- });
15189
- });
15190
-
15191
- describe('Error response (JARM Section 2.1)', () => {
15192
- ${jarmPromptNoneErrorTest()} });
15193
- `;
15194
- }
15195
15354
  function jarmPromptNoneErrorTest() {
15196
15355
  return ` it('should return a signed error JWT for a prompt=none request with no session', async () => {
15197
15356
  // OIDC Core 1.0 Section 3.1.2.1: prompt=none without a session is
@@ -16895,19 +17054,11 @@ ${refreshTokenExpectation} });
16895
17054
  });
16896
17055
  `;
16897
17056
  }
16898
- export function jarmConformanceBlock(features, consentResponseMode = 'jwt') {
17057
+ export function jarmConformanceBlock(features) {
16899
17058
  if (!features.jarm)
16900
17059
  return '';
16901
- const interactiveResponseTests = consentResponseMode === 'plain'
16902
- ? jarmInteractiveConsentPlainBlock()
16903
- : jarmInteractiveConsentJwtBlock();
16904
- const jarmAuthorizeRouteResponsesComment = consentResponseMode === 'plain'
16905
- ? ` // These paths answer inside the authorize route — a Route Handler, which
16906
- // shares the signing key provider with /.well-known/jwks.json — so they do
16907
- // produce a verifiable JARM response even though the consent step above
16908
- // cannot.
16909
- `
16910
- : ` // The authorize route records the mode on the transaction and the consent
17060
+ const interactiveResponseTests = jarmInteractiveConsentJwtBlock();
17061
+ const jarmAuthorizeRouteResponsesComment = ` // The authorize route records the mode on the transaction and the consent
16911
17062
  // route reads it back, so a store that drops unknown fields would answer in
16912
17063
  // plain query. These paths, by contrast, answer inside the authorize route
16913
17064
  // itself and never touch the store round trip.
@@ -18124,6 +18275,7 @@ import { rpInitiatedLogoutConfig } from './routes/logout.js';`
18124
18275
  return `import { ${vitestNames} } from 'vitest';
18125
18276
  import type { SigningKeyProvider, SigningKey } from '${corePkg}';
18126
18277
  import { Hono } from 'hono';
18278
+ import { html } from 'hono/html';
18127
18279
  ${exportPublicJwkImport}import { createApp, validateSigningKeySet } from './app.js';
18128
18280
  import { applyOidc } from './apply.js';
18129
18281
  import { createInMemoryClientResolver, type RegisteredClient${googleLoginConfigTypeImport} } from './config.js';
@@ -18213,24 +18365,15 @@ ${persistentStorageConformanceBlock()}
18213
18365
  expect(consentHtml.includes('&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;')).toBe(true);
18214
18366
  });
18215
18367
 
18216
- it('should preserve a custom Response returned by a view', () => {
18217
- const customResponse = new Response('custom view', {
18218
- status: 202,
18219
- headers: { 'X-View-Renderer': 'custom' },
18220
- });
18221
- const rendered = renderView(customResponse, { status: 400 });
18222
-
18223
- expect(rendered).toBe(customResponse);
18224
- expect(rendered.status).toBe(202);
18225
- expect(rendered.headers.get('X-View-Renderer')).toBe('custom');
18226
- });
18227
-
18228
- it('should render a custom HTML string returned by the error view', async () => {
18229
- const customHtml = '<!DOCTYPE html><p>custom authorization error</p>';
18368
+ // A custom view injected through createApp({ views }) replaces the default
18369
+ // one; hono/html escapes what it interpolates, like JSX does.
18370
+ it('should render a custom error view injected through createApp', async () => {
18230
18371
  const customApp = createApp({
18231
18372
  signingKeyProvider,
18232
18373
  clientResolver: createInMemoryClientResolver(testClients),
18233
- views: { errorPage: () => customHtml },
18374
+ views: {
18375
+ errorPage: (params) => html\`<!DOCTYPE html><p>custom \${params.error}: \${params.errorDescription}</p>\`,
18376
+ },
18234
18377
  });
18235
18378
  const res = await customApp.request(
18236
18379
  '/authorize?response_type=code&client_id=c-conf' +
@@ -18241,7 +18384,9 @@ ${persistentStorageConformanceBlock()}
18241
18384
 
18242
18385
  expect(res.status).toBe(400);
18243
18386
  expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
18244
- expect(await res.text()).toBe(customHtml);
18387
+ expect(await res.text()).toBe(
18388
+ '<!DOCTYPE html><p>custom invalid_request: redirect_uri not registered</p>',
18389
+ );
18245
18390
  });
18246
18391
  });
18247
18392
 
@@ -18580,17 +18725,7 @@ ${introspectionConformanceBlock(features)}
18580
18725
  // Pinned to the default error page so a regression in the rendered markup
18581
18726
  // (or a missing error_description) is caught exactly.
18582
18727
  expect(body).toBe(
18583
- [
18584
- '<!DOCTYPE html>',
18585
- '<html>',
18586
- '<head><title>Error</title></head>',
18587
- '<body>',
18588
- ' <h1>Error</h1>',
18589
- ' <p>invalid_request</p>',
18590
- ' <p>redirect_uri not registered</p>',
18591
- '</body>',
18592
- '</html>',
18593
- ].join('\\n'),
18728
+ ${defaultErrorPageBodyExpectation('invalid_request', 'redirect_uri not registered', 'jsx')},
18594
18729
  );
18595
18730
  });
18596
18731
 
@@ -18609,7 +18744,7 @@ ${introspectionConformanceBlock(features)}
18609
18744
  });
18610
18745
  });
18611
18746
  });
18612
- ${transactionBindingConformanceBlock(features)}${customViewConformanceTestBlock()}${internalRedirectOriginConformanceBlock()}${endpointBehaviorConformanceBlock(features, true)}${idTokenHintConformanceBlock()}${consentWithdrawalConformanceBlock(features)}${reuseFlowConformanceTestBlock(features)}${onlineRefreshTokenConformanceBlock(features)}${revocationDisabledConformanceBlock(features)}${tokenEndpointAuthMethodsConformanceBlock()}${pkceDisabledConformanceBlock(features)}${parConformanceBlock(features)}${tokenExchangeConformanceBlock(features)}${idJagConformanceBlock(features)}${deviceAuthorizationConformanceBlock(features)}${cibaConformanceBlock(features)}${jarmConformanceBlock(features)}${jwtIntrospectionResponseConformanceBlock(features)}${rpInitiatedLogoutConformanceBlock(features)}${googleLoginConformanceBlock(features)}${consentDecisionConformanceBlock()}${customScopeConformanceBlock(scopes)}});
18747
+ ${transactionBindingConformanceBlock(features)}${jsxViewConformanceTestBlock()}${internalRedirectOriginConformanceBlock()}${endpointBehaviorConformanceBlock(features, true)}${idTokenHintConformanceBlock()}${consentWithdrawalConformanceBlock(features)}${reuseFlowConformanceTestBlock(features, 'jsx')}${onlineRefreshTokenConformanceBlock(features)}${revocationDisabledConformanceBlock(features)}${tokenEndpointAuthMethodsConformanceBlock()}${pkceDisabledConformanceBlock(features)}${parConformanceBlock(features)}${tokenExchangeConformanceBlock(features)}${idJagConformanceBlock(features)}${deviceAuthorizationConformanceBlock(features)}${cibaConformanceBlock(features)}${jarmConformanceBlock(features)}${jwtIntrospectionResponseConformanceBlock(features)}${rpInitiatedLogoutConformanceBlock(features)}${googleLoginConformanceBlock(features)}${consentDecisionConformanceBlock()}${customScopeConformanceBlock(scopes)}});
18613
18748
  `;
18614
18749
  }
18615
18750
  //# sourceMappingURL=templates.js.map