@gaballahtech/seba 0.0.0-stage → 0.2.1
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/ARCHITECTURE.md +242 -0
- package/BUILD_REPORT.md +195 -0
- package/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +282 -2
- package/RESEARCH.md +180 -0
- package/SECURITY.md +224 -0
- package/VALIDATION_REPORT.md +187 -0
- package/dist/browser/index.d.ts +9 -0
- package/dist/browser/index.d.ts.map +1 -0
- package/dist/browser/index.js +9 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/platform.d.ts +14 -0
- package/dist/browser/platform.d.ts.map +1 -0
- package/dist/browser/platform.js +60 -0
- package/dist/browser/platform.js.map +1 -0
- package/dist/browser/provider-modal.d.ts +15 -0
- package/dist/browser/provider-modal.d.ts.map +1 -0
- package/dist/browser/provider-modal.js +126 -0
- package/dist/browser/provider-modal.js.map +1 -0
- package/dist/browser/storage.d.ts +32 -0
- package/dist/browser/storage.d.ts.map +1 -0
- package/dist/browser/storage.js +121 -0
- package/dist/browser/storage.js.map +1 -0
- package/dist/core/client.d.ts +49 -0
- package/dist/core/client.d.ts.map +1 -0
- package/dist/core/client.js +371 -0
- package/dist/core/client.js.map +1 -0
- package/dist/core/discovery.d.ts +82 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +243 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/dpop.d.ts +67 -0
- package/dist/core/dpop.d.ts.map +1 -0
- package/dist/core/dpop.js +176 -0
- package/dist/core/dpop.js.map +1 -0
- package/dist/core/errors.d.ts +86 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +104 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/http.d.ts +68 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +128 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/identity.d.ts +145 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +735 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/oidc.d.ts +175 -0
- package/dist/core/oidc.d.ts.map +1 -0
- package/dist/core/oidc.js +413 -0
- package/dist/core/oidc.js.map +1 -0
- package/dist/core/session.d.ts +116 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +350 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/storage.d.ts +22 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +48 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.d.ts +9 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +9 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/platform.d.ts +25 -0
- package/dist/node/platform.d.ts.map +1 -0
- package/dist/node/platform.js +161 -0
- package/dist/node/platform.js.map +1 -0
- package/dist/seba.d.ts +39 -0
- package/dist/seba.d.ts.map +1 -0
- package/dist/seba.js +58 -0
- package/dist/seba.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/examples/node/index.ts +61 -0
- package/examples/react/App.tsx +127 -0
- package/examples/react/index.html +12 -0
- package/examples/react/main.tsx +14 -0
- package/examples/react/vite.config.ts +27 -0
- package/package.json +83 -4
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenID Provider discovery (OpenID Connect Discovery 1.0 §3–§4).
|
|
3
|
+
*
|
|
4
|
+
* SEBA only trusts a discovery document whose `issuer` is byte-identical to the issuer it
|
|
5
|
+
* asked for, and only uses `https` endpoints (loopback `http` for local development).
|
|
6
|
+
*/
|
|
7
|
+
import { SebaDPoPError, SebaDiscoveryError } from "./errors.js";
|
|
8
|
+
import { httpFetch, isOk, JSON_ACCEPT, parseJson, readText } from "./http.js";
|
|
9
|
+
/** Hostnames that are only reachable from the local machine. */
|
|
10
|
+
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "::1", "localhost"]);
|
|
11
|
+
/**
|
|
12
|
+
* Whether a hostname is a loopback interface.
|
|
13
|
+
*
|
|
14
|
+
* @param hostname - Hostname from a parsed URL.
|
|
15
|
+
*/
|
|
16
|
+
export function isLoopbackHost(hostname) {
|
|
17
|
+
const host = hostname.toLowerCase();
|
|
18
|
+
return LOOPBACK_HOSTS.has(host) || host.endsWith(".localhost");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Validate an issuer identifier.
|
|
22
|
+
*
|
|
23
|
+
* Rules applied (Solid-OIDC §6.1, OIDC Discovery §3): absolute URL, `https` scheme (plain
|
|
24
|
+
* `http` is accepted for loopback development hosts only), a host, and no query or
|
|
25
|
+
* fragment. The issuer string is returned unchanged because issuer identity is exact.
|
|
26
|
+
*
|
|
27
|
+
* @param issuer - Candidate issuer URL.
|
|
28
|
+
* @param label - Field name used in error messages.
|
|
29
|
+
* @throws {@link SebaDiscoveryError} when the value is not a usable issuer.
|
|
30
|
+
*/
|
|
31
|
+
export function validateIssuerUrl(issuer, label = "issuer") {
|
|
32
|
+
let url;
|
|
33
|
+
try {
|
|
34
|
+
url = new URL(issuer);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new SebaDiscoveryError(`The ${label} "${issuer}" is not an absolute URL. ` +
|
|
38
|
+
'Use the provider\'s issuer identifier, for example "https://login.example.com".');
|
|
39
|
+
}
|
|
40
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
|
|
41
|
+
throw new SebaDiscoveryError(`The ${label} "${issuer}" must use https. ` +
|
|
42
|
+
"Plain http is only accepted for loopback development hosts (127.0.0.1, ::1, localhost).");
|
|
43
|
+
}
|
|
44
|
+
if (url.hostname === "") {
|
|
45
|
+
throw new SebaDiscoveryError(`The ${label} "${issuer}" has no host component.`);
|
|
46
|
+
}
|
|
47
|
+
if (url.search !== "" || url.hash !== "") {
|
|
48
|
+
throw new SebaDiscoveryError(`The ${label} "${issuer}" must not contain a query string or fragment. ` +
|
|
49
|
+
"An issuer identifier is a stable URL such as \"https://login.example.com\".");
|
|
50
|
+
}
|
|
51
|
+
if (url.username !== "" || url.password !== "") {
|
|
52
|
+
throw new SebaDiscoveryError(`The ${label} "${issuer}" must not contain embedded credentials.`);
|
|
53
|
+
}
|
|
54
|
+
return issuer;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Candidate well-known configuration URLs for an issuer.
|
|
58
|
+
*
|
|
59
|
+
* RFC 8414 inserts the well-known segment between host and path; several providers only
|
|
60
|
+
* serve the appended form. Both are tried, in that order, and only on 404.
|
|
61
|
+
*
|
|
62
|
+
* @param issuer - Validated issuer URL.
|
|
63
|
+
*/
|
|
64
|
+
export function wellKnownCandidates(issuer) {
|
|
65
|
+
const url = new URL(issuer);
|
|
66
|
+
const origin = `${url.protocol}//${url.host}`;
|
|
67
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
68
|
+
if (path === "") {
|
|
69
|
+
return [`${origin}/.well-known/openid-configuration`];
|
|
70
|
+
}
|
|
71
|
+
return [
|
|
72
|
+
`${origin}/.well-known/openid-configuration${path}`,
|
|
73
|
+
`${origin}${path}/.well-known/openid-configuration`,
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Compare a provider-declared issuer with the requested one.
|
|
78
|
+
*
|
|
79
|
+
* Exact equality is required, with one narrowly scoped exception: a provider whose issuer
|
|
80
|
+
* has an empty path may publish it as `https://host/` while the caller typed
|
|
81
|
+
* `https://host` (no path). Both denote the same identifier and real providers
|
|
82
|
+
* (solidcommunity.net, solidweb.me, Community Solid Server) publish the trailing-slash
|
|
83
|
+
* form. Nothing else is normalized: paths, ports, schemes and query strings must match.
|
|
84
|
+
*
|
|
85
|
+
* @param declared - Issuer string from the discovery document.
|
|
86
|
+
* @param requested - Issuer string the caller asked for.
|
|
87
|
+
*/
|
|
88
|
+
export function issuerEquivalent(declared, requested) {
|
|
89
|
+
if (declared === requested) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const left = new URL(declared);
|
|
94
|
+
const right = new URL(requested);
|
|
95
|
+
const emptyPath = (url) => url.pathname === "" || url.pathname === "/";
|
|
96
|
+
return (left.origin === right.origin &&
|
|
97
|
+
emptyPath(left) &&
|
|
98
|
+
emptyPath(right) &&
|
|
99
|
+
left.search === "" &&
|
|
100
|
+
right.search === "" &&
|
|
101
|
+
left.hash === "" &&
|
|
102
|
+
right.hash === "");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Validate an endpoint URL from provider metadata.
|
|
110
|
+
*
|
|
111
|
+
* @param value - Candidate value from the discovery document.
|
|
112
|
+
* @param field - Metadata member name, used in error messages.
|
|
113
|
+
* @param issuer - Issuer the document was fetched for, used in error messages.
|
|
114
|
+
*/
|
|
115
|
+
export function validateEndpointUrl(value, field, issuer) {
|
|
116
|
+
if (typeof value !== "string" || value === "") {
|
|
117
|
+
throw new SebaDiscoveryError(`The discovery document for "${issuer}" is missing the required "${field}" URL.`);
|
|
118
|
+
}
|
|
119
|
+
let url;
|
|
120
|
+
try {
|
|
121
|
+
url = new URL(value);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new SebaDiscoveryError(`The discovery document for "${issuer}" declares "${field}" as "${value}", which is not an absolute URL.`);
|
|
125
|
+
}
|
|
126
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) {
|
|
127
|
+
throw new SebaDiscoveryError(`The discovery document for "${issuer}" declares "${field}" as "${value}", which does not use https. ` +
|
|
128
|
+
"Refusing to send authorization codes or tokens over an insecure endpoint.");
|
|
129
|
+
}
|
|
130
|
+
if (url.hash !== "") {
|
|
131
|
+
throw new SebaDiscoveryError(`The discovery document for "${issuer}" declares "${field}" with a fragment.`);
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
function readStringArray(value, field, issuer) {
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(value) || !value.every((entry) => typeof entry === "string")) {
|
|
140
|
+
throw new SebaDiscoveryError(`The discovery document for "${issuer}" declares "${field}" as a non-string array.`);
|
|
141
|
+
}
|
|
142
|
+
return [...value];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Parse and validate an OpenID Provider configuration document.
|
|
146
|
+
*
|
|
147
|
+
* @param raw - Parsed JSON document.
|
|
148
|
+
* @param requestedIssuer - Issuer that was requested; must match the document exactly.
|
|
149
|
+
*/
|
|
150
|
+
export function parseProviderConfiguration(raw, requestedIssuer) {
|
|
151
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
152
|
+
throw new SebaDiscoveryError(`The discovery document for "${requestedIssuer}" is not a JSON object.`);
|
|
153
|
+
}
|
|
154
|
+
const document = raw;
|
|
155
|
+
const declaredIssuer = document["issuer"];
|
|
156
|
+
if (typeof declaredIssuer !== "string" || !issuerEquivalent(declaredIssuer, requestedIssuer)) {
|
|
157
|
+
const found = typeof declaredIssuer === "string" ? `"${declaredIssuer}"` : "missing";
|
|
158
|
+
throw new SebaDiscoveryError(`The discovery document for "${requestedIssuer}" declares its issuer as ${found}. ` +
|
|
159
|
+
"A mismatching issuer identifier means the document cannot be trusted; check that the configured issuer is correct.");
|
|
160
|
+
}
|
|
161
|
+
const responseTypes = readStringArray(document["response_types_supported"], "response_types_supported", requestedIssuer);
|
|
162
|
+
if (responseTypes !== undefined && !responseTypes.includes("code")) {
|
|
163
|
+
throw new SebaDiscoveryError(`The provider "${requestedIssuer}" does not advertise support for the "code" response type. ` +
|
|
164
|
+
"SEBA only implements the authorization code flow.");
|
|
165
|
+
}
|
|
166
|
+
const dpopAlgorithms = readStringArray(document["dpop_signing_alg_values_supported"], "dpop_signing_alg_values_supported", requestedIssuer);
|
|
167
|
+
if (dpopAlgorithms !== undefined && !dpopAlgorithms.includes("ES256")) {
|
|
168
|
+
throw new SebaDPoPError(`The provider "${requestedIssuer}" does not advertise ES256 in "dpop_signing_alg_values_supported". ` +
|
|
169
|
+
`Advertised algorithms: ${dpopAlgorithms.join(", ") || "(none)"}. SEBA requires ES256.`);
|
|
170
|
+
}
|
|
171
|
+
const configuration = {
|
|
172
|
+
// The provider's spelling is authoritative: real providers commonly publish
|
|
173
|
+
// "https://host/" while users type "https://host". The callback `iss` and later
|
|
174
|
+
// comparisons use this exact string.
|
|
175
|
+
issuer: declaredIssuer,
|
|
176
|
+
authorizationEndpoint: validateEndpointUrl(document["authorization_endpoint"], "authorization_endpoint", requestedIssuer),
|
|
177
|
+
tokenEndpoint: validateEndpointUrl(document["token_endpoint"], "token_endpoint", requestedIssuer),
|
|
178
|
+
jwksUri: validateEndpointUrl(document["jwks_uri"], "jwks_uri", requestedIssuer),
|
|
179
|
+
};
|
|
180
|
+
if (document["revocation_endpoint"] !== undefined) {
|
|
181
|
+
configuration.revocationEndpoint = validateEndpointUrl(document["revocation_endpoint"], "revocation_endpoint", requestedIssuer);
|
|
182
|
+
}
|
|
183
|
+
const scopes = readStringArray(document["scopes_supported"], "scopes_supported", requestedIssuer);
|
|
184
|
+
if (scopes !== undefined) {
|
|
185
|
+
configuration.scopesSupported = scopes;
|
|
186
|
+
}
|
|
187
|
+
const grants = readStringArray(document["grant_types_supported"], "grant_types_supported", requestedIssuer);
|
|
188
|
+
if (grants !== undefined) {
|
|
189
|
+
configuration.grantTypesSupported = grants;
|
|
190
|
+
}
|
|
191
|
+
if (dpopAlgorithms !== undefined) {
|
|
192
|
+
configuration.dpopSigningAlgValuesSupported = dpopAlgorithms;
|
|
193
|
+
}
|
|
194
|
+
return configuration;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Fetch and validate the OpenID Provider configuration for an issuer.
|
|
198
|
+
*
|
|
199
|
+
* @param issuer - Issuer identifier supplied by configuration, WebID discovery or the user.
|
|
200
|
+
* @param fetchImpl - Runtime `fetch` implementation.
|
|
201
|
+
* @throws {@link SebaDiscoveryError} for an unusable issuer or an invalid/missing document.
|
|
202
|
+
* @throws {@link SebaNetworkError} when the provider cannot be reached.
|
|
203
|
+
*/
|
|
204
|
+
export async function discoverProvider(issuer, fetchImpl) {
|
|
205
|
+
const validated = validateIssuerUrl(issuer);
|
|
206
|
+
const candidates = wellKnownCandidates(validated);
|
|
207
|
+
let response = null;
|
|
208
|
+
let lastUrl = candidates[0] ?? validated;
|
|
209
|
+
for (const candidate of candidates) {
|
|
210
|
+
lastUrl = candidate;
|
|
211
|
+
const attempt = await httpFetch(candidate, { headers: { accept: JSON_ACCEPT }, redirect: "follow" }, { fetchImpl });
|
|
212
|
+
if (attempt.status === 404) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
response = attempt;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (response === null) {
|
|
219
|
+
throw new SebaDiscoveryError(`No OpenID Provider configuration was found for issuer "${validated}". ` +
|
|
220
|
+
`Tried ${candidates.map((candidate) => `"${candidate}"`).join(" and ")} and received HTTP 404. ` +
|
|
221
|
+
"Check the issuer URL and that the provider publishes openid-configuration.");
|
|
222
|
+
}
|
|
223
|
+
if (!isOk(response)) {
|
|
224
|
+
throw new SebaDiscoveryError(`Fetching the OpenID Provider configuration for "${validated}" failed with HTTP ${String(response.status)}. ` +
|
|
225
|
+
`Requested ${lastUrl}.`);
|
|
226
|
+
}
|
|
227
|
+
const text = await readText(response, "OpenID Provider configuration");
|
|
228
|
+
const raw = parseJson(text, `The OpenID Provider configuration for "${validated}"`);
|
|
229
|
+
return parseProviderConfiguration(raw, validated);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether a provider advertises Solid `webid` scope support.
|
|
233
|
+
*
|
|
234
|
+
* Used to decide whether Solid-specific ID token requirements (`aud` containing `solid`,
|
|
235
|
+
* `cnf.jkt` binding) are enforced. A provider that does not publish `scopes_supported` is
|
|
236
|
+
* treated as not advertising `webid`, so standard OIDC validation applies.
|
|
237
|
+
*
|
|
238
|
+
* @param configuration - Validated provider configuration.
|
|
239
|
+
*/
|
|
240
|
+
export function advertisesWebIdScope(configuration) {
|
|
241
|
+
return configuration.scopesSupported?.includes("webid") === true;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/core/discovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DPoP key management and proof creation (RFC 9449).
|
|
3
|
+
*
|
|
4
|
+
* Every attempt gets a brand-new proof: a new `jti`, a new signature and a new `iat`.
|
|
5
|
+
* This module holds no proof state, so no code path can replay a proof.
|
|
6
|
+
*/
|
|
7
|
+
import type { DPoPSigner, PersistedKey, ProofInput } from "../types.js";
|
|
8
|
+
/** DPoP signing algorithm required by Solid-OIDC. */
|
|
9
|
+
export declare const DPOP_ALG = "ES256";
|
|
10
|
+
/** A generated DPoP key and its persistable representation. */
|
|
11
|
+
export interface DPoPKeyMaterial {
|
|
12
|
+
/** Signing capability for this key. */
|
|
13
|
+
readonly signer: DPoPSigner;
|
|
14
|
+
/** Value to store (private key or exported JWK + public JWK). */
|
|
15
|
+
readonly persisted: PersistedKey;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate a new ES256 DPoP key pair.
|
|
19
|
+
*
|
|
20
|
+
* The private key is non-extractable whenever the runtime can structured-clone a
|
|
21
|
+
* `CryptoKey` (the property IndexedDB relies on). When it cannot, an extractable key is
|
|
22
|
+
* generated and its JWK is stored instead; this is reported in SECURITY.md and never
|
|
23
|
+
* happens silently.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createDPoPKey(): Promise<DPoPKeyMaterial>;
|
|
26
|
+
/**
|
|
27
|
+
* Rebuild a signer from a persisted key.
|
|
28
|
+
*
|
|
29
|
+
* @param persisted - Stored key material.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadDPoPKey(persisted: PersistedKey): Promise<DPoPSigner>;
|
|
32
|
+
/**
|
|
33
|
+
* Strip private members from a JWK.
|
|
34
|
+
*
|
|
35
|
+
* @param jwk - JWK that may contain private material.
|
|
36
|
+
*/
|
|
37
|
+
export declare function publicJwkOf(jwk: JsonWebKey): JsonWebKey;
|
|
38
|
+
/**
|
|
39
|
+
* Create a fresh DPoP proof.
|
|
40
|
+
*
|
|
41
|
+
* @param input - HTTP method, absolute URL, optional access token and optional nonce.
|
|
42
|
+
* @param key - Public half of the session key, used for the `jwk` header and thumbprint.
|
|
43
|
+
* @param privateKey - Key used to sign this proof.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createDPoPProof(key: {
|
|
46
|
+
privateKey: CryptoKey;
|
|
47
|
+
publicJwk: JsonWebKey;
|
|
48
|
+
}, input: ProofInput): Promise<string>;
|
|
49
|
+
/**
|
|
50
|
+
* RFC 9449 `htu`: the HTTP URI without query and fragment.
|
|
51
|
+
*
|
|
52
|
+
* @param value - Absolute request URL.
|
|
53
|
+
*/
|
|
54
|
+
export declare function normalizeHtu(value: string): string | null;
|
|
55
|
+
/**
|
|
56
|
+
* RFC 9449 `ath`: base64url(SHA-256(ASCII(access_token))).
|
|
57
|
+
*
|
|
58
|
+
* @param accessToken - Access token being presented.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hashAccessToken(accessToken: string): Promise<string>;
|
|
61
|
+
/**
|
|
62
|
+
* Base64url JWK SHA-256 thumbprint of a public key (`cnf.jkt`).
|
|
63
|
+
*
|
|
64
|
+
* @param publicJwk - Public JWK.
|
|
65
|
+
*/
|
|
66
|
+
export declare function dpopThumbprint(publicJwk: JsonWebKey): Promise<string>;
|
|
67
|
+
//# sourceMappingURL=dpop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dpop.d.ts","sourceRoot":"","sources":["../../src/core/dpop.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExE,qDAAqD;AACrD,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;CAClC;AAmBD;;;;;;;GAOG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,eAAe,CAAC,CAmB9D;AAsBD;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,SAAS,EAAE,YAAY,GAAG,OAAO,CAAC,UAAU,CAAC,CAY9E;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAEvD;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE;IAAE,UAAU,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,UAAU,CAAA;CAAE,EACrD,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASzD;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1E;AAUD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAM3E"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DPoP key management and proof creation (RFC 9449).
|
|
3
|
+
*
|
|
4
|
+
* Every attempt gets a brand-new proof: a new `jti`, a new signature and a new `iat`.
|
|
5
|
+
* This module holds no proof state, so no code path can replay a proof.
|
|
6
|
+
*/
|
|
7
|
+
import { SignJWT, calculateJwkThumbprint, exportJWK, generateKeyPair, importJWK } from "jose";
|
|
8
|
+
import { SebaDPoPError } from "./errors.js";
|
|
9
|
+
/** DPoP signing algorithm required by Solid-OIDC. */
|
|
10
|
+
export const DPOP_ALG = "ES256";
|
|
11
|
+
function signerFrom(signingKey) {
|
|
12
|
+
return {
|
|
13
|
+
async createProof(input) {
|
|
14
|
+
return createDPoPProof(signingKey, input);
|
|
15
|
+
},
|
|
16
|
+
async thumbprint() {
|
|
17
|
+
return dpopThumbprint(signingKey.publicJwk);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate a new ES256 DPoP key pair.
|
|
23
|
+
*
|
|
24
|
+
* The private key is non-extractable whenever the runtime can structured-clone a
|
|
25
|
+
* `CryptoKey` (the property IndexedDB relies on). When it cannot, an extractable key is
|
|
26
|
+
* generated and its JWK is stored instead; this is reported in SECURITY.md and never
|
|
27
|
+
* happens silently.
|
|
28
|
+
*/
|
|
29
|
+
export async function createDPoPKey() {
|
|
30
|
+
try {
|
|
31
|
+
const pair = await generateKeyPair(DPOP_ALG, { extractable: false, crv: "P-256" });
|
|
32
|
+
const publicJwk = await exportJWK(pair.publicKey);
|
|
33
|
+
const cloneable = canStructuredClone(pair.privateKey);
|
|
34
|
+
if (cloneable) {
|
|
35
|
+
const signingKey = { privateKey: pair.privateKey, publicJwk };
|
|
36
|
+
return {
|
|
37
|
+
signer: signerFrom(signingKey),
|
|
38
|
+
persisted: { kind: "crypto-key", key: pair.privateKey, publicJwk },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return await generateExtractableKey();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new SebaDPoPError("Unable to generate a DPoP key. The runtime must support Web Crypto ECDSA P-256.", { cause: error });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function generateExtractableKey() {
|
|
48
|
+
const pair = await generateKeyPair(DPOP_ALG, { extractable: true, crv: "P-256" });
|
|
49
|
+
const publicJwk = await exportJWK(pair.publicKey);
|
|
50
|
+
const privateJwk = await exportJWK(pair.privateKey);
|
|
51
|
+
const signingKey = { privateKey: pair.privateKey, publicJwk };
|
|
52
|
+
return {
|
|
53
|
+
signer: signerFrom(signingKey),
|
|
54
|
+
persisted: { kind: "jwk", jwk: privateJwk },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function canStructuredClone(key) {
|
|
58
|
+
try {
|
|
59
|
+
structuredClone(key);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Rebuild a signer from a persisted key.
|
|
68
|
+
*
|
|
69
|
+
* @param persisted - Stored key material.
|
|
70
|
+
*/
|
|
71
|
+
export async function loadDPoPKey(persisted) {
|
|
72
|
+
try {
|
|
73
|
+
if (persisted.kind === "crypto-key") {
|
|
74
|
+
return signerFrom({ privateKey: persisted.key, publicJwk: persisted.publicJwk });
|
|
75
|
+
}
|
|
76
|
+
const privateKey = (await importJWK(persisted.jwk, DPOP_ALG));
|
|
77
|
+
return signerFrom({ privateKey, publicJwk: publicJwkOf(persisted.jwk) });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
throw new SebaDPoPError("The stored DPoP key could not be loaded; the session must be recreated.", {
|
|
81
|
+
cause: error,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Strip private members from a JWK.
|
|
87
|
+
*
|
|
88
|
+
* @param jwk - JWK that may contain private material.
|
|
89
|
+
*/
|
|
90
|
+
export function publicJwkOf(jwk) {
|
|
91
|
+
return { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a fresh DPoP proof.
|
|
95
|
+
*
|
|
96
|
+
* @param input - HTTP method, absolute URL, optional access token and optional nonce.
|
|
97
|
+
* @param key - Public half of the session key, used for the `jwk` header and thumbprint.
|
|
98
|
+
* @param privateKey - Key used to sign this proof.
|
|
99
|
+
*/
|
|
100
|
+
export async function createDPoPProof(key, input) {
|
|
101
|
+
const htu = normalizeHtu(input.url);
|
|
102
|
+
if (htu === null) {
|
|
103
|
+
throw new SebaDPoPError(`The request URL "${input.url}" cannot be normalized for a DPoP proof.`);
|
|
104
|
+
}
|
|
105
|
+
const payload = {
|
|
106
|
+
jti: randomJti(),
|
|
107
|
+
htm: input.method.toUpperCase(),
|
|
108
|
+
htu,
|
|
109
|
+
iat: Math.floor(Date.now() / 1000),
|
|
110
|
+
};
|
|
111
|
+
if (input.accessToken !== undefined) {
|
|
112
|
+
payload["ath"] = await hashAccessToken(input.accessToken);
|
|
113
|
+
}
|
|
114
|
+
if (input.nonce !== undefined) {
|
|
115
|
+
payload["nonce"] = input.nonce;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return await new SignJWT(payload)
|
|
119
|
+
.setProtectedHeader({ typ: "dpop+jwt", alg: DPOP_ALG, jwk: key.publicJwk })
|
|
120
|
+
.sign(key.privateKey);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
throw new SebaDPoPError("Unable to sign the DPoP proof with the session key.", { cause: error });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* RFC 9449 `htu`: the HTTP URI without query and fragment.
|
|
128
|
+
*
|
|
129
|
+
* @param value - Absolute request URL.
|
|
130
|
+
*/
|
|
131
|
+
export function normalizeHtu(value) {
|
|
132
|
+
try {
|
|
133
|
+
const url = new URL(value);
|
|
134
|
+
url.search = "";
|
|
135
|
+
url.hash = "";
|
|
136
|
+
return url.href;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* RFC 9449 `ath`: base64url(SHA-256(ASCII(access_token))).
|
|
144
|
+
*
|
|
145
|
+
* @param accessToken - Access token being presented.
|
|
146
|
+
*/
|
|
147
|
+
export async function hashAccessToken(accessToken) {
|
|
148
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(accessToken));
|
|
149
|
+
return base64urlOf(new Uint8Array(digest));
|
|
150
|
+
}
|
|
151
|
+
function base64urlOf(bytes) {
|
|
152
|
+
let binary = "";
|
|
153
|
+
for (const byte of bytes) {
|
|
154
|
+
binary += String.fromCharCode(byte);
|
|
155
|
+
}
|
|
156
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Base64url JWK SHA-256 thumbprint of a public key (`cnf.jkt`).
|
|
160
|
+
*
|
|
161
|
+
* @param publicJwk - Public JWK.
|
|
162
|
+
*/
|
|
163
|
+
export async function dpopThumbprint(publicJwk) {
|
|
164
|
+
try {
|
|
165
|
+
return await calculateJwkThumbprint(publicJwk, "sha256");
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw new SebaDPoPError("Unable to compute the DPoP key thumbprint.", { cause: error });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function randomJti() {
|
|
172
|
+
const bytes = new Uint8Array(32);
|
|
173
|
+
crypto.getRandomValues(bytes);
|
|
174
|
+
return base64urlOf(bytes);
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=dpop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dpop.js","sourceRoot":"","sources":["../../src/core/dpop.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable error codes exposed by SEBA.
|
|
3
|
+
*
|
|
4
|
+
* Every error thrown by the library is a {@link SebaError} carrying one of these
|
|
5
|
+
* codes, so applications can branch on `error.code` instead of matching strings.
|
|
6
|
+
*/
|
|
7
|
+
export type SebaErrorCode = "SEBA_ERR_CONFIG" | "SEBA_ERR_DISCOVERY" | "SEBA_ERR_REDIRECT_MISMATCH" | "SEBA_ERR_AUTH" | "SEBA_ERR_SESSION_EXPIRED" | "SEBA_ERR_DPOP" | "SEBA_ERR_NETWORK" | "SEBA_ERR_NO_UI";
|
|
8
|
+
/** Options accepted by every SEBA error. */
|
|
9
|
+
export interface SebaErrorOptions {
|
|
10
|
+
/** Underlying error, kept for diagnostics. Never used to carry secrets. */
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Base class of every error thrown by SEBA.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* try {
|
|
19
|
+
* await seba.login();
|
|
20
|
+
* } catch (error) {
|
|
21
|
+
* if (error instanceof SebaError) console.error(error.code, error.message);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare class SebaError extends Error {
|
|
26
|
+
/** Stable, machine-readable error code. */
|
|
27
|
+
readonly code: SebaErrorCode;
|
|
28
|
+
constructor(code: SebaErrorCode, message: string, options?: SebaErrorOptions);
|
|
29
|
+
}
|
|
30
|
+
/** Invalid, missing or contradictory library configuration. */
|
|
31
|
+
export declare class SebaConfigError extends SebaError {
|
|
32
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
33
|
+
}
|
|
34
|
+
/** WebID, Client ID Document or OpenID Provider discovery failed. */
|
|
35
|
+
export declare class SebaDiscoveryError extends SebaError {
|
|
36
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The configured redirect URI is not published by the Client ID Document.
|
|
40
|
+
*
|
|
41
|
+
* The message lists both the requested URI and the published URIs so the
|
|
42
|
+
* mismatch can be fixed without inspecting network traffic.
|
|
43
|
+
*/
|
|
44
|
+
export declare class SebaRedirectMismatchError extends SebaError {
|
|
45
|
+
/** Redirect URI requested by the application. */
|
|
46
|
+
readonly requestedRedirectUri: string;
|
|
47
|
+
/** Redirect URIs published in the Client ID Document. */
|
|
48
|
+
readonly publishedRedirectUris: readonly string[];
|
|
49
|
+
constructor(requestedRedirectUri: string, publishedRedirectUris: readonly string[], options?: SebaErrorOptions);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The authorization response, token response or identity token was rejected.
|
|
53
|
+
*
|
|
54
|
+
* Always indicates a security validation failure (state, `iss`, nonce, PKCE,
|
|
55
|
+
* issuer, audience, signature or expiration), never a transient failure.
|
|
56
|
+
*/
|
|
57
|
+
export declare class SebaAuthError extends SebaError {
|
|
58
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The stored session can no longer be used (refresh token revoked, expired or rejected).
|
|
62
|
+
*
|
|
63
|
+
* The local session state has already been cleared when this is thrown.
|
|
64
|
+
*/
|
|
65
|
+
export declare class SebaSessionExpiredError extends SebaError {
|
|
66
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
67
|
+
}
|
|
68
|
+
/** A DPoP proof could not be created, or the provider rejected the DPoP key binding. */
|
|
69
|
+
export declare class SebaDPoPError extends SebaError {
|
|
70
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
71
|
+
}
|
|
72
|
+
/** A network request failed (offline, DNS, TLS, connection reset, non-HTTP response). */
|
|
73
|
+
export declare class SebaNetworkError extends SebaError {
|
|
74
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
75
|
+
}
|
|
76
|
+
/** Interactive login is impossible in the current environment. */
|
|
77
|
+
export declare class SebaNoUIError extends SebaError {
|
|
78
|
+
constructor(message: string, options?: SebaErrorOptions);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Type guard for {@link SebaError}.
|
|
82
|
+
*
|
|
83
|
+
* @param value - Any caught value.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isSebaError(value: unknown): value is SebaError;
|
|
86
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB,oBAAoB,GACpB,4BAA4B,GAC5B,eAAe,GACf,0BAA0B,GAC1B,eAAe,GACf,kBAAkB,GAClB,gBAAgB,CAAC;AAErB,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,2CAA2C;IAC3C,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;gBAEjB,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAK7E;AAED,+DAA+D;AAC/D,qBAAa,eAAgB,SAAQ,SAAS;gBAChC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED,qEAAqE;AACrE,qBAAa,kBAAmB,SAAQ,SAAS;gBACnC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IACtD,iDAAiD;IACjD,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,yDAAyD;IACzD,QAAQ,CAAC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;gBAEtC,oBAAoB,EAAE,MAAM,EAAE,qBAAqB,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAY/G;AAED;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,SAAS;gBACxC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED,wFAAwF;AACxF,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED,yFAAyF;AACzF,qBAAa,gBAAiB,SAAQ,SAAS;gBACjC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED,kEAAkE;AAClE,qBAAa,aAAc,SAAQ,SAAS;gBAC9B,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB;CAGxD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D"}
|