@_mustachio/openauth 0.13.2 → 0.14.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/client.js +61 -62
- package/dist/esm/domain/authorize.js +21 -3
- package/dist/esm/domain/callback.js +30 -21
- package/dist/esm/domain/client-credentials.js +16 -0
- package/dist/esm/domain/method-dispatch.js +1 -0
- package/dist/esm/domain/method-route.js +12 -0
- package/dist/esm/domain/mount.js +29 -0
- package/dist/esm/domain/refresh.js +16 -5
- package/dist/esm/domain/register.js +8 -2
- package/dist/esm/domain/state-envelope.js +19 -0
- package/dist/esm/domain/subject.js +35 -0
- package/dist/esm/domain/token.js +15 -0
- package/dist/esm/http/handlers/method-route.js +8 -3
- package/dist/esm/http/handlers/token.js +2 -0
- package/dist/esm/http/middleware/tenant.js +5 -20
- package/dist/esm/index.js +4 -0
- package/dist/esm/methods/code.js +9 -8
- package/dist/esm/methods/oauth2-generic.js +2 -1
- package/dist/esm/methods/passkey.js +3 -2
- package/dist/esm/methods/password.js +9 -8
- package/dist/esm/methods/saml-sp/metadata.js +2 -1
- package/dist/types/client.d.ts +61 -40
- 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 +6 -5
- package/dist/types/domain/callback.d.ts.map +1 -1
- package/dist/types/domain/client-credentials.d.ts +3 -1
- package/dist/types/domain/client-credentials.d.ts.map +1 -1
- package/dist/types/domain/method-dispatch.d.ts +5 -0
- package/dist/types/domain/method-dispatch.d.ts.map +1 -1
- package/dist/types/domain/method-route.d.ts +6 -0
- package/dist/types/domain/method-route.d.ts.map +1 -1
- package/dist/types/domain/mount.d.ts +90 -0
- package/dist/types/domain/mount.d.ts.map +1 -0
- package/dist/types/domain/refresh.d.ts.map +1 -1
- package/dist/types/domain/register.d.ts.map +1 -1
- package/dist/types/domain/state-envelope.d.ts +22 -0
- package/dist/types/domain/state-envelope.d.ts.map +1 -1
- package/dist/types/domain/subject.d.ts +48 -0
- package/dist/types/domain/subject.d.ts.map +1 -0
- package/dist/types/domain/token.d.ts +7 -1
- package/dist/types/domain/token.d.ts.map +1 -1
- package/dist/types/http/context.d.ts +3 -0
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/http/handlers/token.d.ts.map +1 -1
- package/dist/types/http/middleware/tenant.d.ts.map +1 -1
- package/dist/types/http/schemas/revocation.d.ts +4 -4
- package/dist/types/http/schemas/token.d.ts +12 -12
- package/dist/types/index.d.ts +17 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/methods/code.d.ts.map +1 -1
- package/dist/types/methods/oauth2-generic.d.ts.map +1 -1
- package/dist/types/methods/passkey.d.ts.map +1 -1
- package/dist/types/methods/password.d.ts.map +1 -1
- package/dist/types/methods/saml-sp/metadata.d.ts.map +1 -1
- package/dist/types/ports/audit-log.d.ts +25 -1
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +49 -39
- package/dist/types/types/idp.d.ts.map +1 -1
- package/dist/types/types/method.d.ts +11 -0
- package/dist/types/types/method.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/client.ts +145 -129
- package/src/domain/authorize.ts +25 -4
- package/src/domain/callback.ts +40 -36
- package/src/domain/client-credentials.ts +20 -1
- package/src/domain/method-dispatch.ts +6 -0
- package/src/domain/method-route.ts +18 -0
- package/src/domain/mount.ts +113 -0
- package/src/domain/refresh.ts +39 -10
- package/src/domain/register.ts +27 -8
- package/src/domain/state-envelope.ts +40 -0
- package/src/domain/subject.ts +103 -0
- package/src/domain/token.ts +25 -1
- package/src/http/context.ts +3 -0
- package/src/http/handlers/method-route.ts +9 -5
- package/src/http/handlers/token.ts +2 -0
- package/src/http/middleware/tenant.ts +5 -24
- package/src/index.ts +18 -2
- package/src/methods/code.ts +15 -6
- package/src/methods/oauth2-generic.ts +6 -1
- package/src/methods/passkey.ts +6 -2
- package/src/methods/password.ts +19 -4
- package/src/methods/saml-sp/metadata.ts +5 -1
- package/src/ports/audit-log.ts +26 -1
- package/src/types/idp.ts +49 -42
- package/src/types/method.ts +11 -0
package/src/methods/code.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
MethodContext,
|
|
32
32
|
MethodResult,
|
|
33
33
|
} from "../types/method"
|
|
34
|
+
import { mountedPath } from "../domain/mount"
|
|
34
35
|
import { renderForm } from "../ui/forms"
|
|
35
36
|
|
|
36
37
|
export type CodeProperties = {
|
|
@@ -120,12 +121,12 @@ export function codeMethod(
|
|
|
120
121
|
kind,
|
|
121
122
|
type: "code",
|
|
122
123
|
routes: {
|
|
123
|
-
"GET /authorize": async (
|
|
124
|
+
"GET /authorize": async (ctx) => ({
|
|
124
125
|
kind: "challenge",
|
|
125
126
|
response: htmlResponse(
|
|
126
127
|
renderForm({
|
|
127
128
|
title: titleRequest,
|
|
128
|
-
action: `/m/${id}/send
|
|
129
|
+
action: mountedPath(ctx.issuerUrl, `/m/${id}/send`),
|
|
129
130
|
fields: destinationField(destinationKind),
|
|
130
131
|
submit: "Send code",
|
|
131
132
|
}),
|
|
@@ -169,7 +170,7 @@ async function handleSend(
|
|
|
169
170
|
response: htmlResponse(
|
|
170
171
|
renderForm({
|
|
171
172
|
title: titleRequest,
|
|
172
|
-
action: `/m/${methodId}/send
|
|
173
|
+
action: mountedPath(ctx.issuerUrl, `/m/${methodId}/send`),
|
|
173
174
|
fields: destinationField(destinationKind),
|
|
174
175
|
submit: "Send code",
|
|
175
176
|
error: "Please enter a valid destination.",
|
|
@@ -193,7 +194,7 @@ async function handleSend(
|
|
|
193
194
|
response: htmlResponse(
|
|
194
195
|
renderForm({
|
|
195
196
|
title: titleVerify,
|
|
196
|
-
action: `/m/${methodId}/verify
|
|
197
|
+
action: mountedPath(ctx.issuerUrl, `/m/${methodId}/verify`),
|
|
197
198
|
fields: [
|
|
198
199
|
{
|
|
199
200
|
name: "code",
|
|
@@ -238,7 +239,13 @@ async function handleVerify(
|
|
|
238
239
|
const form = await safeForm(ctx.request)
|
|
239
240
|
const parsed = verifyBody.safeParse(form)
|
|
240
241
|
if (!parsed.success) {
|
|
241
|
-
return verifyError(
|
|
242
|
+
return verifyError(
|
|
243
|
+
ctx.issuerUrl,
|
|
244
|
+
methodId,
|
|
245
|
+
state,
|
|
246
|
+
titleVerify,
|
|
247
|
+
"Please enter the code.",
|
|
248
|
+
)
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
const submittedHash = base64url.encode(
|
|
@@ -246,6 +253,7 @@ async function handleVerify(
|
|
|
246
253
|
)
|
|
247
254
|
if (!timingSafeEqualStr(submittedHash, state.codeHash)) {
|
|
248
255
|
return verifyError(
|
|
256
|
+
ctx.issuerUrl,
|
|
249
257
|
methodId,
|
|
250
258
|
{ ...state, attempts: state.attempts + 1 },
|
|
251
259
|
titleVerify,
|
|
@@ -261,6 +269,7 @@ async function handleVerify(
|
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
function verifyError(
|
|
272
|
+
issuerUrl: string,
|
|
264
273
|
methodId: string,
|
|
265
274
|
next: CodeState,
|
|
266
275
|
titleVerify: string,
|
|
@@ -271,7 +280,7 @@ function verifyError(
|
|
|
271
280
|
response: htmlResponse(
|
|
272
281
|
renderForm({
|
|
273
282
|
title: titleVerify,
|
|
274
|
-
action: `/m/${methodId}/verify
|
|
283
|
+
action: mountedPath(issuerUrl, `/m/${methodId}/verify`),
|
|
275
284
|
fields: [
|
|
276
285
|
{
|
|
277
286
|
name: "code",
|
|
@@ -37,6 +37,7 @@ import { createRemoteJWKSet, jwtVerify } from "jose"
|
|
|
37
37
|
*/
|
|
38
38
|
type IdTokenClaims = Record<string, unknown>
|
|
39
39
|
|
|
40
|
+
import { mountedPath } from "../domain/mount"
|
|
40
41
|
import { authError } from "../types/error"
|
|
41
42
|
import type { AuthMethod, MethodContext, MethodResult } from "../types/method"
|
|
42
43
|
import type { MethodType } from "../types/tenant"
|
|
@@ -212,7 +213,11 @@ async function exchangeAndSucceed(
|
|
|
212
213
|
error: authError.internalError("flow missing on callback"),
|
|
213
214
|
}
|
|
214
215
|
}
|
|
215
|
-
|
|
216
|
+
// The `redirect_uri` on the token exchange must be byte-identical to the
|
|
217
|
+
// one sent on the authorize redirect (OAuth 2.1 §4.1.3). `flow.callbackPath`
|
|
218
|
+
// is the *inbound* route, which a path-mounted proxy has stripped the mount
|
|
219
|
+
// prefix from, so re-apply it here rather than replaying the stored path.
|
|
220
|
+
const callbackUrl = `${new URL(ctx.request.url).protocol}//${ctx.flow.callbackHost}${mountedPath(ctx.issuerUrl, ctx.flow.callbackPath)}`
|
|
216
221
|
|
|
217
222
|
const body = new URLSearchParams({
|
|
218
223
|
grant_type: "authorization_code",
|
package/src/methods/passkey.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type {
|
|
|
32
32
|
MethodContext,
|
|
33
33
|
MethodResult,
|
|
34
34
|
} from "../types/method"
|
|
35
|
+
import { mountedPath } from "../domain/mount"
|
|
35
36
|
import { renderForm } from "../ui/forms"
|
|
36
37
|
|
|
37
38
|
export type PasskeyProperties = {
|
|
@@ -146,12 +147,15 @@ export function passkeyMethod(
|
|
|
146
147
|
kind,
|
|
147
148
|
type: "passkey",
|
|
148
149
|
routes: {
|
|
149
|
-
"GET /authorize": async () => ({
|
|
150
|
+
"GET /authorize": async (ctx) => ({
|
|
150
151
|
kind: "challenge",
|
|
151
152
|
response: htmlResponse(
|
|
152
153
|
renderForm({
|
|
153
154
|
title: settings.title,
|
|
154
|
-
action:
|
|
155
|
+
action: mountedPath(
|
|
156
|
+
ctx.issuerUrl,
|
|
157
|
+
`/m/${id}/authenticate-options`,
|
|
158
|
+
),
|
|
155
159
|
fields: [
|
|
156
160
|
{
|
|
157
161
|
name: "username",
|
package/src/methods/password.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
MethodContext,
|
|
27
27
|
MethodResult,
|
|
28
28
|
} from "../types/method"
|
|
29
|
+
import { mountedPath } from "../domain/mount"
|
|
29
30
|
import { renderForm } from "../ui/forms"
|
|
30
31
|
|
|
31
32
|
export type PasswordProperties = {
|
|
@@ -140,7 +141,7 @@ async function renderLogin(
|
|
|
140
141
|
): Promise<MethodResult<PasswordProperties, PasswordState>> {
|
|
141
142
|
const body = renderForm({
|
|
142
143
|
title,
|
|
143
|
-
action: `/m/${methodId}/login
|
|
144
|
+
action: mountedPath(ctx.issuerUrl, `/m/${methodId}/login`),
|
|
144
145
|
fields: [
|
|
145
146
|
{
|
|
146
147
|
name: "email",
|
|
@@ -180,6 +181,7 @@ async function handleLogin(
|
|
|
180
181
|
const parsed = loginBodySchema.safeParse(form)
|
|
181
182
|
if (!parsed.success) {
|
|
182
183
|
return reLoginWithError(
|
|
184
|
+
ctx.issuerUrl,
|
|
183
185
|
methodId,
|
|
184
186
|
title,
|
|
185
187
|
"Please enter your email and password.",
|
|
@@ -188,12 +190,22 @@ async function handleLogin(
|
|
|
188
190
|
|
|
189
191
|
const user = await users.findByEmail(parsed.data.email, ctx.tenant.id)
|
|
190
192
|
if (!user) {
|
|
191
|
-
return reLoginWithError(
|
|
193
|
+
return reLoginWithError(
|
|
194
|
+
ctx.issuerUrl,
|
|
195
|
+
methodId,
|
|
196
|
+
title,
|
|
197
|
+
"Invalid email or password.",
|
|
198
|
+
)
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
const verified = await hasher.verify(parsed.data.password, user.passwordHash)
|
|
195
202
|
if (!verified) {
|
|
196
|
-
return reLoginWithError(
|
|
203
|
+
return reLoginWithError(
|
|
204
|
+
ctx.issuerUrl,
|
|
205
|
+
methodId,
|
|
206
|
+
title,
|
|
207
|
+
"Invalid email or password.",
|
|
208
|
+
)
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
return {
|
|
@@ -218,6 +230,7 @@ async function handleRegister(
|
|
|
218
230
|
const parsed = registerBodySchema.safeParse(form)
|
|
219
231
|
if (!parsed.success) {
|
|
220
232
|
return reLoginWithError(
|
|
233
|
+
ctx.issuerUrl,
|
|
221
234
|
methodId,
|
|
222
235
|
title,
|
|
223
236
|
"Password must be at least 8 characters and email must be valid.",
|
|
@@ -227,6 +240,7 @@ async function handleRegister(
|
|
|
227
240
|
const existing = await users.findByEmail(parsed.data.email, ctx.tenant.id)
|
|
228
241
|
if (existing) {
|
|
229
242
|
return reLoginWithError(
|
|
243
|
+
ctx.issuerUrl,
|
|
230
244
|
methodId,
|
|
231
245
|
title,
|
|
232
246
|
"An account with that email already exists.",
|
|
@@ -250,6 +264,7 @@ async function handleRegister(
|
|
|
250
264
|
}
|
|
251
265
|
|
|
252
266
|
function reLoginWithError(
|
|
267
|
+
issuerUrl: string,
|
|
253
268
|
methodId: string,
|
|
254
269
|
title: string,
|
|
255
270
|
error: string,
|
|
@@ -259,7 +274,7 @@ function reLoginWithError(
|
|
|
259
274
|
response: new Response(
|
|
260
275
|
renderForm({
|
|
261
276
|
title,
|
|
262
|
-
action: `/m/${methodId}/login
|
|
277
|
+
action: mountedPath(issuerUrl, `/m/${methodId}/login`),
|
|
263
278
|
fields: [
|
|
264
279
|
{
|
|
265
280
|
name: "email",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* `WantAssertionsSigned` are both read from config, so they state
|
|
23
23
|
* actual runtime behaviour rather than a hardcoded assumption.
|
|
24
24
|
*/
|
|
25
|
+
import { mountedPath } from "../../domain/mount"
|
|
25
26
|
import { authError } from "../../types/error"
|
|
26
27
|
import type { MethodContext, MethodResult } from "../../types/method"
|
|
27
28
|
|
|
@@ -184,7 +185,10 @@ export async function buildSpMetadata(
|
|
|
184
185
|
// served (idp.sloUrl set ⇒ /sls is in publicRoutes). Derived from the
|
|
185
186
|
// same dispatch input as the ACS so it cannot drift.
|
|
186
187
|
const cb = new URL(ctx.dispatch.callbackUrl)
|
|
187
|
-
const slsUrl = `${cb.protocol}//${cb.host}
|
|
188
|
+
const slsUrl = `${cb.protocol}//${cb.host}${mountedPath(
|
|
189
|
+
ctx.dispatch.issuerUrl,
|
|
190
|
+
`/m/${methodId}/sls`,
|
|
191
|
+
)}`
|
|
188
192
|
const xml = buildSpMetadataXml({
|
|
189
193
|
spEntityId,
|
|
190
194
|
acsUrl: ctx.dispatch.callbackUrl,
|
package/src/ports/audit-log.ts
CHANGED
|
@@ -28,13 +28,24 @@ export type AuditEvent =
|
|
|
28
28
|
flowId: string
|
|
29
29
|
}
|
|
30
30
|
| {
|
|
31
|
+
/**
|
|
32
|
+
* A method authenticated the end user and an authorization code was
|
|
33
|
+
* minted. Completes the `authorize_started` / `authorize_failed`
|
|
34
|
+
* pair, which shipped without it.
|
|
35
|
+
*/
|
|
31
36
|
kind: "authorize_succeeded"
|
|
32
37
|
tenantId: TenantId
|
|
33
38
|
clientId: string
|
|
34
39
|
methodId: string
|
|
35
40
|
methodKind: string
|
|
36
41
|
flowId: string
|
|
37
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The **upstream** identifier the method returned. Deliberately not
|
|
44
|
+
* `subjectId`: the OIDC subject is derived from the host's
|
|
45
|
+
* `success()` claim at `/token`, which has not run yet. Correlate
|
|
46
|
+
* with `token_issued.subjectId` via `clientId` + `flowId`.
|
|
47
|
+
*/
|
|
48
|
+
providerSubject: string
|
|
38
49
|
}
|
|
39
50
|
| {
|
|
40
51
|
kind: "authorize_failed"
|
|
@@ -162,6 +173,20 @@ export type AuditEvent =
|
|
|
162
173
|
/** Zod error path string. Never the raw config blob. */
|
|
163
174
|
errorPath: string
|
|
164
175
|
}
|
|
176
|
+
| {
|
|
177
|
+
/**
|
|
178
|
+
* `IdPOptions.success` returned a claim that violates the host's
|
|
179
|
+
* own `subjects` schema. A deployment fault, not RP behaviour —
|
|
180
|
+
* token issuance is refused. Carries paths, never values.
|
|
181
|
+
*/
|
|
182
|
+
kind: "invalid_subject_claim"
|
|
183
|
+
tenantId: TenantId
|
|
184
|
+
clientId: string
|
|
185
|
+
subjectType: string
|
|
186
|
+
reason: "unknown-type" | "invalid-properties"
|
|
187
|
+
/** Standard Schema issue path, or the declared type list. */
|
|
188
|
+
detail: string
|
|
189
|
+
}
|
|
165
190
|
| {
|
|
166
191
|
kind: "unknown_method_kind"
|
|
167
192
|
tenantId: TenantId
|
package/src/types/idp.ts
CHANGED
|
@@ -46,30 +46,6 @@ export type SuccessMapInput = {
|
|
|
46
46
|
context: Record<string, unknown> | null
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
50
|
-
* Optional observation hook payload — fires after the subject claim has
|
|
51
|
-
* already been minted. **Does not** influence the issued subject; use it
|
|
52
|
-
* for audit, analytics, side effects only.
|
|
53
|
-
*/
|
|
54
|
-
export type SuccessEvent = SuccessMapInput & {
|
|
55
|
-
/** The final subject claim that became the JWT `sub`. */
|
|
56
|
-
claim: SubjectClaim
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Optional observation hook payload — fires on a failed auth attempt.
|
|
61
|
-
* Carries enough id information for operators to find the offending flow
|
|
62
|
-
* / config row without leaking secrets.
|
|
63
|
-
*/
|
|
64
|
-
export type FailureEvent = {
|
|
65
|
-
tenantId: TenantId | null
|
|
66
|
-
clientId: string | null
|
|
67
|
-
methodId?: string
|
|
68
|
-
methodKind?: string
|
|
69
|
-
flowId?: string
|
|
70
|
-
error: AuthError
|
|
71
|
-
}
|
|
72
|
-
|
|
73
49
|
/**
|
|
74
50
|
* Input to the optional `IdPOptions.onLogout` hook.
|
|
75
51
|
*
|
|
@@ -217,19 +193,41 @@ export type RegisterClientResponse = {
|
|
|
217
193
|
/**
|
|
218
194
|
* Optional Dynamic Client Registration hook. Hosts that want to expose
|
|
219
195
|
* RFC 7591 client provisioning supply this; the framework validates the
|
|
220
|
-
* wire format, then defers persistence to the
|
|
221
|
-
* `/register` endpoint returns `invalid_request` so
|
|
222
|
-
* "not enabled" signal rather than a 404.
|
|
196
|
+
* wire format and mints credentials, then defers **persistence** to the
|
|
197
|
+
* host. If absent, the `/register` endpoint returns `invalid_request` so
|
|
198
|
+
* RPs receive a clear "not enabled" signal rather than a 404.
|
|
199
|
+
*
|
|
200
|
+
* The library owns credential generation — entropy, hashing, and the
|
|
201
|
+
* `ClientConfig` discriminated union, which requires `pkceRequired: true`
|
|
202
|
+
* as a literal on public clients and a `secretHash` on confidential ones.
|
|
203
|
+
* Those are protocol and security concerns, and making every host
|
|
204
|
+
* reimplement them is how they get done wrong. The host owns the table:
|
|
205
|
+
* write `client` through your own `ConfigStore` and return it.
|
|
206
|
+
*
|
|
207
|
+
* Before 0.14.0 this hook received only `{ tenant, request }` and the
|
|
208
|
+
* framework discarded what it had generated, so hosts had to mint their
|
|
209
|
+
* own — contradicting both this doc comment and `ARCHITECTURE.md`.
|
|
223
210
|
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* `RegisterClientResponse` so the RP can record it.
|
|
211
|
+
* Return the config you actually persisted. Adjusting it first is fine
|
|
212
|
+
* (narrowing `scopes`, substituting your own `id`); if you replace `id`
|
|
213
|
+
* or `secretHash`, return the matching plaintext as `secret` so the RP
|
|
214
|
+
* receives something that works.
|
|
229
215
|
*/
|
|
230
216
|
export type RegisterClient = (input: {
|
|
231
217
|
tenant: TenantContext
|
|
232
218
|
request: RegisterClientRequest
|
|
219
|
+
/**
|
|
220
|
+
* Framework-minted `ClientConfig`, ready to persist as-is. Public
|
|
221
|
+
* clients carry `pkceRequired: true`; confidential clients carry
|
|
222
|
+
* `secretHash` for `secret` below.
|
|
223
|
+
*/
|
|
224
|
+
client: ClientConfig
|
|
225
|
+
/**
|
|
226
|
+
* Plaintext secret matching `client.secretHash`. Present only for
|
|
227
|
+
* confidential clients. Return it in the result so the RP can record
|
|
228
|
+
* it — this is the only time it exists.
|
|
229
|
+
*/
|
|
230
|
+
secret?: string
|
|
233
231
|
}) => Promise<Result<{ client: ClientConfig; secret?: string }, AuthError>>
|
|
234
232
|
|
|
235
233
|
/**
|
|
@@ -255,7 +253,22 @@ export type IdPOptions = {
|
|
|
255
253
|
*/
|
|
256
254
|
resolveTenant: (req: Request) => Promise<Result<TenantId, AuthError>>
|
|
257
255
|
|
|
258
|
-
/**
|
|
256
|
+
/**
|
|
257
|
+
* Opt-in for partitioned callback hosts (recovery mechanism #2).
|
|
258
|
+
*
|
|
259
|
+
* Returns the hostname a given tenant's callbacks arrive on, so the
|
|
260
|
+
* tenant is recoverable from the `Host` header before the state
|
|
261
|
+
* envelope is verified.
|
|
262
|
+
*
|
|
263
|
+
* **The issuer's mount prefix still applies to the returned host.** If
|
|
264
|
+
* `issuerUrl` is `https://example.com/idp`, a tenant host of
|
|
265
|
+
* `acme.example.com` produces `https://acme.example.com/idp/cb/<id>`.
|
|
266
|
+
* This option varies the *authority* of one deployment — those hosts
|
|
267
|
+
* are served by this same service behind the same proxy, so they share
|
|
268
|
+
* its mount. A deployment needing partitioned hosts mounted differently
|
|
269
|
+
* from the issuer is describing two mounts, which one `issuerUrl`
|
|
270
|
+
* cannot express; a second option must not be added to paper over it.
|
|
271
|
+
*/
|
|
259
272
|
callbackHostFor?: (tenantId: TenantId) => string
|
|
260
273
|
|
|
261
274
|
/**
|
|
@@ -313,12 +326,6 @@ export type IdPOptions = {
|
|
|
313
326
|
|
|
314
327
|
theme?: ThemeConfig
|
|
315
328
|
|
|
316
|
-
hooks?: {
|
|
317
|
-
/** Observation only — does NOT influence the subject. */
|
|
318
|
-
onSuccess?: (event: SuccessEvent) => Promise<void>
|
|
319
|
-
onFailure?: (event: FailureEvent) => Promise<void>
|
|
320
|
-
}
|
|
321
|
-
|
|
322
329
|
/**
|
|
323
330
|
* Optional hook fired when an upstream provider signals that a
|
|
324
331
|
* federated session ended — SAML front-channel Single Logout today.
|
|
@@ -327,9 +334,9 @@ export type IdPOptions = {
|
|
|
327
334
|
* OIDC subject (if any) whose library-issued tokens to revoke. See
|
|
328
335
|
* the `LogoutEventInput` / `LogoutHookResult` type docs.
|
|
329
336
|
*
|
|
330
|
-
* Unlike `
|
|
331
|
-
*
|
|
332
|
-
*
|
|
337
|
+
* Unlike `AuditLog` (observation only) this hook **influences**
|
|
338
|
+
* library behaviour — its return drives token revocation — so it sits
|
|
339
|
+
* at the top level alongside `success`.
|
|
333
340
|
*
|
|
334
341
|
* Absent ⇒ the library still verifies the logout, emits a
|
|
335
342
|
* `session_logout` audit event, and returns the protocol
|
package/src/types/method.ts
CHANGED
|
@@ -113,6 +113,17 @@ export type MethodHandler<P, S> = (
|
|
|
113
113
|
export type MethodContext<S = unknown> = {
|
|
114
114
|
/** The raw Web Fetch `Request`. Web standard, not Hono `Context`. */
|
|
115
115
|
request: Request
|
|
116
|
+
/**
|
|
117
|
+
* The issuer URL of this IdP, always present.
|
|
118
|
+
*
|
|
119
|
+
* Distinct from `dispatch.issuerUrl`, which is only populated for
|
|
120
|
+
* `GET /authorize`. Methods that render their own URLs — a form action
|
|
121
|
+
* re-rendered on a validation error, say — need it on every route, and
|
|
122
|
+
* must build those URLs with `mountedPath` so they carry the
|
|
123
|
+
* deployment's mount prefix. `ctx.request.url` is not a substitute: a
|
|
124
|
+
* proxy-stripped inbound URL has already lost that prefix.
|
|
125
|
+
*/
|
|
126
|
+
issuerUrl: string
|
|
116
127
|
/** Path within the method's mount, e.g. `"/callback"` (without the `/<id>` prefix). */
|
|
117
128
|
subPath: string
|
|
118
129
|
tenant: TenantContext
|