@maronn-openid-connect/cli 0.7.1 → 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.
- package/README.md +83 -7
- package/dist/frameworks/hono/index.d.ts.map +1 -1
- package/dist/frameworks/hono/index.js +18 -9
- package/dist/frameworks/hono/index.js.map +1 -1
- package/dist/frameworks/hono/pages.d.ts +7 -6
- package/dist/frameworks/hono/pages.d.ts.map +1 -1
- package/dist/frameworks/hono/pages.js +62 -44
- package/dist/frameworks/hono/pages.js.map +1 -1
- package/dist/frameworks/hono/templates.d.ts +19 -6
- package/dist/frameworks/hono/templates.d.ts.map +1 -1
- package/dist/frameworks/hono/templates.js +284 -304
- package/dist/frameworks/hono/templates.js.map +1 -1
- package/dist/frameworks/hono/views.d.ts +3 -0
- package/dist/frameworks/hono/views.d.ts.map +1 -0
- package/dist/frameworks/hono/views.js +458 -0
- package/dist/frameworks/hono/views.js.map +1 -0
- package/dist/frameworks/nextjs/authorize.d.ts +3 -0
- package/dist/frameworks/nextjs/authorize.d.ts.map +1 -0
- package/dist/frameworks/nextjs/authorize.js +757 -0
- package/dist/frameworks/nextjs/authorize.js.map +1 -0
- package/dist/frameworks/nextjs/ciba.d.ts +8 -0
- package/dist/frameworks/nextjs/ciba.d.ts.map +1 -0
- package/dist/frameworks/nextjs/ciba.js +591 -0
- package/dist/frameworks/nextjs/ciba.js.map +1 -0
- package/dist/frameworks/nextjs/conformance.d.ts +3 -0
- package/dist/frameworks/nextjs/conformance.d.ts.map +1 -0
- package/dist/frameworks/nextjs/conformance.js +2447 -0
- package/dist/frameworks/nextjs/conformance.js.map +1 -0
- package/dist/frameworks/nextjs/device.d.ts +8 -0
- package/dist/frameworks/nextjs/device.d.ts.map +1 -0
- package/dist/frameworks/nextjs/device.js +662 -0
- package/dist/frameworks/nextjs/device.js.map +1 -0
- package/dist/frameworks/nextjs/endpoints.d.ts +7 -0
- package/dist/frameworks/nextjs/endpoints.d.ts.map +1 -0
- package/dist/frameworks/nextjs/endpoints.js +673 -0
- package/dist/frameworks/nextjs/endpoints.js.map +1 -0
- package/dist/frameworks/nextjs/index.d.ts.map +1 -1
- package/dist/frameworks/nextjs/index.js +135 -2
- package/dist/frameworks/nextjs/index.js.map +1 -1
- package/dist/frameworks/nextjs/interaction.d.ts +13 -0
- package/dist/frameworks/nextjs/interaction.d.ts.map +1 -0
- package/dist/frameworks/nextjs/interaction.js +864 -0
- package/dist/frameworks/nextjs/interaction.js.map +1 -0
- package/dist/frameworks/nextjs/logout.d.ts +5 -0
- package/dist/frameworks/nextjs/logout.d.ts.map +1 -0
- package/dist/frameworks/nextjs/logout.js +271 -0
- package/dist/frameworks/nextjs/logout.js.map +1 -0
- package/dist/frameworks/nextjs/metadata.d.ts +6 -0
- package/dist/frameworks/nextjs/metadata.d.ts.map +1 -0
- package/dist/frameworks/nextjs/metadata.js +339 -0
- package/dist/frameworks/nextjs/metadata.js.map +1 -0
- package/dist/frameworks/nextjs/provider.d.ts +6 -0
- package/dist/frameworks/nextjs/provider.d.ts.map +1 -0
- package/dist/frameworks/nextjs/provider.js +797 -0
- package/dist/frameworks/nextjs/provider.js.map +1 -0
- package/dist/frameworks/nextjs/token.d.ts +7 -0
- package/dist/frameworks/nextjs/token.d.ts.map +1 -0
- package/dist/frameworks/nextjs/token.js +1337 -0
- package/dist/frameworks/nextjs/token.js.map +1 -0
- package/dist/frameworks/web-standard/templates.d.ts +1 -13
- package/dist/frameworks/web-standard/templates.d.ts.map +1 -1
- package/dist/frameworks/web-standard/templates.js +12 -1296
- package/dist/frameworks/web-standard/templates.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -414,7 +414,13 @@ async function resolveProviderStores(
|
|
|
414
414
|
}
|
|
415
415
|
`;
|
|
416
416
|
}
|
|
417
|
-
|
|
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
|
-
*
|
|
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
|
-
* -
|
|
491
|
+
* - ${fileRefs.consent} — the consent screen (what is displayed) and the approval
|
|
486
492
|
* (what is granted)
|
|
487
|
-
* -
|
|
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
|
-
* -
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
9482
|
+
export function reuseFlowConformanceTestBlock(features = DEFAULT_FEATURES, viewMarkup = 'string') {
|
|
9468
9483
|
return (reuseCascadeConformanceBlock(features) +
|
|
9469
|
-
requestObjectValueConformanceBlock(features,
|
|
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,
|
|
9943
|
-
const brokenRequestObjectExpectation =
|
|
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
|
-
|
|
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) {
|
|
@@ -15294,59 +15351,6 @@ function jarmInteractiveConsentJwtBlock() {
|
|
|
15294
15351
|
${jarmPromptNoneErrorTest()} });
|
|
15295
15352
|
`;
|
|
15296
15353
|
}
|
|
15297
|
-
function jarmInteractiveConsentPlainBlock() {
|
|
15298
|
-
return ` describe('Interactive flow response (Next.js Server Action limitation)', () => {
|
|
15299
|
-
// On this target the consent step runs as a Next.js Server Action, which is
|
|
15300
|
-
// bundled apart from the Route Handlers and holds its own signing key
|
|
15301
|
-
// provider instance. A response JWT signed there would carry the same kid as
|
|
15302
|
-
// /.well-known/jwks.json but different key material, so every client would
|
|
15303
|
-
// fail signature verification. The Server Action therefore keeps the plain
|
|
15304
|
-
// query response, and these tests pin that so the limitation stays visible.
|
|
15305
|
-
it('should return the plain query response after login and consent', async () => {
|
|
15306
|
-
const { location } = await interactiveFlow(authorizeUrl({ response_mode: 'query.jwt' }));
|
|
15307
|
-
|
|
15308
|
-
// RFC 9207 Section 2: the plain response carries iss, because no JWT iss
|
|
15309
|
-
// claim is available to identify the issuer here.
|
|
15310
|
-
expect([...queryOf(location).keys()].sort()).toEqual(['code', 'iss', 'state']);
|
|
15311
|
-
expect(queryOf(location).get('state')).toBe('jarm-state');
|
|
15312
|
-
expect(queryOf(location).get('iss')).toBe('http://localhost:3000');
|
|
15313
|
-
});
|
|
15314
|
-
|
|
15315
|
-
it('should return the plain query error when the End-User denies consent', async () => {
|
|
15316
|
-
const { location } = await interactiveFlow(
|
|
15317
|
-
authorizeUrl({ response_mode: 'query.jwt' }),
|
|
15318
|
-
'deny',
|
|
15319
|
-
);
|
|
15320
|
-
|
|
15321
|
-
expect([...queryOf(location).keys()].sort()).toEqual(['error', 'iss', 'state']);
|
|
15322
|
-
expect(queryOf(location).get('error')).toBe('access_denied');
|
|
15323
|
-
expect(queryOf(location).get('state')).toBe('jarm-state');
|
|
15324
|
-
});
|
|
15325
|
-
|
|
15326
|
-
it('should exchange the plainly delivered code for tokens', async () => {
|
|
15327
|
-
const { location } = await interactiveFlow(authorizeUrl({ response_mode: 'query.jwt' }));
|
|
15328
|
-
const res = await app.request('/token', {
|
|
15329
|
-
method: 'POST',
|
|
15330
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
15331
|
-
body: new URLSearchParams({
|
|
15332
|
-
grant_type: 'authorization_code',
|
|
15333
|
-
code: queryOf(location).get('code') ?? '',
|
|
15334
|
-
redirect_uri: REDIRECT_URI,
|
|
15335
|
-
client_id: 'c-conf',
|
|
15336
|
-
client_secret: 's',
|
|
15337
|
-
code_verifier: PKCE_VERIFIER,
|
|
15338
|
-
}).toString(),
|
|
15339
|
-
});
|
|
15340
|
-
|
|
15341
|
-
expect(res.status).toBe(200);
|
|
15342
|
-
expect((await res.json()).token_type).toBe('Bearer');
|
|
15343
|
-
});
|
|
15344
|
-
});
|
|
15345
|
-
|
|
15346
|
-
describe('Error response (JARM Section 2.1)', () => {
|
|
15347
|
-
${jarmPromptNoneErrorTest()} });
|
|
15348
|
-
`;
|
|
15349
|
-
}
|
|
15350
15354
|
function jarmPromptNoneErrorTest() {
|
|
15351
15355
|
return ` it('should return a signed error JWT for a prompt=none request with no session', async () => {
|
|
15352
15356
|
// OIDC Core 1.0 Section 3.1.2.1: prompt=none without a session is
|
|
@@ -17050,19 +17054,11 @@ ${refreshTokenExpectation} });
|
|
|
17050
17054
|
});
|
|
17051
17055
|
`;
|
|
17052
17056
|
}
|
|
17053
|
-
export function jarmConformanceBlock(features
|
|
17057
|
+
export function jarmConformanceBlock(features) {
|
|
17054
17058
|
if (!features.jarm)
|
|
17055
17059
|
return '';
|
|
17056
|
-
const interactiveResponseTests =
|
|
17057
|
-
|
|
17058
|
-
: jarmInteractiveConsentJwtBlock();
|
|
17059
|
-
const jarmAuthorizeRouteResponsesComment = consentResponseMode === 'plain'
|
|
17060
|
-
? ` // These paths answer inside the authorize route — a Route Handler, which
|
|
17061
|
-
// shares the signing key provider with /.well-known/jwks.json — so they do
|
|
17062
|
-
// produce a verifiable JARM response even though the consent step above
|
|
17063
|
-
// cannot.
|
|
17064
|
-
`
|
|
17065
|
-
: ` // 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
|
|
17066
17062
|
// route reads it back, so a store that drops unknown fields would answer in
|
|
17067
17063
|
// plain query. These paths, by contrast, answer inside the authorize route
|
|
17068
17064
|
// itself and never touch the store round trip.
|
|
@@ -18279,6 +18275,7 @@ import { rpInitiatedLogoutConfig } from './routes/logout.js';`
|
|
|
18279
18275
|
return `import { ${vitestNames} } from 'vitest';
|
|
18280
18276
|
import type { SigningKeyProvider, SigningKey } from '${corePkg}';
|
|
18281
18277
|
import { Hono } from 'hono';
|
|
18278
|
+
import { html } from 'hono/html';
|
|
18282
18279
|
${exportPublicJwkImport}import { createApp, validateSigningKeySet } from './app.js';
|
|
18283
18280
|
import { applyOidc } from './apply.js';
|
|
18284
18281
|
import { createInMemoryClientResolver, type RegisteredClient${googleLoginConfigTypeImport} } from './config.js';
|
|
@@ -18368,24 +18365,15 @@ ${persistentStorageConformanceBlock()}
|
|
|
18368
18365
|
expect(consentHtml.includes('"><script>alert(1)</script>')).toBe(true);
|
|
18369
18366
|
});
|
|
18370
18367
|
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18374
|
-
headers: { 'X-View-Renderer': 'custom' },
|
|
18375
|
-
});
|
|
18376
|
-
const rendered = renderView(customResponse, { status: 400 });
|
|
18377
|
-
|
|
18378
|
-
expect(rendered).toBe(customResponse);
|
|
18379
|
-
expect(rendered.status).toBe(202);
|
|
18380
|
-
expect(rendered.headers.get('X-View-Renderer')).toBe('custom');
|
|
18381
|
-
});
|
|
18382
|
-
|
|
18383
|
-
it('should render a custom HTML string returned by the error view', async () => {
|
|
18384
|
-
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 () => {
|
|
18385
18371
|
const customApp = createApp({
|
|
18386
18372
|
signingKeyProvider,
|
|
18387
18373
|
clientResolver: createInMemoryClientResolver(testClients),
|
|
18388
|
-
views: {
|
|
18374
|
+
views: {
|
|
18375
|
+
errorPage: (params) => html\`<!DOCTYPE html><p>custom \${params.error}: \${params.errorDescription}</p>\`,
|
|
18376
|
+
},
|
|
18389
18377
|
});
|
|
18390
18378
|
const res = await customApp.request(
|
|
18391
18379
|
'/authorize?response_type=code&client_id=c-conf' +
|
|
@@ -18396,7 +18384,9 @@ ${persistentStorageConformanceBlock()}
|
|
|
18396
18384
|
|
|
18397
18385
|
expect(res.status).toBe(400);
|
|
18398
18386
|
expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8');
|
|
18399
|
-
expect(await res.text()).toBe(
|
|
18387
|
+
expect(await res.text()).toBe(
|
|
18388
|
+
'<!DOCTYPE html><p>custom invalid_request: redirect_uri not registered</p>',
|
|
18389
|
+
);
|
|
18400
18390
|
});
|
|
18401
18391
|
});
|
|
18402
18392
|
|
|
@@ -18735,17 +18725,7 @@ ${introspectionConformanceBlock(features)}
|
|
|
18735
18725
|
// Pinned to the default error page so a regression in the rendered markup
|
|
18736
18726
|
// (or a missing error_description) is caught exactly.
|
|
18737
18727
|
expect(body).toBe(
|
|
18738
|
-
|
|
18739
|
-
'<!DOCTYPE html>',
|
|
18740
|
-
'<html>',
|
|
18741
|
-
'<head><title>Error</title></head>',
|
|
18742
|
-
'<body>',
|
|
18743
|
-
' <h1>Error</h1>',
|
|
18744
|
-
' <p>invalid_request</p>',
|
|
18745
|
-
' <p>redirect_uri not registered</p>',
|
|
18746
|
-
'</body>',
|
|
18747
|
-
'</html>',
|
|
18748
|
-
].join('\\n'),
|
|
18728
|
+
${defaultErrorPageBodyExpectation('invalid_request', 'redirect_uri not registered', 'jsx')},
|
|
18749
18729
|
);
|
|
18750
18730
|
});
|
|
18751
18731
|
|
|
@@ -18764,7 +18744,7 @@ ${introspectionConformanceBlock(features)}
|
|
|
18764
18744
|
});
|
|
18765
18745
|
});
|
|
18766
18746
|
});
|
|
18767
|
-
${transactionBindingConformanceBlock(features)}${
|
|
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)}});
|
|
18768
18748
|
`;
|
|
18769
18749
|
}
|
|
18770
18750
|
//# sourceMappingURL=templates.js.map
|