@_mustachio/openauth 0.11.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 +23 -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 +27 -27
- package/dist/esm/domain/authorize.js +2 -2
- package/dist/esm/domain/callback.js +121 -7
- package/dist/esm/domain/client-auth.js +2 -2
- package/dist/esm/domain/crypto.js +14 -14
- package/dist/esm/domain/discovery.js +2 -2
- package/dist/esm/domain/dpop.js +4 -4
- package/dist/esm/domain/id-token.js +5 -5
- package/dist/esm/domain/jwt.js +4 -4
- package/dist/esm/domain/method-dispatch.js +27 -1
- package/dist/esm/domain/method-route.js +80 -1
- package/dist/esm/domain/par.js +2 -2
- package/dist/esm/domain/pkce.js +2 -2
- package/dist/esm/domain/revoke.js +2 -2
- package/dist/esm/domain/state-envelope.js +2 -2
- package/dist/esm/domain/token.js +5 -5
- 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 +3 -19
- package/dist/esm/http/handlers/callback.js +7 -1
- package/dist/esm/http/handlers/metadata.js +2 -2
- package/dist/esm/http/handlers/method-route.js +47 -2
- package/dist/esm/http/handlers/revocation.js +2 -2
- package/dist/esm/http/middleware/tenant.js +6 -6
- package/dist/esm/http/schemas/authorize.js +2 -2
- package/dist/esm/http/schemas/revocation.js +2 -2
- package/dist/esm/http/schemas/token.js +7 -7
- package/dist/esm/index.js +33 -32
- 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/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 +3 -0
- package/dist/types/adapters/memory/session-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/domain/callback.d.ts +16 -1
- package/dist/types/domain/callback.d.ts.map +1 -1
- 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/http/context.d.ts +3 -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/callback.d.ts.map +1 -1
- package/dist/types/http/handlers/method-route.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- 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 +18 -5
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/session-store.d.ts +23 -0
- package/dist/types/ports/session-store.d.ts.map +1 -1
- package/dist/types/types/idp.d.ts +71 -0
- 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/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 +39 -0
- package/src/adapters/postgres/migrations.ts +6 -0
- package/src/adapters/postgres/session-store.ts +69 -0
- package/src/domain/callback.ts +215 -7
- package/src/domain/method-dispatch.ts +53 -1
- package/src/domain/method-route.ts +190 -1
- package/src/http/context.ts +6 -0
- package/src/http/cookies.ts +19 -1
- package/src/http/handlers/authorize.ts +2 -21
- package/src/http/handlers/callback.ts +9 -1
- package/src/http/handlers/method-route.ts +75 -2
- package/src/index.ts +3 -0
- 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 +25 -0
- package/src/ports/audit-log.ts +18 -5
- package/src/ports/session-store.ts +28 -0
- package/src/types/idp.ts +74 -0
- package/src/types/method.ts +110 -0
|
@@ -7,21 +7,42 @@ import { dispatchMethod } from "./method-dispatch";
|
|
|
7
7
|
import { saveEncryptedCode } from "./token";
|
|
8
8
|
import { verifyStateEnvelope } from "./state-envelope";
|
|
9
9
|
import { AUTH_CODE_TTL_MS } from "./authorize";
|
|
10
|
+
async function extractCallbackState(req) {
|
|
11
|
+
const fromQuery = new URL(req.url).searchParams.get("state");
|
|
12
|
+
if (fromQuery)
|
|
13
|
+
return fromQuery;
|
|
14
|
+
if (req.method !== "POST")
|
|
15
|
+
return null;
|
|
16
|
+
const ct = req.headers.get("content-type") ?? "";
|
|
17
|
+
if (!ct.includes("application/x-www-form-urlencoded"))
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const body = await req.clone().text();
|
|
21
|
+
const form = new URLSearchParams(body);
|
|
22
|
+
const v = form.get("state") ?? form.get("RelayState");
|
|
23
|
+
return v && v.length > 0 ? v : null;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
10
28
|
async function handleCallback(input, deps) {
|
|
11
29
|
const url = new URL(input.rawRequest.url);
|
|
12
|
-
const state =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
const state = await extractCallbackState(input.rawRequest);
|
|
31
|
+
const envelopeRes = state ? await verifyStateEnvelope(state, deps.stateKeys) : null;
|
|
32
|
+
if (envelopeRes === null || isErr(envelopeRes)) {
|
|
33
|
+
const idp = await tryIdpInitiated(input, deps, url);
|
|
34
|
+
if (idp)
|
|
35
|
+
return idp;
|
|
36
|
+
if (!state) {
|
|
37
|
+
return err(authError.invalidRequest("missing state parameter", "state"));
|
|
38
|
+
}
|
|
18
39
|
await safeAudit(deps, {
|
|
19
40
|
kind: "flow_replay_attempt",
|
|
20
41
|
tenantId: null,
|
|
21
42
|
flowId: "unknown",
|
|
22
43
|
timestamp: deps.clock()
|
|
23
44
|
});
|
|
24
|
-
return err(authError.invalidRequest(envelopeRes.error.description, "state"));
|
|
45
|
+
return err(authError.invalidRequest(envelopeRes && isErr(envelopeRes) ? envelopeRes.error.description : "invalid state", "state"));
|
|
25
46
|
}
|
|
26
47
|
const envelope = envelopeRes.value;
|
|
27
48
|
const flowRes = await deps.sessionStore.consumeFlow(envelope.flowId);
|
|
@@ -168,6 +189,99 @@ async function translate(result, flow, deps) {
|
|
|
168
189
|
return err(result.error);
|
|
169
190
|
}
|
|
170
191
|
}
|
|
192
|
+
async function tryIdpInitiated(input, deps, url) {
|
|
193
|
+
if (input.rawRequest.method !== "POST")
|
|
194
|
+
return null;
|
|
195
|
+
if (!input.tenant || !input.issuerUrl)
|
|
196
|
+
return null;
|
|
197
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
198
|
+
if (segments.length < 2 || segments[0] !== "cb")
|
|
199
|
+
return null;
|
|
200
|
+
const methodId = segments[1];
|
|
201
|
+
const methodRes = await deps.methodCache.resolve(input.tenant.config, methodId);
|
|
202
|
+
if (isErr(methodRes))
|
|
203
|
+
return null;
|
|
204
|
+
const method = methodRes.value;
|
|
205
|
+
if (method.unsolicitedCallback !== true)
|
|
206
|
+
return null;
|
|
207
|
+
const callbackUrl = `${url.protocol}//${url.host}/cb/${methodId}`;
|
|
208
|
+
const dispatched = await dispatchMethod({
|
|
209
|
+
method,
|
|
210
|
+
route: "GET /callback",
|
|
211
|
+
tenant: input.tenant,
|
|
212
|
+
request: input.rawRequest,
|
|
213
|
+
subPath: "/callback",
|
|
214
|
+
flow: null,
|
|
215
|
+
cookies: input.cookies,
|
|
216
|
+
sessionStore: deps.sessionStore,
|
|
217
|
+
dispatch: { state: "", callbackUrl, issuerUrl: input.issuerUrl }
|
|
218
|
+
});
|
|
219
|
+
if (isErr(dispatched))
|
|
220
|
+
return err(dispatched.error);
|
|
221
|
+
const result = dispatched.value;
|
|
222
|
+
if (result.kind === "challenge") {
|
|
223
|
+
return ok({
|
|
224
|
+
kind: "challenge",
|
|
225
|
+
response: result.response,
|
|
226
|
+
setCookies: result.setCookies ?? [],
|
|
227
|
+
...result.cache !== undefined ? { cache: result.cache } : {}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (result.kind === "denied") {
|
|
231
|
+
await safeAudit(deps, {
|
|
232
|
+
kind: "authorize_failed",
|
|
233
|
+
tenantId: input.tenant.id,
|
|
234
|
+
clientId: "idp-initiated",
|
|
235
|
+
methodId,
|
|
236
|
+
methodKind: method.kind,
|
|
237
|
+
flowId: "idp-initiated",
|
|
238
|
+
reason: result.reason,
|
|
239
|
+
timestamp: deps.clock()
|
|
240
|
+
});
|
|
241
|
+
return ok({
|
|
242
|
+
kind: "denied",
|
|
243
|
+
reason: result.reason,
|
|
244
|
+
setCookies: result.setCookies ?? []
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (result.kind === "error")
|
|
248
|
+
return err(result.error);
|
|
249
|
+
const binding = result.unsolicitedBinding;
|
|
250
|
+
if (!binding) {
|
|
251
|
+
return err(authError.internalError(`method "${methodId}" handled an unsolicited callback but returned ` + `no unsolicitedBinding`));
|
|
252
|
+
}
|
|
253
|
+
const client = input.tenant.config.clients.find((c) => c.id === binding.clientId);
|
|
254
|
+
if (!client) {
|
|
255
|
+
return err(authError.invalidRequest(`IdP-initiated binding references unknown client "${binding.clientId}"`));
|
|
256
|
+
}
|
|
257
|
+
if (!client.redirectUris.includes(binding.redirectUri)) {
|
|
258
|
+
return err(authError.invalidRequest(`IdP-initiated redirect_uri "${binding.redirectUri}" is not ` + `registered for client "${binding.clientId}"`));
|
|
259
|
+
}
|
|
260
|
+
const code = (deps.newCodeId ?? randomToken)();
|
|
261
|
+
const now = deps.clock();
|
|
262
|
+
const saved = await saveEncryptedCode(code, {
|
|
263
|
+
tenantId: input.tenant.id,
|
|
264
|
+
clientId: binding.clientId,
|
|
265
|
+
appRedirectUri: binding.redirectUri,
|
|
266
|
+
appState: null,
|
|
267
|
+
scopes: binding.scopes,
|
|
268
|
+
methodId,
|
|
269
|
+
methodKind: method.kind,
|
|
270
|
+
context: input.tenant.request.custom ?? null,
|
|
271
|
+
providerSubject: result.providerSubject,
|
|
272
|
+
properties: result.properties,
|
|
273
|
+
authTime: Math.floor(now / 1000),
|
|
274
|
+
expiresAt: now + AUTH_CODE_TTL_MS
|
|
275
|
+
}, AUTH_CODE_TTL_MS, { keyStore: deps.keyStore, tokenStore: deps.tokenStore });
|
|
276
|
+
if (isErr(saved))
|
|
277
|
+
return err(saved.error);
|
|
278
|
+
return ok({
|
|
279
|
+
kind: "issue-code",
|
|
280
|
+
code,
|
|
281
|
+
appRedirectUri: binding.redirectUri,
|
|
282
|
+
appState: null
|
|
283
|
+
});
|
|
284
|
+
}
|
|
171
285
|
function normalizePath(p) {
|
|
172
286
|
return p.replace(/\/+$/, "");
|
|
173
287
|
}
|
|
@@ -79,19 +79,19 @@ function generateSymmetricKey() {
|
|
|
79
79
|
return buf;
|
|
80
80
|
}
|
|
81
81
|
export {
|
|
82
|
-
|
|
83
|
-
timingSafeEqualStr,
|
|
84
|
-
timingSafeEqual,
|
|
85
|
-
sha256,
|
|
86
|
-
randomToken,
|
|
87
|
-
randomId,
|
|
88
|
-
randomHex,
|
|
89
|
-
randomBytes,
|
|
90
|
-
importHmacKey,
|
|
91
|
-
hmacVerify,
|
|
92
|
-
hmacSign,
|
|
93
|
-
generateSymmetricKey,
|
|
94
|
-
encryptPayload,
|
|
82
|
+
base64url,
|
|
95
83
|
decryptPayload,
|
|
96
|
-
|
|
84
|
+
encryptPayload,
|
|
85
|
+
generateSymmetricKey,
|
|
86
|
+
hmacSign,
|
|
87
|
+
hmacVerify,
|
|
88
|
+
importHmacKey,
|
|
89
|
+
randomBytes,
|
|
90
|
+
randomHex,
|
|
91
|
+
randomId,
|
|
92
|
+
randomToken,
|
|
93
|
+
sha256,
|
|
94
|
+
timingSafeEqual,
|
|
95
|
+
timingSafeEqualStr,
|
|
96
|
+
utf8
|
|
97
97
|
};
|
package/dist/esm/domain/dpop.js
CHANGED
|
@@ -114,9 +114,9 @@ function stringifyError(e) {
|
|
|
114
114
|
return e instanceof Error ? e.message : String(e);
|
|
115
115
|
}
|
|
116
116
|
export {
|
|
117
|
-
|
|
118
|
-
computeAth,
|
|
119
|
-
canonicalHtu,
|
|
117
|
+
DEFAULT_DPOP_IAT_TOLERANCE_SEC,
|
|
120
118
|
DEFAULT_DPOP_JTI_TTL_MS,
|
|
121
|
-
|
|
119
|
+
canonicalHtu,
|
|
120
|
+
computeAth,
|
|
121
|
+
verifyDpopProof
|
|
122
122
|
};
|
|
@@ -85,10 +85,10 @@ function shouldIssueIdToken(scopes) {
|
|
|
85
85
|
return scopes.includes("openid");
|
|
86
86
|
}
|
|
87
87
|
export {
|
|
88
|
-
|
|
89
|
-
pickScopedClaims,
|
|
90
|
-
deriveAmr,
|
|
91
|
-
computeAtHash,
|
|
88
|
+
DEFAULT_ID_TOKEN_TTL_MS,
|
|
92
89
|
buildIdTokenClaims,
|
|
93
|
-
|
|
90
|
+
computeAtHash,
|
|
91
|
+
deriveAmr,
|
|
92
|
+
pickScopedClaims,
|
|
93
|
+
shouldIssueIdToken
|
|
94
94
|
};
|
package/dist/esm/domain/jwt.js
CHANGED
|
@@ -13,7 +13,8 @@ async function dispatchMethod(input) {
|
|
|
13
13
|
flow: input.flow,
|
|
14
14
|
methodState: input.flow?.methodState ?? null,
|
|
15
15
|
cookies: input.cookies,
|
|
16
|
-
dispatch: input.dispatch
|
|
16
|
+
dispatch: input.dispatch,
|
|
17
|
+
methodScratch: buildMethodScratch(input.sessionStore, input.tenant.id, input.method.id)
|
|
17
18
|
};
|
|
18
19
|
let result;
|
|
19
20
|
try {
|
|
@@ -31,6 +32,31 @@ async function dispatchMethod(input) {
|
|
|
31
32
|
}
|
|
32
33
|
return ok(result);
|
|
33
34
|
}
|
|
35
|
+
function buildMethodScratch(store, tenantId, methodId) {
|
|
36
|
+
const prefix = `scratch:${tenantId}:${methodId}:`;
|
|
37
|
+
const scope = (key) => `${prefix}${key}`;
|
|
38
|
+
const unsupported = (op) => authError.internalError(`SessionStore does not implement ${op}; methodScratch is unavailable on this adapter`);
|
|
39
|
+
return {
|
|
40
|
+
put: async (key, value, ttlMs) => {
|
|
41
|
+
if (!store.saveScratch)
|
|
42
|
+
return err(unsupported("saveScratch"));
|
|
43
|
+
if (ttlMs <= 0) {
|
|
44
|
+
return err(authError.internalError(`methodScratch.put: ttlMs must be positive, got ${ttlMs}`));
|
|
45
|
+
}
|
|
46
|
+
return store.saveScratch(scope(key), value, ttlMs);
|
|
47
|
+
},
|
|
48
|
+
get: async (key) => {
|
|
49
|
+
if (!store.readScratch)
|
|
50
|
+
return err(unsupported("readScratch"));
|
|
51
|
+
return store.readScratch(scope(key));
|
|
52
|
+
},
|
|
53
|
+
delete: async (key) => {
|
|
54
|
+
if (!store.deleteScratch)
|
|
55
|
+
return err(unsupported("deleteScratch"));
|
|
56
|
+
return store.deleteScratch(scope(key));
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
34
60
|
export {
|
|
35
61
|
dispatchMethod
|
|
36
62
|
};
|
|
@@ -5,6 +5,7 @@ import { safeAudit } from "./audit";
|
|
|
5
5
|
import { AUTH_CODE_TTL_MS } from "./authorize";
|
|
6
6
|
import { randomToken } from "./crypto";
|
|
7
7
|
import { dispatchMethod } from "./method-dispatch";
|
|
8
|
+
import { revokeAllForSubject } from "./revoke";
|
|
8
9
|
import { saveEncryptedCode } from "./token";
|
|
9
10
|
async function handleMethodRoute(input, deps) {
|
|
10
11
|
const flowRes = await deps.sessionStore.readFlow(input.flowId);
|
|
@@ -106,6 +107,84 @@ async function translate(result, flow, deps) {
|
|
|
106
107
|
return err(result.error);
|
|
107
108
|
}
|
|
108
109
|
}
|
|
110
|
+
async function handlePublicMethodRoute(input, deps) {
|
|
111
|
+
if (!input.method.publicRoutes?.includes(input.route)) {
|
|
112
|
+
return err(authError.invalidRequest(`route "${input.route}" is not public on method "${input.method.id}"`, "path"));
|
|
113
|
+
}
|
|
114
|
+
const dispatched = await dispatchMethod({
|
|
115
|
+
method: input.method,
|
|
116
|
+
route: input.route,
|
|
117
|
+
tenant: input.tenant,
|
|
118
|
+
request: input.rawRequest,
|
|
119
|
+
subPath: input.subPath,
|
|
120
|
+
flow: null,
|
|
121
|
+
cookies: new Map,
|
|
122
|
+
sessionStore: deps.sessionStore,
|
|
123
|
+
dispatch: input.dispatch
|
|
124
|
+
});
|
|
125
|
+
if (isErr(dispatched))
|
|
126
|
+
return err(dispatched.error);
|
|
127
|
+
const r = dispatched.value;
|
|
128
|
+
switch (r.kind) {
|
|
129
|
+
case "challenge": {
|
|
130
|
+
if (r.logout) {
|
|
131
|
+
const sideEffect = await runUpstreamLogout(input, deps, r.logout);
|
|
132
|
+
if (isErr(sideEffect))
|
|
133
|
+
return err(sideEffect.error);
|
|
134
|
+
}
|
|
135
|
+
return ok({
|
|
136
|
+
kind: "challenge",
|
|
137
|
+
response: r.response,
|
|
138
|
+
...r.cache !== undefined ? { cache: r.cache } : {}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
case "denied":
|
|
142
|
+
return ok({ kind: "denied", reason: r.reason });
|
|
143
|
+
case "success":
|
|
144
|
+
return err(authError.internalError(`public route "${input.route}" on method "${input.method.id}" ` + `returned success — a flowless route cannot authenticate`));
|
|
145
|
+
case "error":
|
|
146
|
+
return err(r.error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function runUpstreamLogout(input, deps, logout) {
|
|
150
|
+
let hookResult = undefined;
|
|
151
|
+
if (deps.onLogout) {
|
|
152
|
+
const evt = {
|
|
153
|
+
tenant: input.tenant,
|
|
154
|
+
methodId: input.method.id,
|
|
155
|
+
methodKind: input.method.kind,
|
|
156
|
+
reason: "upstream_slo",
|
|
157
|
+
...logout.nameId !== undefined ? { nameId: logout.nameId } : {},
|
|
158
|
+
...logout.sessionIndex !== undefined ? { sessionIndex: logout.sessionIndex } : {}
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
hookResult = await deps.onLogout(evt);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
return err(authError.internalError(`onLogout hook threw during upstream Single Logout for method ` + `"${input.method.id}"`, e));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const revokeSubject = hookResult && typeof hookResult === "object" ? hookResult.revokeSubject : undefined;
|
|
167
|
+
if (revokeSubject) {
|
|
168
|
+
const revoked = await revokeAllForSubject(input.tenant.id, revokeSubject, {
|
|
169
|
+
tokenStore: deps.tokenStore,
|
|
170
|
+
...deps.auditLog ? { auditLog: deps.auditLog } : {},
|
|
171
|
+
clock: deps.clock
|
|
172
|
+
});
|
|
173
|
+
if (isErr(revoked))
|
|
174
|
+
return err(revoked.error);
|
|
175
|
+
}
|
|
176
|
+
await safeAudit(deps, {
|
|
177
|
+
kind: "session_logout",
|
|
178
|
+
tenantId: input.tenant.id,
|
|
179
|
+
via: "upstream_slo",
|
|
180
|
+
methodId: input.method.id,
|
|
181
|
+
methodKind: input.method.kind,
|
|
182
|
+
...revokeSubject ? { subjectId: revokeSubject } : {},
|
|
183
|
+
timestamp: deps.clock()
|
|
184
|
+
});
|
|
185
|
+
return ok(undefined);
|
|
186
|
+
}
|
|
109
187
|
export {
|
|
110
|
-
handleMethodRoute
|
|
188
|
+
handleMethodRoute,
|
|
189
|
+
handlePublicMethodRoute
|
|
111
190
|
};
|
package/dist/esm/domain/par.js
CHANGED
package/dist/esm/domain/pkce.js
CHANGED
|
@@ -75,6 +75,6 @@ function isEnvelopeShape(value) {
|
|
|
75
75
|
return typeof v.tenantId === "string" && typeof v.flowId === "string" && typeof v.nonce === "string" && typeof v.kid === "string";
|
|
76
76
|
}
|
|
77
77
|
export {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
mintStateEnvelope,
|
|
79
|
+
verifyStateEnvelope
|
|
80
80
|
};
|
package/dist/esm/domain/token.js
CHANGED
|
@@ -237,10 +237,10 @@ async function hashTokenForAudit(token) {
|
|
|
237
237
|
return base64url.encode(await sha256(utf8.encode(token))).slice(0, 16);
|
|
238
238
|
}
|
|
239
239
|
export {
|
|
240
|
-
|
|
241
|
-
mintTokens,
|
|
242
|
-
hashClientSecret,
|
|
243
|
-
exchangeCode,
|
|
240
|
+
DEFAULT_ACCESS_TTL_MS,
|
|
244
241
|
DEFAULT_REFRESH_TTL_MS,
|
|
245
|
-
|
|
242
|
+
exchangeCode,
|
|
243
|
+
hashClientSecret,
|
|
244
|
+
mintTokens,
|
|
245
|
+
saveEncryptedCode
|
|
246
246
|
};
|
package/dist/esm/error.js
CHANGED
|
@@ -61,13 +61,13 @@ class InvalidAuthorizationCodeError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
export {
|
|
64
|
-
|
|
65
|
-
UnauthorizedClientError,
|
|
66
|
-
OauthError,
|
|
67
|
-
MissingProviderError,
|
|
68
|
-
MissingParameterError,
|
|
69
|
-
InvalidSubjectError,
|
|
70
|
-
InvalidRefreshTokenError,
|
|
64
|
+
InvalidAccessTokenError,
|
|
71
65
|
InvalidAuthorizationCodeError,
|
|
72
|
-
|
|
66
|
+
InvalidRefreshTokenError,
|
|
67
|
+
InvalidSubjectError,
|
|
68
|
+
MissingParameterError,
|
|
69
|
+
MissingProviderError,
|
|
70
|
+
OauthError,
|
|
71
|
+
UnauthorizedClientError,
|
|
72
|
+
UnknownStateError
|
|
73
73
|
};
|
package/dist/esm/http/cookies.js
CHANGED
|
@@ -67,6 +67,22 @@ var STRIPPED_HEADERS = [
|
|
|
67
67
|
"permissions-policy",
|
|
68
68
|
"cache-control"
|
|
69
69
|
];
|
|
70
|
+
function cacheControlHeader(cache) {
|
|
71
|
+
if (!cache)
|
|
72
|
+
return "no-store";
|
|
73
|
+
if ((cache.maxAge ?? -1) === 0)
|
|
74
|
+
return "no-store";
|
|
75
|
+
const parts = [];
|
|
76
|
+
if (cache.isPrivate)
|
|
77
|
+
parts.push("private");
|
|
78
|
+
if (cache.maxAge !== undefined)
|
|
79
|
+
parts.push(`max-age=${cache.maxAge}`);
|
|
80
|
+
if (cache.sMaxAge !== undefined)
|
|
81
|
+
parts.push(`s-maxage=${cache.sMaxAge}`);
|
|
82
|
+
if (cache.immutable)
|
|
83
|
+
parts.push("immutable");
|
|
84
|
+
return parts.length ? parts.join(", ") : "no-store";
|
|
85
|
+
}
|
|
70
86
|
function applyResponsePolicy(response, opts = {}) {
|
|
71
87
|
const headers = new Headers(response.headers);
|
|
72
88
|
const stripped = [];
|
|
@@ -92,7 +108,8 @@ function applyResponsePolicy(response, opts = {}) {
|
|
|
92
108
|
});
|
|
93
109
|
}
|
|
94
110
|
export {
|
|
95
|
-
|
|
111
|
+
applyResponsePolicy,
|
|
112
|
+
cacheControlHeader,
|
|
96
113
|
parseCookieHeader,
|
|
97
|
-
|
|
114
|
+
serializeSetCookie
|
|
98
115
|
};
|
package/dist/esm/http/errors.js
CHANGED
|
@@ -63,9 +63,9 @@ function isNonRecoverable(error) {
|
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
65
|
export {
|
|
66
|
-
|
|
67
|
-
publicErrorCode,
|
|
68
|
-
isNonRecoverable,
|
|
66
|
+
authorizeDirectErrorResponse,
|
|
69
67
|
authorizeRedirectErrorResponse,
|
|
70
|
-
|
|
68
|
+
isNonRecoverable,
|
|
69
|
+
publicErrorCode,
|
|
70
|
+
tokenEndpointErrorResponse
|
|
71
71
|
};
|
|
@@ -4,7 +4,7 @@ import { startAuthorize } from "../../domain/authorize";
|
|
|
4
4
|
import { asTenantId } from "../../types/tenant";
|
|
5
5
|
import { authError } from "../../types/error";
|
|
6
6
|
import { renderPicker as renderDefaultPicker } from "../../ui/picker";
|
|
7
|
-
import { applyResponsePolicy } from "../cookies";
|
|
7
|
+
import { applyResponsePolicy, cacheControlHeader } from "../cookies";
|
|
8
8
|
import {
|
|
9
9
|
authorizeDirectErrorResponse,
|
|
10
10
|
authorizeRedirectErrorResponse,
|
|
@@ -48,7 +48,7 @@ function makeAuthorizeHandler(deps) {
|
|
|
48
48
|
}
|
|
49
49
|
const q = parsed.data;
|
|
50
50
|
if (raw.request_uri === undefined) {
|
|
51
|
-
const client = tenant.config.clients.find((
|
|
51
|
+
const client = tenant.config.clients.find((c) => c.id === q.client_id);
|
|
52
52
|
if (client?.requirePushedAuthorizationRequests) {
|
|
53
53
|
return authorizeDirectErrorResponse(authError.invalidRequest("this client requires Pushed Authorization Requests (RFC 9126)", "request_uri"));
|
|
54
54
|
}
|
|
@@ -123,7 +123,7 @@ function makeAuthorizeHandler(deps) {
|
|
|
123
123
|
return applyResponsePolicy(out.response, {
|
|
124
124
|
setCookies: [flowCookie, ...out.setCookies],
|
|
125
125
|
cookieDefaults: deps.cookieDefaults,
|
|
126
|
-
cacheControl:
|
|
126
|
+
cacheControl: cacheControlHeader(out.cache)
|
|
127
127
|
});
|
|
128
128
|
}
|
|
129
129
|
case "issue-code": {
|
|
@@ -229,22 +229,6 @@ function clearFlowCookie() {
|
|
|
229
229
|
maxAge: 0
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
|
-
function cacheControlFor(cache) {
|
|
233
|
-
if (!cache)
|
|
234
|
-
return "no-store";
|
|
235
|
-
if ((cache.maxAge ?? -1) === 0)
|
|
236
|
-
return "no-store";
|
|
237
|
-
const parts = [];
|
|
238
|
-
if (cache.isPrivate)
|
|
239
|
-
parts.push("private");
|
|
240
|
-
if (cache.maxAge !== undefined)
|
|
241
|
-
parts.push(`max-age=${cache.maxAge}`);
|
|
242
|
-
if (cache.sMaxAge !== undefined)
|
|
243
|
-
parts.push(`s-maxage=${cache.sMaxAge}`);
|
|
244
|
-
if (cache.immutable)
|
|
245
|
-
parts.push("immutable");
|
|
246
|
-
return parts.length ? parts.join(", ") : "no-store";
|
|
247
|
-
}
|
|
248
232
|
export {
|
|
249
233
|
makeAuthorizeHandler
|
|
250
234
|
};
|
|
@@ -5,7 +5,13 @@ import { applyResponsePolicy } from "../cookies";
|
|
|
5
5
|
import { authorizeDirectErrorResponse } from "../errors";
|
|
6
6
|
function makeCallbackHandler(deps) {
|
|
7
7
|
return async (c) => {
|
|
8
|
-
const
|
|
8
|
+
const tenant = c.get("tenant");
|
|
9
|
+
const result = await handleCallback({
|
|
10
|
+
rawRequest: c.req.raw,
|
|
11
|
+
cookies: c.get("cookies"),
|
|
12
|
+
...tenant ? { tenant } : {},
|
|
13
|
+
issuerUrl: c.get("issuerUrl")
|
|
14
|
+
}, {
|
|
9
15
|
configStore: deps.configStore,
|
|
10
16
|
sessionStore: deps.sessionStore,
|
|
11
17
|
tokenStore: deps.tokenStore,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// src/http/handlers/method-route.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
handleMethodRoute,
|
|
4
|
+
handlePublicMethodRoute
|
|
5
|
+
} from "../../domain/method-route";
|
|
3
6
|
import { authError } from "../../types/error";
|
|
4
7
|
import { isErr } from "../../types/result";
|
|
5
|
-
import { applyResponsePolicy } from "../cookies";
|
|
8
|
+
import { applyResponsePolicy, cacheControlHeader } from "../cookies";
|
|
6
9
|
import { authorizeDirectErrorResponse } from "../errors";
|
|
7
10
|
function makeMethodRouteHandler(deps) {
|
|
8
11
|
return async (c) => {
|
|
@@ -10,6 +13,48 @@ function makeMethodRouteHandler(deps) {
|
|
|
10
13
|
if (!tenant) {
|
|
11
14
|
return authorizeDirectErrorResponse(authError.invalidRequest("tenant unresolved"));
|
|
12
15
|
}
|
|
16
|
+
{
|
|
17
|
+
const u = new URL(c.req.url);
|
|
18
|
+
const segs = u.pathname.split("/").filter(Boolean);
|
|
19
|
+
if (segs.length >= 3 && segs[0] === "m") {
|
|
20
|
+
const methodId = segs[1];
|
|
21
|
+
const subPath = "/" + segs.slice(2).join("/");
|
|
22
|
+
const httpMethod = c.req.method.toUpperCase() === "POST" ? "POST" : "GET";
|
|
23
|
+
const routeKey = `${httpMethod} ${subPath}`;
|
|
24
|
+
const resolved = await deps.methodCache.resolve(tenant.config, methodId);
|
|
25
|
+
if (!isErr(resolved) && resolved.value.publicRoutes?.includes(routeKey)) {
|
|
26
|
+
const issuerUrl = c.get("issuerUrl");
|
|
27
|
+
const callbackHost = deps.callbackHostFor?.(tenant.id) ?? new URL(issuerUrl).host;
|
|
28
|
+
const callbackUrl = `${new URL(issuerUrl).protocol}//${callbackHost}/cb/${methodId}`;
|
|
29
|
+
const pub = await handlePublicMethodRoute({
|
|
30
|
+
rawRequest: c.req.raw,
|
|
31
|
+
tenant,
|
|
32
|
+
method: resolved.value,
|
|
33
|
+
route: routeKey,
|
|
34
|
+
subPath,
|
|
35
|
+
dispatch: { state: "", callbackUrl, issuerUrl }
|
|
36
|
+
}, {
|
|
37
|
+
sessionStore: deps.sessionStore,
|
|
38
|
+
tokenStore: deps.tokenStore,
|
|
39
|
+
clock: deps.clock,
|
|
40
|
+
...deps.auditLog ? { auditLog: deps.auditLog } : {},
|
|
41
|
+
...deps.onLogout ? { onLogout: deps.onLogout } : {}
|
|
42
|
+
});
|
|
43
|
+
if (isErr(pub))
|
|
44
|
+
return authorizeDirectErrorResponse(pub.error);
|
|
45
|
+
if (pub.value.kind === "denied") {
|
|
46
|
+
return applyResponsePolicy(new Response(`access_denied: ${pub.value.reason}`, {
|
|
47
|
+
status: 403,
|
|
48
|
+
headers: { "content-type": "text/plain" }
|
|
49
|
+
}), { cookieDefaults: deps.cookieDefaults });
|
|
50
|
+
}
|
|
51
|
+
return applyResponsePolicy(pub.value.response, {
|
|
52
|
+
cacheControl: cacheControlHeader(pub.value.cache),
|
|
53
|
+
cookieDefaults: deps.cookieDefaults
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
13
58
|
const cookies = c.get("cookies");
|
|
14
59
|
const flowId = cookies.get("idp.flow");
|
|
15
60
|
if (!flowId) {
|