@_mustachio/openauth 0.10.0 → 0.12.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/dist/esm/adapters/d1/index.js +18 -9
- package/dist/esm/adapters/d1/migrations.js +7 -2
- package/dist/esm/adapters/d1/session-store.js +39 -0
- package/dist/esm/adapters/d1/session.js +2 -2
- package/dist/esm/adapters/durable-object/index.js +4 -0
- package/dist/esm/adapters/durable-object/session-store.js +42 -0
- package/dist/esm/adapters/dynamo/index.js +24 -7
- package/dist/esm/adapters/dynamo/session-store.js +49 -0
- package/dist/esm/adapters/kms/index.js +6 -2
- package/dist/esm/adapters/kv/index.js +6 -2
- package/dist/esm/adapters/memory/index.js +24 -7
- package/dist/esm/adapters/memory/session-store.js +45 -0
- package/dist/esm/adapters/memory/token-store.js +13 -0
- package/dist/esm/adapters/postgres/executor.js +2 -2
- package/dist/esm/adapters/postgres/index.js +33 -10
- package/dist/esm/adapters/postgres/migrations.js +8 -2
- package/dist/esm/adapters/postgres/session-store.js +42 -0
- package/dist/esm/client.js +35 -28
- package/dist/esm/domain/authorize.js +7 -2
- package/dist/esm/domain/callback.js +124 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +46 -4
- package/dist/esm/domain/dpop.js +122 -0
- package/dist/esm/domain/id-token.js +94 -0
- package/dist/esm/domain/introspect.js +5 -1
- package/dist/esm/domain/jwt.js +30 -2
- package/dist/esm/domain/logout.js +75 -0
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +83 -1
- package/dist/esm/domain/par.js +51 -0
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/refresh.js +12 -1
- package/dist/esm/domain/register.js +74 -0
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token-exchange.js +1 -0
- package/dist/esm/domain/token.js +53 -12
- package/dist/esm/domain/userinfo.js +38 -3
- package/dist/esm/error.js +8 -8
- package/dist/esm/http/cookies.js +19 -2
- package/dist/esm/http/errors.js +4 -4
- package/dist/esm/http/handlers/authorize.js +92 -21
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/end-session.js +64 -0
- package/dist/esm/http/handlers/metadata.js +7 -4
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/par.js +53 -0
- package/dist/esm/http/handlers/register.js +46 -0
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/handlers/token.js +36 -6
- package/dist/esm/http/handlers/userinfo.js +35 -29
- package/dist/esm/http/middleware/tenant.js +6 -6
- package/dist/esm/http/router.js +10 -0
- package/dist/esm/http/schemas/authorize.js +8 -2
- package/dist/esm/http/schemas/end-session.js +13 -0
- package/dist/esm/http/schemas/par.js +9 -0
- package/dist/esm/http/schemas/register.js +16 -0
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +40 -34
- package/dist/esm/methods/oauth2-factory.js +2 -2
- package/dist/esm/methods/password-hash.js +2 -2
- package/dist/esm/methods/providers/index.js +30 -14
- package/dist/esm/methods/saml-sp/acs.js +208 -0
- package/dist/esm/methods/saml-sp/attributes.js +75 -0
- package/dist/esm/methods/saml-sp/authnrequest.js +63 -0
- package/dist/esm/methods/saml-sp/cache-provider.js +33 -0
- package/dist/esm/methods/saml-sp/cert-rotation.js +13 -0
- package/dist/esm/methods/saml-sp/factory.js +70 -0
- package/dist/esm/methods/saml-sp/index.js +7 -0
- package/dist/esm/methods/saml-sp/metadata.js +69 -0
- package/dist/esm/methods/saml-sp/method.js +29 -0
- package/dist/esm/methods/saml-sp/parse-idp-metadata.js +102 -0
- package/dist/esm/methods/saml-sp/saml-instance.js +49 -0
- package/dist/esm/methods/saml-sp/slo-initiate.js +84 -0
- package/dist/esm/methods/saml-sp/sls.js +154 -0
- package/dist/esm/methods/saml-sp/types.js +0 -0
- package/dist/esm/types/error.js +5 -0
- package/dist/esm/types/result.js +3 -3
- package/dist/esm/ui/forms.js +3 -3
- package/dist/esm/ui/index.js +8 -5
- package/dist/esm/ui/picker.js +2 -2
- package/dist/types/adapters/d1/migrations.d.ts.map +1 -1
- package/dist/types/adapters/d1/session-store.d.ts +3 -0
- package/dist/types/adapters/d1/session-store.d.ts.map +1 -1
- package/dist/types/adapters/durable-object/session-store.d.ts +3 -0
- package/dist/types/adapters/durable-object/session-store.d.ts.map +1 -1
- package/dist/types/adapters/dynamo/session-store.d.ts +3 -0
- package/dist/types/adapters/dynamo/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/session-store.d.ts +6 -1
- package/dist/types/adapters/memory/session-store.d.ts.map +1 -1
- package/dist/types/adapters/memory/token-store.d.ts +1 -0
- package/dist/types/adapters/memory/token-store.d.ts.map +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts +1 -1
- package/dist/types/adapters/postgres/migrations.d.ts.map +1 -1
- package/dist/types/adapters/postgres/session-store.d.ts +3 -0
- package/dist/types/adapters/postgres/session-store.d.ts.map +1 -1
- package/dist/types/client.d.ts +19 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/domain/authorize.d.ts.map +1 -1
- package/dist/types/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/client-credentials.d.ts +2 -0
- package/dist/types/domain/client-credentials.d.ts.map +1 -1
- package/dist/types/domain/discovery.d.ts +37 -0
- package/dist/types/domain/discovery.d.ts.map +1 -1
- package/dist/types/domain/dpop.d.ts +61 -0
- package/dist/types/domain/dpop.d.ts.map +1 -0
- package/dist/types/domain/id-token.d.ts +83 -0
- package/dist/types/domain/id-token.d.ts.map +1 -0
- package/dist/types/domain/introspect.d.ts +8 -0
- package/dist/types/domain/introspect.d.ts.map +1 -1
- package/dist/types/domain/jwt.d.ts +27 -1
- package/dist/types/domain/jwt.d.ts.map +1 -1
- package/dist/types/domain/logout.d.ts +69 -0
- package/dist/types/domain/logout.d.ts.map +1 -0
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +57 -1
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/par.d.ts +62 -0
- package/dist/types/domain/par.d.ts.map +1 -0
- package/dist/types/domain/refresh.d.ts +9 -0
- package/dist/types/domain/refresh.d.ts.map +1 -1
- package/dist/types/domain/register.d.ts +33 -0
- package/dist/types/domain/register.d.ts.map +1 -0
- package/dist/types/domain/token-exchange.d.ts +2 -0
- package/dist/types/domain/token-exchange.d.ts.map +1 -1
- package/dist/types/domain/token.d.ts +41 -1
- package/dist/types/domain/token.d.ts.map +1 -1
- package/dist/types/domain/userinfo.d.ts +44 -5
- package/dist/types/domain/userinfo.d.ts.map +1 -1
- package/dist/types/http/context.d.ts +6 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/cookies.d.ts +9 -1
- package/dist/types/http/cookies.d.ts.map +1 -1
- package/dist/types/http/handlers/authorize.d.ts.map +1 -1
- package/dist/types/http/handlers/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/end-session.d.ts +3 -0
- package/dist/types/http/handlers/end-session.d.ts.map +1 -0
- package/dist/types/http/handlers/metadata.d.ts +1 -1
- package/dist/types/http/handlers/metadata.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/par.d.ts +3 -0
- package/dist/types/http/handlers/par.d.ts.map +1 -0
- package/dist/types/http/handlers/register.d.ts +3 -0
- package/dist/types/http/handlers/register.d.ts.map +1 -0
- package/dist/types/http/handlers/token.d.ts.map +1 -1
- package/dist/types/http/handlers/userinfo.d.ts.map +1 -1
- package/dist/types/http/router.d.ts.map +1 -1
- package/dist/types/http/schemas/authorize.d.ts +40 -0
- package/dist/types/http/schemas/authorize.d.ts.map +1 -1
- package/dist/types/http/schemas/end-session.d.ts +31 -0
- package/dist/types/http/schemas/end-session.d.ts.map +1 -0
- package/dist/types/http/schemas/par.d.ts +20 -0
- package/dist/types/http/schemas/par.d.ts.map +1 -0
- package/dist/types/http/schemas/register.d.ts +42 -0
- package/dist/types/http/schemas/register.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/acs.d.ts +4 -0
- package/dist/types/methods/saml-sp/acs.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/attributes.d.ts +40 -0
- package/dist/types/methods/saml-sp/attributes.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts +4 -0
- package/dist/types/methods/saml-sp/authnrequest.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts +38 -0
- package/dist/types/methods/saml-sp/cache-provider.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts +23 -0
- package/dist/types/methods/saml-sp/cert-rotation.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/factory.d.ts +704 -0
- package/dist/types/methods/saml-sp/factory.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/index.d.ts +24 -0
- package/dist/types/methods/saml-sp/index.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/metadata.d.ts +42 -0
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/method.d.ts +17 -0
- package/dist/types/methods/saml-sp/method.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts +4 -0
- package/dist/types/methods/saml-sp/parse-idp-metadata.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts +81 -0
- package/dist/types/methods/saml-sp/saml-instance.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts +4 -0
- package/dist/types/methods/saml-sp/slo-initiate.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/sls.d.ts +4 -0
- package/dist/types/methods/saml-sp/sls.d.ts.map +1 -0
- package/dist/types/methods/saml-sp/types.d.ts +204 -0
- package/dist/types/methods/saml-sp/types.d.ts.map +1 -0
- package/dist/types/ports/audit-log.d.ts +40 -0
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/session-store.d.ts +50 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/ports/token-store.d.ts +12 -0
- package/dist/types/ports/token-store.d.ts.map +1 -1
- package/dist/types/types/authorization.d.ts +23 -0
- package/dist/types/types/authorization.d.ts.map +1 -1
- package/dist/types/types/error.d.ts +9 -0
- package/dist/types/types/error.d.ts.map +1 -1
- package/dist/types/types/flow.d.ts +10 -0
- package/dist/types/types/flow.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +162 -1
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +109 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/dist/types/types/tenant.d.ts +37 -0
- package/dist/types/types/tenant.d.ts.map +1 -1
- package/dist/types/types/token.d.ts +128 -0
- package/dist/types/types/token.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/adapters/d1/migrations.ts +5 -0
- package/src/adapters/d1/session-store.ts +68 -0
- package/src/adapters/durable-object/session-store.ts +61 -0
- package/src/adapters/dynamo/session-store.ts +63 -0
- package/src/adapters/memory/session-store.ts +83 -1
- package/src/adapters/memory/token-store.ts +16 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/client.ts +32 -0
- package/src/domain/authorize.ts +9 -0
- package/src/domain/callback.ts +220 -7
- package/src/domain/client-credentials.ts +2 -0
- package/src/domain/discovery.ts +80 -2
- package/src/domain/dpop.ts +293 -0
- package/src/domain/id-token.ts +213 -0
- package/src/domain/introspect.ts +10 -0
- package/src/domain/jwt.ts +80 -1
- package/src/domain/logout.ts +180 -0
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +195 -1
- package/src/domain/par.ts +141 -0
- package/src/domain/refresh.ts +41 -0
- package/src/domain/register.ts +168 -0
- package/src/domain/token-exchange.ts +11 -0
- package/src/domain/token.ts +134 -8
- package/src/domain/userinfo.ts +121 -6
- package/src/http/context.ts +10 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +146 -24
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/end-session.ts +106 -0
- package/src/http/handlers/metadata.ts +7 -2
- package/src/http/handlers/method-route.ts +75 -2
- package/src/http/handlers/par.ts +84 -0
- package/src/http/handlers/register.ts +70 -0
- package/src/http/handlers/token.ts +56 -0
- package/src/http/handlers/userinfo.ts +62 -38
- package/src/http/router.ts +19 -0
- package/src/http/schemas/authorize.ts +27 -0
- package/src/http/schemas/end-session.ts +23 -0
- package/src/http/schemas/par.ts +23 -0
- package/src/http/schemas/register.ts +29 -0
- package/src/index.ts +21 -1
- package/src/methods/saml-sp/acs.ts +399 -0
- package/src/methods/saml-sp/attributes.ts +137 -0
- package/src/methods/saml-sp/authnrequest.ts +128 -0
- package/src/methods/saml-sp/cache-provider.ts +76 -0
- package/src/methods/saml-sp/cert-rotation.ts +34 -0
- package/src/methods/saml-sp/factory.ts +118 -0
- package/src/methods/saml-sp/index.ts +34 -0
- package/src/methods/saml-sp/metadata.ts +204 -0
- package/src/methods/saml-sp/method.ts +65 -0
- package/src/methods/saml-sp/parse-idp-metadata.ts +164 -0
- package/src/methods/saml-sp/saml-instance.ts +173 -0
- package/src/methods/saml-sp/slo-initiate.ts +177 -0
- package/src/methods/saml-sp/sls.ts +306 -0
- package/src/methods/saml-sp/types.ts +203 -0
- package/src/ports/CONSISTENCY.md +77 -21
- package/src/ports/audit-log.ts +42 -0
- package/src/ports/session-store.ts +61 -0
- package/src/ports/token-store.ts +13 -0
- package/src/types/authorization.ts +25 -0
- package/src/types/error.ts +23 -0
- package/src/types/flow.ts +10 -0
- package/src/types/idp.ts +170 -0
- package/src/types/method.ts +110 -0
- package/src/types/tenant.ts +37 -0
- package/src/types/token.ts +135 -0
|
@@ -17,18 +17,24 @@ import { isErr } from "../../types/result"
|
|
|
17
17
|
import { startAuthorize } from "../../domain/authorize"
|
|
18
18
|
import { asTenantId } from "../../types/tenant"
|
|
19
19
|
import { authError } from "../../types/error"
|
|
20
|
-
import type {
|
|
20
|
+
import type {
|
|
21
|
+
AuthorizationRequest,
|
|
22
|
+
ClaimsRequest,
|
|
23
|
+
} from "../../types/authorization"
|
|
21
24
|
import type { PickerContext, PickerMethod } from "../../types/picker"
|
|
22
25
|
import { renderPicker as renderDefaultPicker } from "../../ui/picker"
|
|
23
26
|
|
|
24
|
-
import { applyResponsePolicy } from "../cookies"
|
|
27
|
+
import { applyResponsePolicy, cacheControlHeader } from "../cookies"
|
|
25
28
|
import type { HttpContext, HttpDeps } from "../context"
|
|
26
29
|
import {
|
|
27
30
|
authorizeDirectErrorResponse,
|
|
28
31
|
authorizeRedirectErrorResponse,
|
|
29
32
|
isNonRecoverable,
|
|
30
33
|
} from "../errors"
|
|
31
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
authorizeQuerySchema,
|
|
36
|
+
authorizeRequestUriQuerySchema,
|
|
37
|
+
} from "../schemas/authorize"
|
|
32
38
|
|
|
33
39
|
export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
34
40
|
return async (c: HttpContext): Promise<Response> => {
|
|
@@ -41,7 +47,51 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
|
41
47
|
|
|
42
48
|
const url = new URL(c.req.url)
|
|
43
49
|
const raw = Object.fromEntries(url.searchParams.entries())
|
|
44
|
-
|
|
50
|
+
|
|
51
|
+
// RFC 9126 §4: rehydrate a PAR record when `request_uri` is present.
|
|
52
|
+
// The user-agent URL carries only `client_id` + `request_uri`; the
|
|
53
|
+
// stored params drive the rest of the request through the standard
|
|
54
|
+
// parser below.
|
|
55
|
+
let workingRaw: Record<string, string> = raw
|
|
56
|
+
if (raw.request_uri !== undefined) {
|
|
57
|
+
const parParsed = authorizeRequestUriQuerySchema.safeParse(raw)
|
|
58
|
+
if (!parParsed.success) {
|
|
59
|
+
return authorizeDirectErrorResponse(
|
|
60
|
+
authError.invalidRequest(
|
|
61
|
+
parParsed.error.issues[0]?.message ?? "invalid request",
|
|
62
|
+
parParsed.error.issues[0]?.path[0]?.toString() ?? "request",
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
if (!deps.sessionStore.consumePar) {
|
|
67
|
+
return authorizeDirectErrorResponse(
|
|
68
|
+
authError.invalidRequest(
|
|
69
|
+
"session adapter does not support PAR",
|
|
70
|
+
"request_uri",
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
const par = await deps.sessionStore.consumePar(parParsed.data.request_uri)
|
|
75
|
+
if (isErr(par)) {
|
|
76
|
+
return authorizeDirectErrorResponse(
|
|
77
|
+
authError.invalidRequest(
|
|
78
|
+
"unknown or expired request_uri",
|
|
79
|
+
"request_uri",
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
if (par.value.clientId !== parParsed.data.client_id) {
|
|
84
|
+
return authorizeDirectErrorResponse(
|
|
85
|
+
authError.invalidRequest(
|
|
86
|
+
"request_uri belongs to a different client",
|
|
87
|
+
"request_uri",
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
workingRaw = par.value.params
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsed = authorizeQuerySchema.safeParse(workingRaw)
|
|
45
95
|
if (!parsed.success) {
|
|
46
96
|
const first = parsed.error.issues[0]
|
|
47
97
|
const field = first?.path[0]?.toString() ?? "request"
|
|
@@ -51,6 +101,20 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
|
51
101
|
}
|
|
52
102
|
const q = parsed.data
|
|
53
103
|
|
|
104
|
+
// RFC 9126 §2 per-client PAR enforcement: refuse direct `/authorize`
|
|
105
|
+
// when the client requires PAR and `request_uri` was absent.
|
|
106
|
+
if (raw.request_uri === undefined) {
|
|
107
|
+
const client = tenant.config.clients.find((c) => c.id === q.client_id)
|
|
108
|
+
if (client?.requirePushedAuthorizationRequests) {
|
|
109
|
+
return authorizeDirectErrorResponse(
|
|
110
|
+
authError.invalidRequest(
|
|
111
|
+
"this client requires Pushed Authorization Requests (RFC 9126)",
|
|
112
|
+
"request_uri",
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
54
118
|
if (q.response_type !== "code") {
|
|
55
119
|
// OAuth 2.1: only `code` is supported. Implicit (`token`) is removed.
|
|
56
120
|
return authorizeDirectErrorResponse({
|
|
@@ -60,6 +124,17 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
|
60
124
|
})
|
|
61
125
|
}
|
|
62
126
|
|
|
127
|
+
let claimsRequest: ClaimsRequest | undefined
|
|
128
|
+
if (q.claims !== undefined) {
|
|
129
|
+
const parsedClaims = parseClaimsParameter(q.claims)
|
|
130
|
+
if (!parsedClaims.ok) {
|
|
131
|
+
return authorizeDirectErrorResponse(
|
|
132
|
+
authError.invalidRequest(parsedClaims.error, "claims"),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
claimsRequest = parsedClaims.value
|
|
136
|
+
}
|
|
137
|
+
|
|
63
138
|
const request: AuthorizationRequest = {
|
|
64
139
|
tenantId: asTenantId(tenant.id),
|
|
65
140
|
clientId: q.client_id,
|
|
@@ -78,6 +153,7 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
|
78
153
|
...(q.prompt !== undefined ? { prompt: q.prompt } : {}),
|
|
79
154
|
...(q.ui_locales !== undefined ? { uiLocales: q.ui_locales } : {}),
|
|
80
155
|
...(q.nonce !== undefined ? { nonce: q.nonce } : {}),
|
|
156
|
+
...(claimsRequest !== undefined ? { claimsRequest } : {}),
|
|
81
157
|
}
|
|
82
158
|
|
|
83
159
|
const result = await startAuthorize(
|
|
@@ -133,7 +209,7 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
|
133
209
|
return applyResponsePolicy(out.response, {
|
|
134
210
|
setCookies: [flowCookie, ...out.setCookies],
|
|
135
211
|
cookieDefaults: deps.cookieDefaults,
|
|
136
|
-
cacheControl:
|
|
212
|
+
cacheControl: cacheControlHeader(out.cache),
|
|
137
213
|
})
|
|
138
214
|
}
|
|
139
215
|
case "issue-code": {
|
|
@@ -189,6 +265,71 @@ export function makeAuthorizeHandler(deps: HttpDeps) {
|
|
|
189
265
|
}
|
|
190
266
|
}
|
|
191
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Parse + validate the OIDC Core §5.5 `claims` parameter. Accepts a
|
|
270
|
+
* JSON string; verifies it's an object whose `userinfo` / `id_token`
|
|
271
|
+
* values, if present, are objects mapping claim names to `null` or
|
|
272
|
+
* `{essential?, value?, values?}` entries. Returns a discriminated
|
|
273
|
+
* `{ok, value}` shape so the caller can map failure → `invalid_request`
|
|
274
|
+
* without dragging the AuthError import into this helper.
|
|
275
|
+
*/
|
|
276
|
+
function parseClaimsParameter(
|
|
277
|
+
raw: string,
|
|
278
|
+
): { ok: true; value: ClaimsRequest } | { ok: false; error: string } {
|
|
279
|
+
let parsed: unknown
|
|
280
|
+
try {
|
|
281
|
+
parsed = JSON.parse(raw)
|
|
282
|
+
} catch (e) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
error: `claims parameter is not valid JSON: ${e instanceof Error ? e.message : String(e)}`,
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
289
|
+
return { ok: false, error: "claims parameter must be a JSON object" }
|
|
290
|
+
}
|
|
291
|
+
const obj = parsed as Record<string, unknown>
|
|
292
|
+
const result: ClaimsRequest = {}
|
|
293
|
+
for (const section of ["userinfo", "id_token"] as const) {
|
|
294
|
+
const v = obj[section]
|
|
295
|
+
if (v === undefined) continue
|
|
296
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
error: `claims.${section} must be a JSON object`,
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const sectionMap: Record<
|
|
303
|
+
string,
|
|
304
|
+
{ essential?: boolean; value?: unknown; values?: unknown[] } | null
|
|
305
|
+
> = {}
|
|
306
|
+
for (const [name, entry] of Object.entries(v)) {
|
|
307
|
+
if (entry === null) {
|
|
308
|
+
sectionMap[name] = null
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
if (typeof entry !== "object" || Array.isArray(entry)) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
error: `claims.${section}.${name} must be null or a JSON object`,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const e = entry as Record<string, unknown>
|
|
318
|
+
const normalized: {
|
|
319
|
+
essential?: boolean
|
|
320
|
+
value?: unknown
|
|
321
|
+
values?: unknown[]
|
|
322
|
+
} = {}
|
|
323
|
+
if (typeof e.essential === "boolean") normalized.essential = e.essential
|
|
324
|
+
if ("value" in e) normalized.value = e.value
|
|
325
|
+
if (Array.isArray(e.values)) normalized.values = e.values
|
|
326
|
+
sectionMap[name] = normalized
|
|
327
|
+
}
|
|
328
|
+
result[section] = sectionMap
|
|
329
|
+
}
|
|
330
|
+
return { ok: true, value: result }
|
|
331
|
+
}
|
|
332
|
+
|
|
192
333
|
function clearFlowCookie() {
|
|
193
334
|
return {
|
|
194
335
|
name: "idp.flow",
|
|
@@ -200,22 +341,3 @@ function clearFlowCookie() {
|
|
|
200
341
|
}
|
|
201
342
|
}
|
|
202
343
|
|
|
203
|
-
function cacheControlFor(
|
|
204
|
-
cache:
|
|
205
|
-
| {
|
|
206
|
-
maxAge?: number
|
|
207
|
-
sMaxAge?: number
|
|
208
|
-
isPrivate?: boolean
|
|
209
|
-
immutable?: boolean
|
|
210
|
-
}
|
|
211
|
-
| undefined,
|
|
212
|
-
): string {
|
|
213
|
-
if (!cache) return "no-store"
|
|
214
|
-
if ((cache.maxAge ?? -1) === 0) return "no-store"
|
|
215
|
-
const parts: string[] = []
|
|
216
|
-
if (cache.isPrivate) parts.push("private")
|
|
217
|
-
if (cache.maxAge !== undefined) parts.push(`max-age=${cache.maxAge}`)
|
|
218
|
-
if (cache.sMaxAge !== undefined) parts.push(`s-maxage=${cache.sMaxAge}`)
|
|
219
|
-
if (cache.immutable) parts.push("immutable")
|
|
220
|
-
return parts.length ? parts.join(", ") : "no-store"
|
|
221
|
-
}
|
|
@@ -16,8 +16,16 @@ import { authorizeDirectErrorResponse } from "../errors"
|
|
|
16
16
|
|
|
17
17
|
export function makeCallbackHandler(deps: HttpDeps) {
|
|
18
18
|
return async (c: HttpContext): Promise<Response> => {
|
|
19
|
+
const tenant = c.get("tenant")
|
|
19
20
|
const result = await handleCallback(
|
|
20
|
-
{
|
|
21
|
+
{
|
|
22
|
+
rawRequest: c.req.raw,
|
|
23
|
+
cookies: c.get("cookies"),
|
|
24
|
+
// Only the IdP-initiated (no-state) path reads these; the
|
|
25
|
+
// SP-initiated path derives tenant from the consumed flow.
|
|
26
|
+
...(tenant ? { tenant } : {}),
|
|
27
|
+
issuerUrl: c.get("issuerUrl"),
|
|
28
|
+
},
|
|
21
29
|
{
|
|
22
30
|
configStore: deps.configStore,
|
|
23
31
|
sessionStore: deps.sessionStore,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET|POST /end_session` — OIDC RP-Initiated Logout 1.0 §2.
|
|
3
|
+
*
|
|
4
|
+
* Per spec, both GET (query) and POST (form body) are supported. The
|
|
5
|
+
* tenant is resolved via the standard `tenantMiddleware`; the issuer URL
|
|
6
|
+
* is taken from the per-request context.
|
|
7
|
+
*
|
|
8
|
+
* Outputs follow `domain/logout.ts`:
|
|
9
|
+
* - `redirect` → 302 to the validated `post_logout_redirect_uri` with
|
|
10
|
+
* `state` echoed when present.
|
|
11
|
+
* - `ok` → 200 with a minimal plain-text body. Hosts that need a styled
|
|
12
|
+
* "logged out" page can layer their own handler over `/end_session`.
|
|
13
|
+
* - errors → 400 with a JSON OAuth-style body (never 302; an unregistered
|
|
14
|
+
* `post_logout_redirect_uri` would otherwise be an open redirector).
|
|
15
|
+
*/
|
|
16
|
+
import { endSession } from "../../domain/logout"
|
|
17
|
+
import { authError } from "../../types/error"
|
|
18
|
+
import { isErr } from "../../types/result"
|
|
19
|
+
|
|
20
|
+
import { applyResponsePolicy } from "../cookies"
|
|
21
|
+
import type { HttpContext, HttpDeps } from "../context"
|
|
22
|
+
import { tokenEndpointErrorResponse } from "../errors"
|
|
23
|
+
import { endSessionParamsSchema } from "../schemas/end-session"
|
|
24
|
+
|
|
25
|
+
export function makeEndSessionHandler(deps: HttpDeps) {
|
|
26
|
+
return async (c: HttpContext): Promise<Response> => {
|
|
27
|
+
const tenant = c.get("tenant")
|
|
28
|
+
if (!tenant) {
|
|
29
|
+
return tokenEndpointErrorResponse(
|
|
30
|
+
authError.invalidRequest("tenant unresolved"),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// GET → query; POST → form body. Anything else → 400.
|
|
35
|
+
let raw: Record<string, string>
|
|
36
|
+
const httpMethod = c.req.method.toUpperCase()
|
|
37
|
+
if (httpMethod === "GET") {
|
|
38
|
+
raw = Object.fromEntries(new URL(c.req.url).searchParams.entries())
|
|
39
|
+
} else if (httpMethod === "POST") {
|
|
40
|
+
const ct = (c.req.header("content-type") ?? "").toLowerCase()
|
|
41
|
+
if (!ct.startsWith("application/x-www-form-urlencoded")) {
|
|
42
|
+
return tokenEndpointErrorResponse(
|
|
43
|
+
authError.invalidRequest(
|
|
44
|
+
"POST /end_session requires application/x-www-form-urlencoded body",
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
raw = Object.fromEntries(new URLSearchParams(await c.req.raw.text()))
|
|
49
|
+
} else {
|
|
50
|
+
return tokenEndpointErrorResponse(
|
|
51
|
+
authError.invalidRequest(`unsupported method "${httpMethod}"`),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const parsed = endSessionParamsSchema.safeParse(raw)
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
return tokenEndpointErrorResponse(
|
|
58
|
+
authError.invalidRequest(
|
|
59
|
+
parsed.error.issues[0]?.message ?? "invalid request",
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
const q = parsed.data
|
|
64
|
+
|
|
65
|
+
const res = await endSession(
|
|
66
|
+
{
|
|
67
|
+
...(q.id_token_hint !== undefined
|
|
68
|
+
? { idTokenHint: q.id_token_hint }
|
|
69
|
+
: {}),
|
|
70
|
+
...(q.client_id !== undefined ? { clientId: q.client_id } : {}),
|
|
71
|
+
...(q.post_logout_redirect_uri !== undefined
|
|
72
|
+
? { postLogoutRedirectUri: q.post_logout_redirect_uri }
|
|
73
|
+
: {}),
|
|
74
|
+
...(q.state !== undefined ? { state: q.state } : {}),
|
|
75
|
+
},
|
|
76
|
+
tenant,
|
|
77
|
+
{
|
|
78
|
+
configStore: deps.configStore,
|
|
79
|
+
tokenStore: deps.tokenStore,
|
|
80
|
+
keyStore: deps.keyStore,
|
|
81
|
+
...(deps.auditLog ? { auditLog: deps.auditLog } : {}),
|
|
82
|
+
issuerUrl: c.get("issuerUrl"),
|
|
83
|
+
clock: deps.clock,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if (isErr(res)) return tokenEndpointErrorResponse(res.error)
|
|
88
|
+
|
|
89
|
+
if (res.value.kind === "redirect") {
|
|
90
|
+
return applyResponsePolicy(
|
|
91
|
+
new Response(null, {
|
|
92
|
+
status: 302,
|
|
93
|
+
headers: { location: res.value.url },
|
|
94
|
+
}),
|
|
95
|
+
{ setCookies: [], cookieDefaults: deps.cookieDefaults },
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
return new Response("Logged out.", {
|
|
99
|
+
status: 200,
|
|
100
|
+
headers: {
|
|
101
|
+
"content-type": "text/plain; charset=utf-8",
|
|
102
|
+
"cache-control": "no-store",
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -16,9 +16,14 @@ import { isErr } from "../../types/result"
|
|
|
16
16
|
import type { HttpContext, HttpDeps } from "../context"
|
|
17
17
|
import { tokenEndpointErrorResponse } from "../errors"
|
|
18
18
|
|
|
19
|
-
export function makeDiscoveryHandler(
|
|
19
|
+
export function makeDiscoveryHandler(deps: HttpDeps) {
|
|
20
20
|
return async (c: HttpContext): Promise<Response> => {
|
|
21
|
-
const doc = buildDiscoveryDocument({
|
|
21
|
+
const doc = buildDiscoveryDocument({
|
|
22
|
+
issuerUrl: c.get("issuerUrl"),
|
|
23
|
+
...(deps.customScopeClaims !== undefined
|
|
24
|
+
? { customScopeClaims: deps.customScopeClaims }
|
|
25
|
+
: {}),
|
|
26
|
+
})
|
|
22
27
|
return new Response(JSON.stringify(doc), {
|
|
23
28
|
status: 200,
|
|
24
29
|
headers: {
|
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
* `domain/method-route.handleMethodRoute` orchestrator handles flow lookup
|
|
8
8
|
* + dispatch + result translation.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
handleMethodRoute,
|
|
12
|
+
handlePublicMethodRoute,
|
|
13
|
+
} from "../../domain/method-route"
|
|
14
|
+
import type { RouteKey } from "../../domain/method-dispatch"
|
|
11
15
|
import { authError } from "../../types/error"
|
|
12
16
|
import { isErr } from "../../types/result"
|
|
13
17
|
|
|
14
|
-
import { applyResponsePolicy } from "../cookies"
|
|
18
|
+
import { applyResponsePolicy, cacheControlHeader } from "../cookies"
|
|
15
19
|
import type { HttpContext, HttpDeps } from "../context"
|
|
16
20
|
import { authorizeDirectErrorResponse } from "../errors"
|
|
17
21
|
|
|
@@ -23,6 +27,75 @@ export function makeMethodRouteHandler(deps: HttpDeps) {
|
|
|
23
27
|
authError.invalidRequest("tenant unresolved"),
|
|
24
28
|
)
|
|
25
29
|
}
|
|
30
|
+
|
|
31
|
+
// Public fast-path (additive — leaves the cookie-gated path below
|
|
32
|
+
// byte-identical for every non-public request). Only a clean hit —
|
|
33
|
+
// well-formed `/m/<id>/<sub>`, method resolves, and the resolved
|
|
34
|
+
// method explicitly lists this route key in `publicRoutes` — is
|
|
35
|
+
// diverted here, cookie-free. Anything else falls through to the
|
|
36
|
+
// original flow-cookie-gated logic with its original error
|
|
37
|
+
// ordering. `methodCache.resolve` is a cache lookup; the duplicate
|
|
38
|
+
// resolve on the gated path is a cache hit.
|
|
39
|
+
{
|
|
40
|
+
const u = new URL(c.req.url)
|
|
41
|
+
const segs = u.pathname.split("/").filter(Boolean)
|
|
42
|
+
if (segs.length >= 3 && segs[0] === "m") {
|
|
43
|
+
const methodId = segs[1]!
|
|
44
|
+
const subPath = "/" + segs.slice(2).join("/")
|
|
45
|
+
const httpMethod =
|
|
46
|
+
c.req.method.toUpperCase() === "POST" ? "POST" : "GET"
|
|
47
|
+
const routeKey = `${httpMethod} ${subPath}` as RouteKey
|
|
48
|
+
const resolved = await deps.methodCache.resolve(
|
|
49
|
+
tenant.config,
|
|
50
|
+
methodId,
|
|
51
|
+
)
|
|
52
|
+
if (
|
|
53
|
+
!isErr(resolved) &&
|
|
54
|
+
resolved.value.publicRoutes?.includes(routeKey)
|
|
55
|
+
) {
|
|
56
|
+
const issuerUrl = c.get("issuerUrl")
|
|
57
|
+
const callbackHost =
|
|
58
|
+
deps.callbackHostFor?.(tenant.id) ?? new URL(issuerUrl).host
|
|
59
|
+
// Mirrors the ACS URL derivation in domain/authorize.ts
|
|
60
|
+
// (callbackHost/path/url). Source of truth is there; the
|
|
61
|
+
// metadata.test.ts anti-drift test asserts the emitted
|
|
62
|
+
// entityID/ACS equal what buildAuthnRequestRedirect derives.
|
|
63
|
+
const callbackUrl = `${new URL(issuerUrl).protocol}//${callbackHost}/cb/${methodId}`
|
|
64
|
+
const pub = await handlePublicMethodRoute(
|
|
65
|
+
{
|
|
66
|
+
rawRequest: c.req.raw,
|
|
67
|
+
tenant,
|
|
68
|
+
method: resolved.value,
|
|
69
|
+
route: routeKey,
|
|
70
|
+
subPath,
|
|
71
|
+
dispatch: { state: "", callbackUrl, issuerUrl },
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
sessionStore: deps.sessionStore,
|
|
75
|
+
tokenStore: deps.tokenStore,
|
|
76
|
+
clock: deps.clock,
|
|
77
|
+
...(deps.auditLog ? { auditLog: deps.auditLog } : {}),
|
|
78
|
+
...(deps.onLogout ? { onLogout: deps.onLogout } : {}),
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
if (isErr(pub)) return authorizeDirectErrorResponse(pub.error)
|
|
82
|
+
if (pub.value.kind === "denied") {
|
|
83
|
+
return applyResponsePolicy(
|
|
84
|
+
new Response(`access_denied: ${pub.value.reason}`, {
|
|
85
|
+
status: 403,
|
|
86
|
+
headers: { "content-type": "text/plain" },
|
|
87
|
+
}),
|
|
88
|
+
{ cookieDefaults: deps.cookieDefaults },
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
return applyResponsePolicy(pub.value.response, {
|
|
92
|
+
cacheControl: cacheControlHeader(pub.value.cache),
|
|
93
|
+
cookieDefaults: deps.cookieDefaults,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
const cookies = c.get("cookies")
|
|
27
100
|
const flowId = cookies.get("idp.flow")
|
|
28
101
|
if (!flowId) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /par` — Pushed Authorization Requests (RFC 9126 §2).
|
|
3
|
+
*
|
|
4
|
+
* Body: `application/x-www-form-urlencoded` carrying the same parameter
|
|
5
|
+
* set the RP would otherwise put on `GET /authorize`, plus `client_id`
|
|
6
|
+
* and optionally `client_secret`. Client credentials may alternatively
|
|
7
|
+
* ride in `Authorization: Basic` (RFC 6749 §2.3.1).
|
|
8
|
+
*
|
|
9
|
+
* Response: HTTP 201, JSON `{request_uri, expires_in}` on success.
|
|
10
|
+
*/
|
|
11
|
+
import { resolveClientCredentials } from "../../domain/client-auth"
|
|
12
|
+
import { pushAuthorizationRequest } from "../../domain/par"
|
|
13
|
+
import { authError } from "../../types/error"
|
|
14
|
+
import { isErr } from "../../types/result"
|
|
15
|
+
|
|
16
|
+
import type { HttpContext, HttpDeps } from "../context"
|
|
17
|
+
import { tokenEndpointErrorResponse } from "../errors"
|
|
18
|
+
import { parBodySchema } from "../schemas/par"
|
|
19
|
+
|
|
20
|
+
export function makeParHandler(deps: HttpDeps) {
|
|
21
|
+
return async (c: HttpContext): Promise<Response> => {
|
|
22
|
+
const tenant = c.get("tenant")
|
|
23
|
+
if (!tenant) {
|
|
24
|
+
return tokenEndpointErrorResponse(
|
|
25
|
+
authError.invalidRequest("tenant unresolved"),
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
const ct = (c.req.header("content-type") ?? "").toLowerCase()
|
|
29
|
+
if (!ct.startsWith("application/x-www-form-urlencoded")) {
|
|
30
|
+
return tokenEndpointErrorResponse(
|
|
31
|
+
authError.invalidRequest(
|
|
32
|
+
"body must be application/x-www-form-urlencoded",
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
const raw = Object.fromEntries(
|
|
37
|
+
new URLSearchParams(await c.req.raw.text()).entries(),
|
|
38
|
+
)
|
|
39
|
+
const parsed = parBodySchema.safeParse(raw)
|
|
40
|
+
if (!parsed.success) {
|
|
41
|
+
return tokenEndpointErrorResponse(
|
|
42
|
+
authError.invalidRequest(
|
|
43
|
+
parsed.error.issues[0]?.message ?? "invalid request",
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
const creds = resolveClientCredentials({
|
|
48
|
+
authorizationHeader: c.req.header("authorization") ?? null,
|
|
49
|
+
bodyClientId: parsed.data.client_id,
|
|
50
|
+
bodyClientSecret: parsed.data.client_secret,
|
|
51
|
+
})
|
|
52
|
+
const clientId = creds?.clientId ?? parsed.data.client_id
|
|
53
|
+
|
|
54
|
+
// Strip auth-only fields from the param record we persist — they
|
|
55
|
+
// belong to `/par`, not to the rehydrated `/authorize` request.
|
|
56
|
+
const { client_secret: _strip, ...paramsForAuthorize } = parsed.data
|
|
57
|
+
void _strip
|
|
58
|
+
|
|
59
|
+
const res = await pushAuthorizationRequest(
|
|
60
|
+
{
|
|
61
|
+
clientId,
|
|
62
|
+
...(creds?.clientSecret !== undefined
|
|
63
|
+
? { clientSecret: creds.clientSecret }
|
|
64
|
+
: {}),
|
|
65
|
+
params: paramsForAuthorize,
|
|
66
|
+
},
|
|
67
|
+
tenant,
|
|
68
|
+
{
|
|
69
|
+
configStore: deps.configStore,
|
|
70
|
+
sessionStore: deps.sessionStore,
|
|
71
|
+
...(deps.auditLog ? { auditLog: deps.auditLog } : {}),
|
|
72
|
+
clock: deps.clock,
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
if (isErr(res)) return tokenEndpointErrorResponse(res.error)
|
|
76
|
+
return new Response(JSON.stringify(res.value), {
|
|
77
|
+
status: 201,
|
|
78
|
+
headers: {
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
"cache-control": "no-store",
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `POST /register` — RFC 7591 Dynamic Client Registration.
|
|
3
|
+
*
|
|
4
|
+
* Body: `application/json`. Tenant resolved via the standard middleware.
|
|
5
|
+
* The framework validates structure here and defers persistence to the
|
|
6
|
+
* `IdPOptions.registerClient` hook (in `domain/register.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Per RFC 7591 §3.2.1, success returns HTTP 201 with JSON body.
|
|
9
|
+
*/
|
|
10
|
+
import { registerNewClient } from "../../domain/register"
|
|
11
|
+
import { authError } from "../../types/error"
|
|
12
|
+
import type { RegisterClientRequest } from "../../types/idp"
|
|
13
|
+
import { isErr } from "../../types/result"
|
|
14
|
+
|
|
15
|
+
import type { HttpContext, HttpDeps } from "../context"
|
|
16
|
+
import { tokenEndpointErrorResponse } from "../errors"
|
|
17
|
+
import { registerBodySchema } from "../schemas/register"
|
|
18
|
+
|
|
19
|
+
export function makeRegisterHandler(deps: HttpDeps) {
|
|
20
|
+
return async (c: HttpContext): Promise<Response> => {
|
|
21
|
+
const tenant = c.get("tenant")
|
|
22
|
+
if (!tenant) {
|
|
23
|
+
return tokenEndpointErrorResponse(
|
|
24
|
+
authError.invalidRequest("tenant unresolved"),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
const ct = (c.req.header("content-type") ?? "").toLowerCase()
|
|
28
|
+
if (!ct.startsWith("application/json")) {
|
|
29
|
+
return tokenEndpointErrorResponse(
|
|
30
|
+
authError.invalidRequest("body must be application/json"),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
let raw: unknown
|
|
34
|
+
try {
|
|
35
|
+
raw = await c.req.raw.json()
|
|
36
|
+
} catch (e) {
|
|
37
|
+
const reason = e instanceof Error ? e.message : String(e)
|
|
38
|
+
return tokenEndpointErrorResponse(
|
|
39
|
+
authError.invalidRequest(`malformed JSON: ${reason}`),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
const parsed = registerBodySchema.safeParse(raw)
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
return tokenEndpointErrorResponse(
|
|
45
|
+
authError.invalidRequest(
|
|
46
|
+
parsed.error.issues[0]?.message ?? "invalid request",
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
// Schema is `.passthrough()` (RFC 7591 allows extension metadata
|
|
51
|
+
// fields); the declared `RegisterClientRequest` fields are all
|
|
52
|
+
// statically present in `parsed.data`. This cast just drops the
|
|
53
|
+
// passthrough's `[k: string]: unknown` index signature so the
|
|
54
|
+
// downstream domain function sees the typed contract.
|
|
55
|
+
const request = parsed.data as RegisterClientRequest
|
|
56
|
+
|
|
57
|
+
const res = await registerNewClient(request, tenant, {
|
|
58
|
+
...(deps.registerClient ? { registerClient: deps.registerClient } : {}),
|
|
59
|
+
clock: deps.clock,
|
|
60
|
+
})
|
|
61
|
+
if (isErr(res)) return tokenEndpointErrorResponse(res.error)
|
|
62
|
+
return new Response(JSON.stringify(res.value), {
|
|
63
|
+
status: 201,
|
|
64
|
+
headers: {
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
"cache-control": "no-store",
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|