@_mustachio/openauth 0.10.0 → 0.11.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/memory/session-store.js +22 -0
- package/dist/esm/adapters/memory/token-store.js +13 -0
- package/dist/esm/client.js +9 -2
- package/dist/esm/domain/authorize.js +5 -0
- package/dist/esm/domain/callback.js +3 -0
- package/dist/esm/domain/discovery.js +44 -2
- 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 +28 -0
- package/dist/esm/domain/logout.js +75 -0
- package/dist/esm/domain/method-route.js +3 -0
- package/dist/esm/domain/par.js +51 -0
- package/dist/esm/domain/refresh.js +12 -1
- package/dist/esm/domain/register.js +74 -0
- package/dist/esm/domain/token-exchange.js +1 -0
- package/dist/esm/domain/token.js +48 -7
- package/dist/esm/domain/userinfo.js +38 -3
- package/dist/esm/http/handlers/authorize.js +90 -3
- package/dist/esm/http/handlers/end-session.js +64 -0
- package/dist/esm/http/handlers/metadata.js +5 -2
- package/dist/esm/http/handlers/par.js +53 -0
- package/dist/esm/http/handlers/register.js +46 -0
- package/dist/esm/http/handlers/token.js +36 -6
- package/dist/esm/http/handlers/userinfo.js +35 -29
- package/dist/esm/http/router.js +10 -0
- package/dist/esm/http/schemas/authorize.js +7 -1
- 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/index.js +7 -2
- package/dist/esm/types/error.js +5 -0
- package/dist/types/adapters/memory/session-store.d.ts +3 -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/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/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/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 +4 -1
- package/dist/types/http/context.d.ts.map +1 -1
- package/dist/types/http/handlers/authorize.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/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/ports/audit-log.d.ts +27 -0
- package/dist/types/ports/audit-log.d.ts.map +1 -1
- package/dist/types/ports/session-store.d.ts +27 -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 +91 -1
- package/dist/types/types/idp.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 +1 -1
- package/src/adapters/memory/session-store.ts +44 -1
- package/src/adapters/memory/token-store.ts +16 -0
- package/src/client.ts +32 -0
- package/src/domain/authorize.ts +9 -0
- package/src/domain/callback.ts +5 -0
- 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-route.ts +5 -0
- 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 +4 -0
- package/src/http/handlers/authorize.ts +144 -3
- package/src/http/handlers/end-session.ts +106 -0
- package/src/http/handlers/metadata.ts +7 -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 +18 -1
- package/src/ports/CONSISTENCY.md +52 -21
- package/src/ports/audit-log.ts +29 -0
- package/src/ports/session-store.ts +33 -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 +96 -0
- package/src/types/tenant.ts +37 -0
- package/src/types/token.ts +135 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/domain/register.ts
|
|
2
|
+
import { authError } from "../types/error";
|
|
3
|
+
import { err, isErr, ok } from "../types/result";
|
|
4
|
+
import { randomId, randomToken } from "./crypto";
|
|
5
|
+
import { hashClientSecret } from "./token";
|
|
6
|
+
async function registerNewClient(request, tenant, deps) {
|
|
7
|
+
if (!deps.registerClient) {
|
|
8
|
+
return err(authError.invalidRequest("dynamic client registration is not enabled on this deployment"));
|
|
9
|
+
}
|
|
10
|
+
if (!Array.isArray(request.redirect_uris) || request.redirect_uris.length === 0) {
|
|
11
|
+
return err(authError.invalidRequest("redirect_uris must be a non-empty array", "redirect_uris"));
|
|
12
|
+
}
|
|
13
|
+
for (const uri of request.redirect_uris) {
|
|
14
|
+
try {
|
|
15
|
+
new URL(uri);
|
|
16
|
+
} catch {
|
|
17
|
+
return err(authError.invalidRequest(`redirect_uri "${uri}" is not a valid absolute URI`, "redirect_uris"));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const authMethod = request.token_endpoint_auth_method ?? "client_secret_basic";
|
|
21
|
+
const isPublic = authMethod === "none";
|
|
22
|
+
const clientId = (deps.newClientId ?? randomId)();
|
|
23
|
+
const grantTypes = request.grant_types ?? ["authorization_code"];
|
|
24
|
+
const responseTypes = request.response_types ?? ["code"];
|
|
25
|
+
const scopes = request.scope ? request.scope.split(/\s+/).filter(Boolean) : ["openid"];
|
|
26
|
+
let secret;
|
|
27
|
+
let secretHash;
|
|
28
|
+
if (!isPublic) {
|
|
29
|
+
secret = (deps.newClientSecret ?? randomToken)();
|
|
30
|
+
secretHash = await hashClientSecret(secret);
|
|
31
|
+
}
|
|
32
|
+
const clientPartial = {
|
|
33
|
+
id: clientId,
|
|
34
|
+
name: request.client_name ?? clientId,
|
|
35
|
+
redirectUris: request.redirect_uris,
|
|
36
|
+
scopes,
|
|
37
|
+
grantTypes: grantTypes.filter((g) => g === "authorization_code" || g === "refresh_token" || g === "client_credentials"),
|
|
38
|
+
...request.post_logout_redirect_uris !== undefined ? { postLogoutRedirectUris: request.post_logout_redirect_uris } : {},
|
|
39
|
+
...request.sector_identifier_uri !== undefined ? { sectorIdentifier: request.sector_identifier_uri } : {}
|
|
40
|
+
};
|
|
41
|
+
const clientConfig = isPublic ? {
|
|
42
|
+
...clientPartial,
|
|
43
|
+
type: "public",
|
|
44
|
+
pkceRequired: true
|
|
45
|
+
} : {
|
|
46
|
+
...clientPartial,
|
|
47
|
+
type: "confidential",
|
|
48
|
+
secretHash,
|
|
49
|
+
pkceRequired: true
|
|
50
|
+
};
|
|
51
|
+
const hookResult = await deps.registerClient({
|
|
52
|
+
tenant,
|
|
53
|
+
request
|
|
54
|
+
});
|
|
55
|
+
if (isErr(hookResult))
|
|
56
|
+
return err(hookResult.error);
|
|
57
|
+
const persisted = hookResult.value.client;
|
|
58
|
+
const persistedSecret = hookResult.value.secret;
|
|
59
|
+
const issuedAt = Math.floor(deps.clock() / 1000);
|
|
60
|
+
return ok({
|
|
61
|
+
client_id: persisted.id,
|
|
62
|
+
...persistedSecret !== undefined ? { client_secret: persistedSecret } : {},
|
|
63
|
+
client_id_issued_at: issuedAt,
|
|
64
|
+
client_secret_expires_at: 0,
|
|
65
|
+
redirect_uris: persisted.redirectUris,
|
|
66
|
+
grant_types: persisted.grantTypes,
|
|
67
|
+
response_types: responseTypes,
|
|
68
|
+
token_endpoint_auth_method: authMethod,
|
|
69
|
+
...request.client_name !== undefined ? { client_name: request.client_name } : {}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
registerNewClient
|
|
74
|
+
};
|
|
@@ -75,6 +75,7 @@ async function exchangeToken(req, deps) {
|
|
|
75
75
|
methodId: subjectClaims.mid ?? "token_exchange",
|
|
76
76
|
methodKind: subjectClaims.mkind ?? "token_exchange",
|
|
77
77
|
scopes: requestedScopes,
|
|
78
|
+
...subjectClaims.auth_time !== undefined ? { authTime: subjectClaims.auth_time } : {},
|
|
78
79
|
...subjectClaims.aud !== undefined && subjectClaims.aud !== callerClient.id ? { audience: subjectClaims.aud } : {}
|
|
79
80
|
},
|
|
80
81
|
family: (deps.newRefreshFamily ?? randomId)(),
|
package/dist/esm/domain/token.js
CHANGED
|
@@ -12,7 +12,8 @@ import {
|
|
|
12
12
|
sha256,
|
|
13
13
|
utf8
|
|
14
14
|
} from "./crypto";
|
|
15
|
-
import {
|
|
15
|
+
import { buildIdTokenClaims, shouldIssueIdToken } from "./id-token";
|
|
16
|
+
import { signAccessToken, signIdToken } from "./jwt";
|
|
16
17
|
import { validatePkce } from "./pkce";
|
|
17
18
|
async function saveEncryptedCode(code, payload, ttl, deps) {
|
|
18
19
|
const keyResult = await deps.keyStore.currentEncryptionKey();
|
|
@@ -57,6 +58,9 @@ async function exchangeCode(req, deps) {
|
|
|
57
58
|
const authResult = await verifyClientCredentials(client, req.clientSecret);
|
|
58
59
|
if (authResult)
|
|
59
60
|
return err(authResult);
|
|
61
|
+
if (client.dpopRequired && req.dpopJkt === undefined) {
|
|
62
|
+
return err(authError.invalidDpopProof(`client "${client.id}" requires DPoP-bound tokens`));
|
|
63
|
+
}
|
|
60
64
|
if (payload.appRedirectUri !== req.redirectUri) {
|
|
61
65
|
return err(authError.invalidGrant("redirect_uri mismatch with auth code"));
|
|
62
66
|
}
|
|
@@ -104,7 +108,10 @@ async function exchangeCode(req, deps) {
|
|
|
104
108
|
const minted = await mintTokens({
|
|
105
109
|
tenant,
|
|
106
110
|
claim,
|
|
107
|
-
payload
|
|
111
|
+
payload: {
|
|
112
|
+
...payload,
|
|
113
|
+
...req.dpopJkt !== undefined ? { dpopJkt: req.dpopJkt } : {}
|
|
114
|
+
},
|
|
108
115
|
deps,
|
|
109
116
|
family: (deps.newRefreshFamily ?? randomId)()
|
|
110
117
|
});
|
|
@@ -115,11 +122,13 @@ async function mintTokens(args) {
|
|
|
115
122
|
const accessTtl = tenant.config.accessTtl !== undefined ? tenant.config.accessTtl * 1000 : DEFAULT_ACCESS_TTL_MS;
|
|
116
123
|
const refreshTtl = tenant.config.refreshTtl !== undefined ? tenant.config.refreshTtl * 1000 : DEFAULT_REFRESH_TTL_MS;
|
|
117
124
|
const now = deps.clock();
|
|
118
|
-
const
|
|
125
|
+
const receivingClient = tenant.config.clients.find((c) => c.id === payload.clientId);
|
|
126
|
+
const subjectId = await deriveSubjectId(claim, receivingClient?.sectorIdentifier);
|
|
119
127
|
const keyRes = await deps.keyStore.currentSigningKey();
|
|
120
128
|
if (isErr(keyRes))
|
|
121
129
|
return err(keyRes.error);
|
|
122
130
|
const signingKey = keyRes.value;
|
|
131
|
+
const userinfoClaimNames = Object.keys(payload.claimsRequest?.userinfo ?? {});
|
|
123
132
|
const claims = {
|
|
124
133
|
iss: deps.issuerUrl,
|
|
125
134
|
sub: subjectId,
|
|
@@ -130,7 +139,10 @@ async function mintTokens(args) {
|
|
|
130
139
|
mid: payload.methodId,
|
|
131
140
|
mkind: payload.methodKind,
|
|
132
141
|
scope: payload.scopes.join(" "),
|
|
133
|
-
claim
|
|
142
|
+
claim,
|
|
143
|
+
...payload.authTime !== undefined ? { auth_time: payload.authTime } : {},
|
|
144
|
+
...payload.dpopJkt !== undefined ? { cnf: { jkt: payload.dpopJkt } } : {},
|
|
145
|
+
...userinfoClaimNames.length > 0 ? { uic: userinfoClaimNames } : {}
|
|
134
146
|
};
|
|
135
147
|
let accessToken;
|
|
136
148
|
try {
|
|
@@ -151,6 +163,9 @@ async function mintTokens(args) {
|
|
|
151
163
|
family,
|
|
152
164
|
methodId: payload.methodId,
|
|
153
165
|
methodKind: payload.methodKind,
|
|
166
|
+
authTime: payload.authTime ?? Math.floor(now / 1000),
|
|
167
|
+
...payload.dpopJkt !== undefined ? { dpopJkt: payload.dpopJkt } : {},
|
|
168
|
+
...payload.claimsRequest !== undefined ? { claimsRequest: payload.claimsRequest } : {},
|
|
154
169
|
issuedAt: now,
|
|
155
170
|
expiresAt: now + refreshTtl
|
|
156
171
|
};
|
|
@@ -158,6 +173,28 @@ async function mintTokens(args) {
|
|
|
158
173
|
if (isErr(saved))
|
|
159
174
|
return err(saved.error);
|
|
160
175
|
}
|
|
176
|
+
let idToken;
|
|
177
|
+
if (payload.authTime !== undefined && shouldIssueIdToken(payload.scopes)) {
|
|
178
|
+
const idClaims = await buildIdTokenClaims({
|
|
179
|
+
issuerUrl: deps.issuerUrl,
|
|
180
|
+
audience: payload.clientId,
|
|
181
|
+
subjectId,
|
|
182
|
+
claim,
|
|
183
|
+
scopes: payload.scopes,
|
|
184
|
+
authTime: payload.authTime,
|
|
185
|
+
...payload.appNonce !== undefined ? { appNonce: payload.appNonce } : {},
|
|
186
|
+
now,
|
|
187
|
+
methodKind: payload.methodKind,
|
|
188
|
+
accessToken,
|
|
189
|
+
...payload.claimsRequest !== undefined ? { claimsRequest: payload.claimsRequest } : {},
|
|
190
|
+
...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
idToken = await signIdToken(idClaims, signingKey.privateKeyRef, signingKey.alg, signingKey.kid);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
return err(authError.serverError("id_token sign failed", e));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
161
198
|
await safeAudit(deps, {
|
|
162
199
|
kind: "token_issued",
|
|
163
200
|
tenantId: payload.tenantId,
|
|
@@ -166,23 +203,27 @@ async function mintTokens(args) {
|
|
|
166
203
|
methodKind: payload.methodKind,
|
|
167
204
|
subjectId,
|
|
168
205
|
refreshTokenIdHash: refresh ? await hashTokenForAudit(refresh) : "",
|
|
206
|
+
...idToken !== undefined ? { idTokenIssued: true } : {},
|
|
207
|
+
...payload.dpopJkt !== undefined ? { dpopBound: true } : {},
|
|
169
208
|
timestamp: now
|
|
170
209
|
});
|
|
171
210
|
return ok({
|
|
172
211
|
access_token: accessToken,
|
|
173
|
-
token_type: "Bearer",
|
|
212
|
+
token_type: payload.dpopJkt !== undefined ? "DPoP" : "Bearer",
|
|
174
213
|
expires_in: Math.floor(accessTtl / 1000),
|
|
175
214
|
...refresh !== undefined ? { refresh_token: refresh } : {},
|
|
215
|
+
...idToken !== undefined ? { id_token: idToken } : {},
|
|
176
216
|
scope: payload.scopes.join(" ")
|
|
177
217
|
});
|
|
178
218
|
}
|
|
179
219
|
async function hashClientSecret(plain) {
|
|
180
220
|
return base64url.encode(await sha256(utf8.encode(plain)));
|
|
181
221
|
}
|
|
182
|
-
async function deriveSubjectId(claim) {
|
|
222
|
+
async function deriveSubjectId(claim, sectorIdentifier) {
|
|
183
223
|
const c = claim;
|
|
184
224
|
const ordered = canonicalize(c.properties);
|
|
185
|
-
|
|
225
|
+
const seed = sectorIdentifier !== undefined ? `${sectorIdentifier}\x00${c.type}\x00${ordered}` : `${c.type}\x00${ordered}`;
|
|
226
|
+
return base64url.encode(await sha256(seed)).slice(0, 22);
|
|
186
227
|
}
|
|
187
228
|
function canonicalize(value) {
|
|
188
229
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -1,25 +1,60 @@
|
|
|
1
1
|
// src/domain/userinfo.ts
|
|
2
2
|
import { authError } from "../types/error";
|
|
3
3
|
import { err, isErr, ok } from "../types/result";
|
|
4
|
+
import { computeAth, verifyDpopProof } from "./dpop";
|
|
5
|
+
import { pickScopedClaims } from "./id-token";
|
|
4
6
|
import { verifyAccessToken } from "./jwt";
|
|
5
|
-
async function userinfo(
|
|
7
|
+
async function userinfo(input, deps) {
|
|
6
8
|
const keysRes = await deps.keyStore.signingKeys();
|
|
7
9
|
if (isErr(keysRes))
|
|
8
10
|
return err(keysRes.error);
|
|
9
11
|
let claims;
|
|
10
12
|
try {
|
|
11
|
-
claims = await verifyAccessToken(
|
|
13
|
+
claims = await verifyAccessToken(input.accessToken, keysRes.value, {
|
|
12
14
|
...deps.issuerUrl ? { issuer: deps.issuerUrl } : {}
|
|
13
15
|
});
|
|
14
16
|
} catch {
|
|
15
17
|
return err(authError.invalidGrant("access token invalid or expired"));
|
|
16
18
|
}
|
|
19
|
+
const boundJkt = claims.cnf?.jkt;
|
|
20
|
+
if (boundJkt !== undefined) {
|
|
21
|
+
if (input.scheme !== "DPoP") {
|
|
22
|
+
return err(authError.invalidDpopProof('access token is DPoP-bound; Authorization scheme must be "DPoP"'));
|
|
23
|
+
}
|
|
24
|
+
if (!input.dpopProof) {
|
|
25
|
+
return err(authError.invalidDpopProof("access token is DPoP-bound; request is missing a DPoP proof"));
|
|
26
|
+
}
|
|
27
|
+
if (!input.htu || !input.htm || input.nowSec === undefined) {
|
|
28
|
+
return err(authError.invalidDpopProof("userinfo dpop verification requires htu, htm, and nowSec"));
|
|
29
|
+
}
|
|
30
|
+
if (!deps.tokenStore) {
|
|
31
|
+
return err(authError.invalidDpopProof("userinfo dpop verification requires a token-store (jti replay protection)"));
|
|
32
|
+
}
|
|
33
|
+
const expectedAth = await computeAth(input.accessToken);
|
|
34
|
+
const dpopRes = await verifyDpopProof({
|
|
35
|
+
proofJwt: input.dpopProof,
|
|
36
|
+
htu: input.htu,
|
|
37
|
+
htm: input.htm,
|
|
38
|
+
nowSec: input.nowSec,
|
|
39
|
+
expectedAth
|
|
40
|
+
}, { tokenStore: deps.tokenStore });
|
|
41
|
+
if (isErr(dpopRes))
|
|
42
|
+
return err(dpopRes.error);
|
|
43
|
+
if (dpopRes.value.jkt !== boundJkt) {
|
|
44
|
+
return err(authError.invalidDpopProof("dpop proof key does not match access token cnf.jkt"));
|
|
45
|
+
}
|
|
46
|
+
} else if (input.scheme === "DPoP") {
|
|
47
|
+
return err(authError.invalidGrant("access token is not DPoP-bound; use Authorization: Bearer"));
|
|
48
|
+
}
|
|
17
49
|
const claim = claims.claim;
|
|
50
|
+
const scopes = claims.scope ? claims.scope.split(" ").filter(Boolean) : [];
|
|
51
|
+
const scopedClaims = pickScopedClaims(claim, scopes, claims.uic ?? [], deps.customScopeClaims ?? {});
|
|
18
52
|
return ok({
|
|
19
53
|
sub: claims.sub,
|
|
20
54
|
subject_type: claim.type,
|
|
21
55
|
properties: claim.properties,
|
|
22
|
-
scope: claims.scope
|
|
56
|
+
scope: claims.scope,
|
|
57
|
+
...scopedClaims
|
|
23
58
|
});
|
|
24
59
|
}
|
|
25
60
|
export {
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
authorizeRedirectErrorResponse,
|
|
11
11
|
isNonRecoverable
|
|
12
12
|
} from "../errors";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
authorizeQuerySchema,
|
|
15
|
+
authorizeRequestUriQuerySchema
|
|
16
|
+
} from "../schemas/authorize";
|
|
14
17
|
function makeAuthorizeHandler(deps) {
|
|
15
18
|
return async (c) => {
|
|
16
19
|
const tenant = c.get("tenant");
|
|
@@ -19,13 +22,37 @@ function makeAuthorizeHandler(deps) {
|
|
|
19
22
|
}
|
|
20
23
|
const url = new URL(c.req.url);
|
|
21
24
|
const raw = Object.fromEntries(url.searchParams.entries());
|
|
22
|
-
|
|
25
|
+
let workingRaw = raw;
|
|
26
|
+
if (raw.request_uri !== undefined) {
|
|
27
|
+
const parParsed = authorizeRequestUriQuerySchema.safeParse(raw);
|
|
28
|
+
if (!parParsed.success) {
|
|
29
|
+
return authorizeDirectErrorResponse(authError.invalidRequest(parParsed.error.issues[0]?.message ?? "invalid request", parParsed.error.issues[0]?.path[0]?.toString() ?? "request"));
|
|
30
|
+
}
|
|
31
|
+
if (!deps.sessionStore.consumePar) {
|
|
32
|
+
return authorizeDirectErrorResponse(authError.invalidRequest("session adapter does not support PAR", "request_uri"));
|
|
33
|
+
}
|
|
34
|
+
const par = await deps.sessionStore.consumePar(parParsed.data.request_uri);
|
|
35
|
+
if (isErr(par)) {
|
|
36
|
+
return authorizeDirectErrorResponse(authError.invalidRequest("unknown or expired request_uri", "request_uri"));
|
|
37
|
+
}
|
|
38
|
+
if (par.value.clientId !== parParsed.data.client_id) {
|
|
39
|
+
return authorizeDirectErrorResponse(authError.invalidRequest("request_uri belongs to a different client", "request_uri"));
|
|
40
|
+
}
|
|
41
|
+
workingRaw = par.value.params;
|
|
42
|
+
}
|
|
43
|
+
const parsed = authorizeQuerySchema.safeParse(workingRaw);
|
|
23
44
|
if (!parsed.success) {
|
|
24
45
|
const first = parsed.error.issues[0];
|
|
25
46
|
const field = first?.path[0]?.toString() ?? "request";
|
|
26
47
|
return authorizeDirectErrorResponse(authError.invalidRequest(first?.message ?? "invalid request", field));
|
|
27
48
|
}
|
|
28
49
|
const q = parsed.data;
|
|
50
|
+
if (raw.request_uri === undefined) {
|
|
51
|
+
const client = tenant.config.clients.find((c2) => c2.id === q.client_id);
|
|
52
|
+
if (client?.requirePushedAuthorizationRequests) {
|
|
53
|
+
return authorizeDirectErrorResponse(authError.invalidRequest("this client requires Pushed Authorization Requests (RFC 9126)", "request_uri"));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
29
56
|
if (q.response_type !== "code") {
|
|
30
57
|
return authorizeDirectErrorResponse({
|
|
31
58
|
code: "invalid_request",
|
|
@@ -33,6 +60,14 @@ function makeAuthorizeHandler(deps) {
|
|
|
33
60
|
field: "response_type"
|
|
34
61
|
});
|
|
35
62
|
}
|
|
63
|
+
let claimsRequest;
|
|
64
|
+
if (q.claims !== undefined) {
|
|
65
|
+
const parsedClaims = parseClaimsParameter(q.claims);
|
|
66
|
+
if (!parsedClaims.ok) {
|
|
67
|
+
return authorizeDirectErrorResponse(authError.invalidRequest(parsedClaims.error, "claims"));
|
|
68
|
+
}
|
|
69
|
+
claimsRequest = parsedClaims.value;
|
|
70
|
+
}
|
|
36
71
|
const request = {
|
|
37
72
|
tenantId: asTenantId(tenant.id),
|
|
38
73
|
clientId: q.client_id,
|
|
@@ -46,7 +81,8 @@ function makeAuthorizeHandler(deps) {
|
|
|
46
81
|
...q.code_challenge_method !== undefined ? { codeChallengeMethod: q.code_challenge_method } : {},
|
|
47
82
|
...q.prompt !== undefined ? { prompt: q.prompt } : {},
|
|
48
83
|
...q.ui_locales !== undefined ? { uiLocales: q.ui_locales } : {},
|
|
49
|
-
...q.nonce !== undefined ? { nonce: q.nonce } : {}
|
|
84
|
+
...q.nonce !== undefined ? { nonce: q.nonce } : {},
|
|
85
|
+
...claimsRequest !== undefined ? { claimsRequest } : {}
|
|
50
86
|
};
|
|
51
87
|
const result = await startAuthorize({
|
|
52
88
|
request,
|
|
@@ -132,6 +168,57 @@ function makeAuthorizeHandler(deps) {
|
|
|
132
168
|
}
|
|
133
169
|
};
|
|
134
170
|
}
|
|
171
|
+
function parseClaimsParameter(raw) {
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(raw);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
error: `claims parameter is not valid JSON: ${e instanceof Error ? e.message : String(e)}`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
182
|
+
return { ok: false, error: "claims parameter must be a JSON object" };
|
|
183
|
+
}
|
|
184
|
+
const obj = parsed;
|
|
185
|
+
const result = {};
|
|
186
|
+
for (const section of ["userinfo", "id_token"]) {
|
|
187
|
+
const v = obj[section];
|
|
188
|
+
if (v === undefined)
|
|
189
|
+
continue;
|
|
190
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: `claims.${section} must be a JSON object`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const sectionMap = {};
|
|
197
|
+
for (const [name, entry] of Object.entries(v)) {
|
|
198
|
+
if (entry === null) {
|
|
199
|
+
sectionMap[name] = null;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (typeof entry !== "object" || Array.isArray(entry)) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: `claims.${section}.${name} must be null or a JSON object`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const e = entry;
|
|
209
|
+
const normalized = {};
|
|
210
|
+
if (typeof e.essential === "boolean")
|
|
211
|
+
normalized.essential = e.essential;
|
|
212
|
+
if ("value" in e)
|
|
213
|
+
normalized.value = e.value;
|
|
214
|
+
if (Array.isArray(e.values))
|
|
215
|
+
normalized.values = e.values;
|
|
216
|
+
sectionMap[name] = normalized;
|
|
217
|
+
}
|
|
218
|
+
result[section] = sectionMap;
|
|
219
|
+
}
|
|
220
|
+
return { ok: true, value: result };
|
|
221
|
+
}
|
|
135
222
|
function clearFlowCookie() {
|
|
136
223
|
return {
|
|
137
224
|
name: "idp.flow",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/http/handlers/end-session.ts
|
|
2
|
+
import { endSession } from "../../domain/logout";
|
|
3
|
+
import { authError } from "../../types/error";
|
|
4
|
+
import { isErr } from "../../types/result";
|
|
5
|
+
import { applyResponsePolicy } from "../cookies";
|
|
6
|
+
import { tokenEndpointErrorResponse } from "../errors";
|
|
7
|
+
import { endSessionParamsSchema } from "../schemas/end-session";
|
|
8
|
+
function makeEndSessionHandler(deps) {
|
|
9
|
+
return async (c) => {
|
|
10
|
+
const tenant = c.get("tenant");
|
|
11
|
+
if (!tenant) {
|
|
12
|
+
return tokenEndpointErrorResponse(authError.invalidRequest("tenant unresolved"));
|
|
13
|
+
}
|
|
14
|
+
let raw;
|
|
15
|
+
const httpMethod = c.req.method.toUpperCase();
|
|
16
|
+
if (httpMethod === "GET") {
|
|
17
|
+
raw = Object.fromEntries(new URL(c.req.url).searchParams.entries());
|
|
18
|
+
} else if (httpMethod === "POST") {
|
|
19
|
+
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
20
|
+
if (!ct.startsWith("application/x-www-form-urlencoded")) {
|
|
21
|
+
return tokenEndpointErrorResponse(authError.invalidRequest("POST /end_session requires application/x-www-form-urlencoded body"));
|
|
22
|
+
}
|
|
23
|
+
raw = Object.fromEntries(new URLSearchParams(await c.req.raw.text()));
|
|
24
|
+
} else {
|
|
25
|
+
return tokenEndpointErrorResponse(authError.invalidRequest(`unsupported method "${httpMethod}"`));
|
|
26
|
+
}
|
|
27
|
+
const parsed = endSessionParamsSchema.safeParse(raw);
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
return tokenEndpointErrorResponse(authError.invalidRequest(parsed.error.issues[0]?.message ?? "invalid request"));
|
|
30
|
+
}
|
|
31
|
+
const q = parsed.data;
|
|
32
|
+
const res = await endSession({
|
|
33
|
+
...q.id_token_hint !== undefined ? { idTokenHint: q.id_token_hint } : {},
|
|
34
|
+
...q.client_id !== undefined ? { clientId: q.client_id } : {},
|
|
35
|
+
...q.post_logout_redirect_uri !== undefined ? { postLogoutRedirectUri: q.post_logout_redirect_uri } : {},
|
|
36
|
+
...q.state !== undefined ? { state: q.state } : {}
|
|
37
|
+
}, tenant, {
|
|
38
|
+
configStore: deps.configStore,
|
|
39
|
+
tokenStore: deps.tokenStore,
|
|
40
|
+
keyStore: deps.keyStore,
|
|
41
|
+
...deps.auditLog ? { auditLog: deps.auditLog } : {},
|
|
42
|
+
issuerUrl: c.get("issuerUrl"),
|
|
43
|
+
clock: deps.clock
|
|
44
|
+
});
|
|
45
|
+
if (isErr(res))
|
|
46
|
+
return tokenEndpointErrorResponse(res.error);
|
|
47
|
+
if (res.value.kind === "redirect") {
|
|
48
|
+
return applyResponsePolicy(new Response(null, {
|
|
49
|
+
status: 302,
|
|
50
|
+
headers: { location: res.value.url }
|
|
51
|
+
}), { setCookies: [], cookieDefaults: deps.cookieDefaults });
|
|
52
|
+
}
|
|
53
|
+
return new Response("Logged out.", {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: {
|
|
56
|
+
"content-type": "text/plain; charset=utf-8",
|
|
57
|
+
"cache-control": "no-store"
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
makeEndSessionHandler
|
|
64
|
+
};
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import { buildDiscoveryDocument, buildJwks } from "../../domain/discovery";
|
|
3
3
|
import { isErr } from "../../types/result";
|
|
4
4
|
import { tokenEndpointErrorResponse } from "../errors";
|
|
5
|
-
function makeDiscoveryHandler(
|
|
5
|
+
function makeDiscoveryHandler(deps) {
|
|
6
6
|
return async (c) => {
|
|
7
|
-
const doc = buildDiscoveryDocument({
|
|
7
|
+
const doc = buildDiscoveryDocument({
|
|
8
|
+
issuerUrl: c.get("issuerUrl"),
|
|
9
|
+
...deps.customScopeClaims !== undefined ? { customScopeClaims: deps.customScopeClaims } : {}
|
|
10
|
+
});
|
|
8
11
|
return new Response(JSON.stringify(doc), {
|
|
9
12
|
status: 200,
|
|
10
13
|
headers: {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/http/handlers/par.ts
|
|
2
|
+
import { resolveClientCredentials } from "../../domain/client-auth";
|
|
3
|
+
import { pushAuthorizationRequest } from "../../domain/par";
|
|
4
|
+
import { authError } from "../../types/error";
|
|
5
|
+
import { isErr } from "../../types/result";
|
|
6
|
+
import { tokenEndpointErrorResponse } from "../errors";
|
|
7
|
+
import { parBodySchema } from "../schemas/par";
|
|
8
|
+
function makeParHandler(deps) {
|
|
9
|
+
return async (c) => {
|
|
10
|
+
const tenant = c.get("tenant");
|
|
11
|
+
if (!tenant) {
|
|
12
|
+
return tokenEndpointErrorResponse(authError.invalidRequest("tenant unresolved"));
|
|
13
|
+
}
|
|
14
|
+
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
15
|
+
if (!ct.startsWith("application/x-www-form-urlencoded")) {
|
|
16
|
+
return tokenEndpointErrorResponse(authError.invalidRequest("body must be application/x-www-form-urlencoded"));
|
|
17
|
+
}
|
|
18
|
+
const raw = Object.fromEntries(new URLSearchParams(await c.req.raw.text()).entries());
|
|
19
|
+
const parsed = parBodySchema.safeParse(raw);
|
|
20
|
+
if (!parsed.success) {
|
|
21
|
+
return tokenEndpointErrorResponse(authError.invalidRequest(parsed.error.issues[0]?.message ?? "invalid request"));
|
|
22
|
+
}
|
|
23
|
+
const creds = resolveClientCredentials({
|
|
24
|
+
authorizationHeader: c.req.header("authorization") ?? null,
|
|
25
|
+
bodyClientId: parsed.data.client_id,
|
|
26
|
+
bodyClientSecret: parsed.data.client_secret
|
|
27
|
+
});
|
|
28
|
+
const clientId = creds?.clientId ?? parsed.data.client_id;
|
|
29
|
+
const { client_secret: _strip, ...paramsForAuthorize } = parsed.data;
|
|
30
|
+
const res = await pushAuthorizationRequest({
|
|
31
|
+
clientId,
|
|
32
|
+
...creds?.clientSecret !== undefined ? { clientSecret: creds.clientSecret } : {},
|
|
33
|
+
params: paramsForAuthorize
|
|
34
|
+
}, tenant, {
|
|
35
|
+
configStore: deps.configStore,
|
|
36
|
+
sessionStore: deps.sessionStore,
|
|
37
|
+
...deps.auditLog ? { auditLog: deps.auditLog } : {},
|
|
38
|
+
clock: deps.clock
|
|
39
|
+
});
|
|
40
|
+
if (isErr(res))
|
|
41
|
+
return tokenEndpointErrorResponse(res.error);
|
|
42
|
+
return new Response(JSON.stringify(res.value), {
|
|
43
|
+
status: 201,
|
|
44
|
+
headers: {
|
|
45
|
+
"content-type": "application/json",
|
|
46
|
+
"cache-control": "no-store"
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
makeParHandler
|
|
53
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/http/handlers/register.ts
|
|
2
|
+
import { registerNewClient } from "../../domain/register";
|
|
3
|
+
import { authError } from "../../types/error";
|
|
4
|
+
import { isErr } from "../../types/result";
|
|
5
|
+
import { tokenEndpointErrorResponse } from "../errors";
|
|
6
|
+
import { registerBodySchema } from "../schemas/register";
|
|
7
|
+
function makeRegisterHandler(deps) {
|
|
8
|
+
return async (c) => {
|
|
9
|
+
const tenant = c.get("tenant");
|
|
10
|
+
if (!tenant) {
|
|
11
|
+
return tokenEndpointErrorResponse(authError.invalidRequest("tenant unresolved"));
|
|
12
|
+
}
|
|
13
|
+
const ct = (c.req.header("content-type") ?? "").toLowerCase();
|
|
14
|
+
if (!ct.startsWith("application/json")) {
|
|
15
|
+
return tokenEndpointErrorResponse(authError.invalidRequest("body must be application/json"));
|
|
16
|
+
}
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = await c.req.raw.json();
|
|
20
|
+
} catch (e) {
|
|
21
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
22
|
+
return tokenEndpointErrorResponse(authError.invalidRequest(`malformed JSON: ${reason}`));
|
|
23
|
+
}
|
|
24
|
+
const parsed = registerBodySchema.safeParse(raw);
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
return tokenEndpointErrorResponse(authError.invalidRequest(parsed.error.issues[0]?.message ?? "invalid request"));
|
|
27
|
+
}
|
|
28
|
+
const request = parsed.data;
|
|
29
|
+
const res = await registerNewClient(request, tenant, {
|
|
30
|
+
...deps.registerClient ? { registerClient: deps.registerClient } : {},
|
|
31
|
+
clock: deps.clock
|
|
32
|
+
});
|
|
33
|
+
if (isErr(res))
|
|
34
|
+
return tokenEndpointErrorResponse(res.error);
|
|
35
|
+
return new Response(JSON.stringify(res.value), {
|
|
36
|
+
status: 201,
|
|
37
|
+
headers: {
|
|
38
|
+
"content-type": "application/json",
|
|
39
|
+
"cache-control": "no-store"
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
makeRegisterHandler
|
|
46
|
+
};
|