@gaballahtech/seba 0.0.0-stage → 0.2.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.
Files changed (86) hide show
  1. package/ARCHITECTURE.md +242 -0
  2. package/BUILD_REPORT.md +195 -0
  3. package/CHANGELOG.md +59 -0
  4. package/LICENSE +21 -0
  5. package/README.md +282 -2
  6. package/RESEARCH.md +180 -0
  7. package/SECURITY.md +224 -0
  8. package/VALIDATION_REPORT.md +187 -0
  9. package/dist/browser/index.d.ts +9 -0
  10. package/dist/browser/index.d.ts.map +1 -0
  11. package/dist/browser/index.js +9 -0
  12. package/dist/browser/index.js.map +1 -0
  13. package/dist/browser/platform.d.ts +14 -0
  14. package/dist/browser/platform.d.ts.map +1 -0
  15. package/dist/browser/platform.js +60 -0
  16. package/dist/browser/platform.js.map +1 -0
  17. package/dist/browser/provider-modal.d.ts +15 -0
  18. package/dist/browser/provider-modal.d.ts.map +1 -0
  19. package/dist/browser/provider-modal.js +126 -0
  20. package/dist/browser/provider-modal.js.map +1 -0
  21. package/dist/browser/storage.d.ts +32 -0
  22. package/dist/browser/storage.d.ts.map +1 -0
  23. package/dist/browser/storage.js +121 -0
  24. package/dist/browser/storage.js.map +1 -0
  25. package/dist/core/client.d.ts +49 -0
  26. package/dist/core/client.d.ts.map +1 -0
  27. package/dist/core/client.js +371 -0
  28. package/dist/core/client.js.map +1 -0
  29. package/dist/core/discovery.d.ts +82 -0
  30. package/dist/core/discovery.d.ts.map +1 -0
  31. package/dist/core/discovery.js +243 -0
  32. package/dist/core/discovery.js.map +1 -0
  33. package/dist/core/dpop.d.ts +67 -0
  34. package/dist/core/dpop.d.ts.map +1 -0
  35. package/dist/core/dpop.js +176 -0
  36. package/dist/core/dpop.js.map +1 -0
  37. package/dist/core/errors.d.ts +86 -0
  38. package/dist/core/errors.d.ts.map +1 -0
  39. package/dist/core/errors.js +104 -0
  40. package/dist/core/errors.js.map +1 -0
  41. package/dist/core/http.d.ts +68 -0
  42. package/dist/core/http.d.ts.map +1 -0
  43. package/dist/core/http.js +128 -0
  44. package/dist/core/http.js.map +1 -0
  45. package/dist/core/identity.d.ts +145 -0
  46. package/dist/core/identity.d.ts.map +1 -0
  47. package/dist/core/identity.js +735 -0
  48. package/dist/core/identity.js.map +1 -0
  49. package/dist/core/oidc.d.ts +175 -0
  50. package/dist/core/oidc.d.ts.map +1 -0
  51. package/dist/core/oidc.js +413 -0
  52. package/dist/core/oidc.js.map +1 -0
  53. package/dist/core/session.d.ts +116 -0
  54. package/dist/core/session.d.ts.map +1 -0
  55. package/dist/core/session.js +350 -0
  56. package/dist/core/session.js.map +1 -0
  57. package/dist/core/storage.d.ts +22 -0
  58. package/dist/core/storage.d.ts.map +1 -0
  59. package/dist/core/storage.js +48 -0
  60. package/dist/core/storage.js.map +1 -0
  61. package/dist/index.d.ts +18 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +3 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/node/index.d.ts +9 -0
  66. package/dist/node/index.d.ts.map +1 -0
  67. package/dist/node/index.js +9 -0
  68. package/dist/node/index.js.map +1 -0
  69. package/dist/node/platform.d.ts +25 -0
  70. package/dist/node/platform.d.ts.map +1 -0
  71. package/dist/node/platform.js +161 -0
  72. package/dist/node/platform.js.map +1 -0
  73. package/dist/seba.d.ts +39 -0
  74. package/dist/seba.d.ts.map +1 -0
  75. package/dist/seba.js +58 -0
  76. package/dist/seba.js.map +1 -0
  77. package/dist/types.d.ts +279 -0
  78. package/dist/types.d.ts.map +1 -0
  79. package/dist/types.js +8 -0
  80. package/dist/types.js.map +1 -0
  81. package/examples/node/index.ts +61 -0
  82. package/examples/react/App.tsx +127 -0
  83. package/examples/react/index.html +12 -0
  84. package/examples/react/main.tsx +14 -0
  85. package/examples/react/vite.config.ts +27 -0
  86. package/package.json +83 -4
package/README.md CHANGED
@@ -1,3 +1,283 @@
1
- # Temporary Holding Version
1
+ # SEBA
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Solid-OIDC authentication in four calls: `login`, `restoreSession`, `fetch`, `logout`.
4
+
5
+ SEBA implements the Solid-OIDC authorization-code flow with PKCE, DPoP-bound tokens and a
6
+ static Client ID Document. It runs in browsers (IndexedDB session storage, redirect-based
7
+ login) and in Node.js (in-memory storage, real loopback callback server). The public API is
8
+ the same in both.
9
+
10
+ - No dynamic client registration. Your published Client ID Document is the `client_id`.
11
+ - Access tokens live in memory only; the refresh token and DPoP key are the only secrets
12
+ persisted.
13
+ - A brand-new DPoP proof is signed for every single attempt. There is no proof cache and no
14
+ option to add one.
15
+ - Every incoming value is validated: `state`, RFC 9207 `iss`, `nonce`, PKCE S256, ID token
16
+ signature, `iss`, `aud`, `azp`, `exp`/`iat`, and `cnf.jkt` binding.
17
+
18
+ ## Requirements
19
+
20
+ - Node.js 20 or newer, or any browser with Web Crypto (ES256), `fetch` and IndexedDB.
21
+ - A publicly dereferenceable **Solid Client ID Document** served over HTTPS.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npm install @gaballahtech/seba
27
+ ```
28
+
29
+ ## Publish a Client ID Document
30
+
31
+ Host a JSON-LD document and use its URL as `clientId`. The `client_id` member must equal the
32
+ document URL, and every callback you use must be listed verbatim.
33
+
34
+ ```json
35
+ {
36
+ "@context": ["https://www.w3.org/ns/solid/oidc-context.jsonld"],
37
+ "client_id": "https://app.example/client-id.jsonld",
38
+ "redirect_uris": [
39
+ "https://app.example/auth/callback",
40
+ "http://127.0.0.1:3000/auth/callback"
41
+ ],
42
+ "client_name": "My Solid App"
43
+ }
44
+ ```
45
+
46
+ No `registration_endpoint` is ever called; `client_secret` is not used.
47
+
48
+ ## Browser
49
+
50
+ ```ts
51
+ import { Seba } from "@gaballahtech/seba";
52
+
53
+ const seba = new Seba({
54
+ clientId: "https://app.example/client-id.jsonld",
55
+ redirectUri: "https://app.example/auth/callback",
56
+ });
57
+ ```
58
+
59
+ Call `login()` on the page that handles the callback, as well as on the page that starts it:
60
+
61
+ ```ts
62
+ // Runs on every load of the callback route.
63
+ const session = await seba.login({ webId: "https://alice.example/profile/card#me" });
64
+ ```
65
+
66
+ - Without an authorization response in the URL, `login()` stores the PKCE verifier in
67
+ `sessionStorage` and navigates to the provider. **That promise never settles**, because
68
+ the document is replaced.
69
+ - When the provider redirects back with `?code=…&state=…&iss=…`, the same call validates the
70
+ response, exchanges the code, stores the refresh token and DPoP key in IndexedDB, keeps the
71
+ access token in memory, removes the callback parameters from the address bar, and resolves
72
+ with the session.
73
+
74
+ On later page loads:
75
+
76
+ ```ts
77
+ const session = (await seba.restoreSession()) ?? (await seba.login());
78
+ ```
79
+
80
+ `login()` with no arguments opens a small built-in dialog asking for a WebID or an identity
81
+ provider URL. Pass `{ webId }` or `{ issuer }` to skip it.
82
+
83
+ ### React
84
+
85
+ See [`examples/react/App.tsx`](examples/react/App.tsx).
86
+
87
+ ## Node.js
88
+
89
+ Node has no document, so `login()` starts a temporary HTTP server on the loopback interface
90
+ from `redirectUri` and waits for the provider to redirect back. The redirect URI must use
91
+ plain `http` on `127.0.0.1`, `::1` or `localhost`, and must be published in the Client ID
92
+ Document.
93
+
94
+ ```ts
95
+ import { exec } from "node:child_process";
96
+ import { Seba } from "@gaballahtech/seba/node";
97
+
98
+ const seba = new Seba({
99
+ clientId: "https://app.example/client-id.jsonld",
100
+ redirectUri: "http://127.0.0.1:3000/auth/callback",
101
+ openAuthorizationUrl(url) {
102
+ exec(`xdg-open "${url}"`); // or `open` on macOS, `start` on Windows
103
+ },
104
+ });
105
+
106
+ const session = (await seba.restoreSession()) ?? (await seba.login({ issuer: "https://idp.example" }));
107
+ const response = await seba.fetch(session.webId);
108
+ console.log(await response.text());
109
+
110
+ await seba.logout();
111
+ ```
112
+
113
+ Without `openAuthorizationUrl`, `login()` throws `SebaNoUIError` instead of hanging. Node
114
+ storage is in memory, so a process restart requires a new login.
115
+
116
+ See [`examples/node/index.ts`](examples/node/index.ts).
117
+
118
+ ## API
119
+
120
+ ### `new Seba(config)`
121
+
122
+ | Option | Required | Description |
123
+ |---|---|---|
124
+ | `clientId` | yes | HTTPS URL of your Client ID Document (loopback `http` allowed in development). |
125
+ | `redirectUri` | yes | Callback URL published verbatim in `redirect_uris`. Node.js: loopback `http` only. |
126
+ | `storage` | no | `"indexeddb"` (browser default) or `"memory"` (Node.js default). |
127
+ | `debug` | no | `true` emits non-sensitive diagnostics through `console.debug`. |
128
+ | `openAuthorizationUrl` | Node.js | Receives the authorization URL; without it interactive login is impossible. |
129
+
130
+ ### `login(options?)`
131
+
132
+ ```ts
133
+ login(options?: { webId?: string; issuer?: string }): Promise<Session>
134
+ ```
135
+
136
+ Starts or completes an interactive login. Prefers `issuer`; otherwise resolves the issuer
137
+ from the WebID profile (`solid:oidcIssuer`); otherwise asks the user in the browser. The
138
+ resulting WebID must match `options.webId` when it was supplied.
139
+
140
+ ### `restoreSession()`
141
+
142
+ ```ts
143
+ restoreSession(): Promise<Session | null>
144
+ ```
145
+
146
+ Loads the stored refresh token and DPoP key, refreshes the access token, and returns the
147
+ session. Returns `null` when nothing is stored. Throws `SebaSessionExpiredError` when the
148
+ provider rejected the grant (local state is cleared) and `SebaNetworkError` when the
149
+ provider is unreachable (local state is kept).
150
+
151
+ ### `fetch(input, init?)`
152
+
153
+ ```ts
154
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
155
+ ```
156
+
157
+ A `fetch` with `Authorization: DPoP …` and a fresh DPoP proof. Same-origin redirects are
158
+ followed (up to five, one new proof per hop); cross-origin redirects are returned to the
159
+ caller instead of being followed with credentials. One retry is allowed for a
160
+ `use_dpop_nonce` challenge and one for an expired/rejected access token; a streamed body is
161
+ never resent. The access token is refreshed automatically when it is close to expiry.
162
+
163
+ ### `logout()`
164
+
165
+ ```ts
166
+ logout(): Promise<void>
167
+ ```
168
+
169
+ Clears memory and storage first, then revokes the refresh token if the provider advertises a
170
+ revocation endpoint. Revocation failures never block logout.
171
+
172
+ ### `Session`
173
+
174
+ ```ts
175
+ interface Session {
176
+ webId: string;
177
+ isLoggedIn: true;
178
+ provider: string;
179
+ }
180
+ ```
181
+
182
+ No tokens, no keys, no expiry bookkeeping.
183
+
184
+ ## Errors
185
+
186
+ All errors extend `SebaError` and carry a stable `code`:
187
+
188
+ | Class | Meaning |
189
+ |---|---|
190
+ | `SebaConfigError` | Invalid configuration or unsupported environment. |
191
+ | `SebaDiscoveryError` | Issuer, discovery document, WebID profile or Client ID Document could not be validated. |
192
+ | `SebaRedirectMismatchError` | The redirect URI is not published by the Client ID Document. |
193
+ | `SebaAuthError` | Authorization response, token response or ID token failed validation. |
194
+ | `SebaDPoPError` | DPoP key or proof could not be created/verified. |
195
+ | `SebaSessionExpiredError` | The stored grant is no longer valid (state cleared). |
196
+ | `SebaNetworkError` | Transport failure or 5xx response (state kept). |
197
+ | `SebaNoUIError` | Interactive login is impossible in the current runtime (Node.js without a hook). |
198
+
199
+ ## Security defaults
200
+
201
+ - PKCE S256 only; `state` and `nonce` are 256-bit random values, single-use.
202
+ - RFC 9207 `iss` in the authorization response must equal the issuer the login was started
203
+ for.
204
+ - ID token signatures are verified against the discovered JWKS; `aud` must contain the
205
+ client ID and `"solid"` when the provider advertises `webid`; `cnf.jkt` must match the
206
+ session key (and is required for `webid`).
207
+ - The refresh token and DPoP key are the only persisted secrets; access tokens are held in
208
+ a private field.
209
+ - Non-extractable `CryptoKey` storage is used when the engine supports structured cloning;
210
+ otherwise an exported JWK fallback is used (documented in `SECURITY.md`).
211
+ - Refresh-token rotation is atomic, and concurrent refreshes are collapsed into one.
212
+
213
+ See [`SECURITY.md`](SECURITY.md) for the full threat model and limitations, and
214
+ [`ARCHITECTURE.md`](ARCHITECTURE.md) for module boundaries.
215
+
216
+ ## Provider compatibility
217
+
218
+ This table is deliberately conservative: "Publicly tested" means the full login → fetch →
219
+ restore → logout cycle was executed against that provider. If it was not, it says so.
220
+
221
+ | Provider | Protocol support (spec level) | Locally tested | Publicly tested |
222
+ |---|---|---|---|
223
+ | Hand-written fake provider (`tests/integration`) | Solid-OIDC-shaped: array `aud` with `"solid"`, `azp`, `cnf.jkt` | Full cycle in CI (`npm test`) | — |
224
+ | Community Solid Server 7.3.0 | **Not conformant** with Solid-OIDC §8.1: ID token has a string `aud` without `"solid"`, no `cnf` | Discovery, Client ID Document, PKCE/state/nonce, consent, callback, DPoP token exchange all pass; login rejected at ID token validation | No |
225
+ | solidcommunity.net | Discovery and issuer handling verified; issuer is `https://solidcommunity.net/` | Discovery only | **No** — no account was available |
226
+ | solidweb.org | Discovery verified; issuer is `https://solidweb.org` (no trailing slash); no revocation endpoint advertised | Discovery only | **No** — no account was available |
227
+ | solidweb.me | Discovery verified; issuer is `https://solidweb.me/` | Discovery only | **No** — no account was available |
228
+
229
+ > **⚠️ Community Solid Server 7.3.0**: CSS issues `id_token.aud` as a string instead of `["solid", client_id]` and omits `cnf.jkt`, violating Solid-OIDC §8.1. Seba correctly rejects this token. Track the upstream issue at https://github.com/CommunitySolidServer/CommunitySolidServer/issues/2295. Do not weaken Seba's validation to work around this.
230
+
231
+ No public provider has completed the full cycle in this release. See
232
+ [`VALIDATION_REPORT.md`](VALIDATION_REPORT.md) for transcripts and blockers, and
233
+ [`CHANGELOG.md`](CHANGELOG.md) for what changed.
234
+
235
+ SEBA does not support dynamic client registration. The Client ID Document URL is the
236
+ `client_id`, and the provider must be able to dereference it.
237
+
238
+ ## Running the examples
239
+
240
+ Node (after `npm run build`; requires a provider and a published loopback redirect URI):
241
+
242
+ ```bash
243
+ SEBA_CLIENT_ID=http://localhost:8443/client-id.jsonld \
244
+ SEBA_REDIRECT_URI=http://localhost:3001/auth/callback \
245
+ SEBA_ISSUER=http://localhost:3000 \
246
+ WEBID=http://localhost:3000/alice/profile/card#me \
247
+ node examples/node/index.ts
248
+ ```
249
+
250
+ React:
251
+
252
+ ```bash
253
+ VITE_SEBA_CLIENT_ID=https://app.example/client-id.jsonld npm run example:react:build
254
+ npx vite preview --config examples/react/vite.config.ts
255
+ ```
256
+
257
+ The React example expects its redirect URI to be `${window.location.origin}/auth/callback`
258
+ and published in the Client ID Document.
259
+
260
+ ## Validation tooling
261
+
262
+ ```bash
263
+ npm run validate:discovery # discovery against public providers (no login)
264
+ node scripts/validation/css-setup.mjs # create a local CSS account + pod
265
+ node scripts/validation/css-flow.mjs # full flow against local CSS (see report)
266
+ ```
267
+
268
+ ## Development
269
+
270
+ ```bash
271
+ npm install
272
+ npm run typecheck
273
+ npm test
274
+ npm run build
275
+ ```
276
+
277
+ The test suite includes unit tests, DPoP replay tests, and an integration suite that runs the
278
+ full login → fetch → refresh → logout flow against a hand-written OpenID Provider over
279
+ loopback HTTP.
280
+
281
+ ## License
282
+
283
+ MIT
package/RESEARCH.md ADDED
@@ -0,0 +1,180 @@
1
+ # RESEARCH
2
+
3
+ Bounded research for SEBA. Ten-source budget; sources below were actually fetched and read
4
+ during this project (no blog posts, no secondary summaries).
5
+
6
+ ## Sources inspected
7
+
8
+ 1. **Solid-OIDC specification** (`solid.github.io/solid-oidc`, Solid-OIDC 1.0 draft) —
9
+ §5 client identification, §6 WebID issuer discovery, §7 authorization code flow + PKCE,
10
+ §8 callback/`iss` handling, §9 token validation and DPoP binding, §10 refresh,
11
+ §11 conformance discovery.
12
+ 2. **RFC 9449 — OAuth 2.0 Demonstrating Proof of Possession (DPoP)** — §4 proof JWT
13
+ construction and validation, §4.2 claims, §4.3 `ath`, §5 token requests, §6 nonce,
14
+ §7 protected resource access, §8 server-side validation rules.
15
+ 3. **RFC 7636 — Proof Key for Code Exchange** — §4.1 verifier generation, §4.2 S256
16
+ challenge, §4.3/4.4/4.5 exchange rules.
17
+ 4. **OpenID Connect Discovery 1.0 (errata set 2)** — §3 metadata values, §4 discovery
18
+ request/response/validation, §7.1 TLS requirements.
19
+ 5. **`jose` 6.2.15 type definitions and exports inspected locally** (`SignJWT`,
20
+ `jwtVerify`, `createRemoteJWKSet`, `generateKeyPair`, `exportJWK`, `importJWK`,
21
+ `calculateJwkThumbprint`, `base64url`, `errors.JOSEError`).
22
+ 6. **`idb` 8.0.4 type definitions inspected locally** (`openDB`, `IDBPDatabase`, upgrade
23
+ callbacks, `IDBPDatabase` transaction helpers).
24
+
25
+ OpenID Connect Core 1.0 §3.1.3.7 (ID token validation steps) was not fetched separately;
26
+ its steps are reproduced here through the explicit requirements Solid-OIDC §9 places on
27
+ the same validations, and each step is implemented and covered by a test in
28
+ `tests/unit/oidc.test.ts`.
29
+
30
+ ## Relevant protocol requirements
31
+
32
+ ### Client identification (Solid-OIDC §5)
33
+
34
+ - `client_id` is the URL of a dereferenceable **Client ID Document**, served over HTTPS.
35
+ - The document is JSON-LD and carries the Solid-OIDC context
36
+ `https://www.w3.org/ns/solid/oidc-context.jsonld`.
37
+ - The document's `client_id` member must equal the URL it is served from.
38
+ - `redirect_uris` must list the exact redirect URI used in the authorization request.
39
+ - Dynamic Client Registration is not used, so the client never calls
40
+ `registration_endpoint` (SEBA ignores that metadata entirely).
41
+
42
+ ### WebID discovery (Solid-OIDC §6)
43
+
44
+ - Given a WebID `https://host/profile/card#me`, the profile document is
45
+ `https://host/profile/card` — the fragment identifies the subject, it is not sent to the
46
+ server.
47
+ - The profile must state `<webID> solid:oidcIssuer <issuer>`.
48
+ - The issuer must be an `https` URL with a host, and no query or fragment. Loopback `http`
49
+ is accepted for local development only.
50
+ - JSON-LD (`application/ld+json`) and Turtle (`text/turtle`) are the interoperable
51
+ profile formats; a `Link` header with
52
+ `rel="http://www.w3.org/ns/solid/terms#oidcIssuer"` is a documented fallback.
53
+
54
+ ### OpenID Provider discovery (OIDC Discovery §3/§4)
55
+
56
+ - `issuer` in the metadata document must be identical to the requested issuer; a mismatch
57
+ invalidates the whole document.
58
+ - `authorization_endpoint`, `token_endpoint` and `jwks_uri` are required and must be
59
+ absolute `https` URLs.
60
+ - `revocation_endpoint` and `dpop_signing_alg_values_supported` are optional.
61
+ - For issuers with a path component, RFC 8414 places the well-known segment between host
62
+ and path (`https://host/.well-known/openid-configuration/<path>`); OIDC Discovery
63
+ examples also show `https://host/<path>/.well-known/openid-configuration`. SEBA tries
64
+ the RFC 8414 form first and falls back to the appended form on 404 (documented
65
+ compatibility handling).
66
+
67
+ ### Authorization Code Flow + PKCE (RFC 7636, OIDC Core)
68
+
69
+ - `code_verifier`: 43–128 characters from `[A-Za-z0-9-._~]`.
70
+ - `code_challenge` = `BASE64URL(SHA-256(ASCII(code_verifier)))`, `code_challenge_method=S256`.
71
+ - `state` must be cryptographically random and validated before the code is used.
72
+ - `nonce` must be cryptographically random and must match the ID token's `nonce` claim.
73
+ - The token request repeats `redirect_uri`, `client_id`, `code` and `code_verifier`.
74
+
75
+ ### DPoP (RFC 9449)
76
+
77
+ - Proof header: `typ=dpop+jwt`, `alg=ES256`, `jwk` = **public** key only.
78
+ - Payload: `jti` (unique), `htm` (uppercase method), `htu` (target URI without query and
79
+ fragment, syntactically normalized), `iat` (seconds).
80
+ - `ath` = `BASE64URL(SHA-256(ASCII(access_token)))`, included only when an access token is
81
+ presented (protected-resource requests), never on token-endpoint requests.
82
+ - Proofs are single-use by design: the server tracks `jti`. A new proof is required for
83
+ every attempt, including retries and authorization-server nonce challenges.
84
+ - The authorization server binds the issued access/refresh token to the proof key via
85
+ `cnf.jkt` (JWK thumbprint). The client must keep using that key.
86
+ - `DPoP-Nonce` challenges (`use_dpop_nonce`) require a fresh proof carrying the new
87
+ `nonce`; a nonce may be cached, a signed proof may not.
88
+
89
+ ### Refresh (Solid-OIDC §10, RFC 9449 §5)
90
+
91
+ - `grant_type=refresh_token` with a fresh DPoP proof bound to the same key.
92
+ - Refresh tokens rotate; the newest refresh token must be persisted atomically.
93
+ - A rotated/deleted refresh token that is rejected invalidates the local session.
94
+
95
+ ## Existing implementation findings
96
+
97
+ No existing implementation was consulted, by design: `@inrupt/solid-client-authn-*` is
98
+ excluded by the task, and reading a reference client was not necessary once the normative
99
+ text above was available. The only "existing implementation" inspected was the installed
100
+ `jose`/`idb` type surface, to confirm the exact function signatures used here
101
+ (for example `generateKeyPair` returning `{ privateKey, publicKey }` and defaulting to
102
+ non-extractable keys, and `calculateJwkThumbprint` defaulting to SHA-256).
103
+
104
+ ## Security findings that shaped the design
105
+
106
+ 1. **Proof reuse is the primary DPoP failure mode.** Any cache keyed by `(htu, htm)`
107
+ silently disables replay protection. SEBA therefore has no proof cache, no TTL, and no
108
+ configuration knob for one; every attempt signs a brand-new JWT with a new `jti`.
109
+ 2. **Refresh tokens are bearer-equivalent if the key leaks.** Both the refresh token and
110
+ the private key live in IndexedDB; an XSS on the application origin can use both. This
111
+ is inherent to browser-based public clients and is documented, not "solved".
112
+ 3. **`redirect: "manual"` makes cross-origin redirects opaque in browsers.** Credentials
113
+ cannot be re-attached safely, so SEBA never follows cross-origin redirects with
114
+ `Authorization`/`DPoP` headers, and returns the redirect response to the caller.
115
+ 4. **Content negotiation matters.** Solid providers serve WebID profiles as
116
+ `text/turtle`, `application/ld+json` or HTML with a `Link` header; a parser written for
117
+ one concrete example is fragile.
118
+ 5. **Provider clock skew is real.** A 60-second tolerance is applied to `exp`/`nbf`/`iat`;
119
+ it is not configurable, so it cannot be widened into a replay window.
120
+ 6. **RFC 9207 `iss` is mandatory in Solid-OIDC** and prevents mix-up attacks; SEBA rejects
121
+ callbacks without it.
122
+
123
+ ## Identified technical limitations
124
+
125
+ - **Browsers**: `fetch(..., { redirect: "manual" })` yields an opaque response
126
+ (status `0`) for redirects, so redirected requests are surfaced to the caller instead of
127
+ being followed. The library never re-sends credentials to another origin.
128
+ - **IndexedDB** is not available in private-mode variants of some older browsers; SEBA
129
+ fails with `SebaConfigError` rather than falling back to `localStorage`.
130
+ - **Non-extractable keys**: whether a `CryptoKey` survives IndexedDB structured cloning
131
+ depends on the engine. SEBA probes this at login and, when cloning is impossible, stores
132
+ an exported JWK instead (documented downgrade, never silent).
133
+ - **Token introspection / revocation semantics** vary per provider; revocation is
134
+ best-effort and never blocks local logout.
135
+ - **Live provider interoperability was not tested**: there is no network access to any
136
+ Solid identity provider from the build environment and no test account. Compatibility
137
+ claims in `README.md` are limited accordingly.
138
+
139
+ ## Unresolved questions (and the conservative decision taken)
140
+
141
+ 1. **Does every provider emit `iss` on the authorization response (RFC 9207)?**
142
+ Solid-OIDC requires it. Decision: require it, fail closed with `SebaAuthError`, and
143
+ document the requirement. A silent fallback would re-open mix-up attacks.
144
+ 2. **Should `aud` contain `"solid"`?** Solid-OIDC requires the ID token audience to
145
+ include both the client ID and `solid`. Decision: require `"solid"` **only when the
146
+ provider advertises the `webid` scope** in `scopes_supported`; this keeps SEBA strict for
147
+ Solid providers while remaining usable against plain OIDC providers during evaluation.
148
+ `azp` is required whenever `aud` has more than one value, and must equal `client_id`.
149
+ 3. **Is `cnf.jkt` always present on ID tokens?** Not guaranteed by every deployment.
150
+ Decision: validate `cnf.jkt` whenever present, and require it when the provider
151
+ advertises `webid`. Access-token binding is enforced by the resource server regardless.
152
+ 4. **Turtle breadth.** A full RDF parser is a large dependency. Decision: a documented
153
+ subset parser (`src/core/identity.ts`) plus JSON-LD and `Link`-header support; exact
154
+ supported syntax is listed in `README.md` and each construct is unit-tested. Unsupported
155
+ constructs raise `SebaDiscoveryError` instead of being mis-parsed.
156
+ 5. **Issuer with a path.** Decision: try RFC 8414 ordering, then the appended form, and
157
+ never accept a discovery document whose `issuer` differs from the request.
158
+
159
+ ## Explicit decisions
160
+
161
+ | Decision | Justification |
162
+ |---|---|
163
+ | Static Client ID Document only; DCR never implemented | Required by the product constraint; removes a whole class of client-registration attacks and provider-specific behaviour. |
164
+ | ES256 / P-256 DPoP keys only | Required by Solid-OIDC; avoids algorithm negotiation bugs. |
165
+ | No proof cache, no TTL, no `trustedZoneTtl` option | RFC 9449 replay protection is per-proof; any reuse optimisation would defeat it. |
166
+ | Access token held in a private field only | The spec forbids persisting access tokens; nothing else in the library exposes them. |
167
+ | Refresh token + key + metadata in IndexedDB, keyed by `client_id` | One origin can host several applications; keying by client ID avoids cross-app overwrite. |
168
+ | `sessionStorage` for `state`/`nonce`/`code_verifier` | Survives the redirect round-trip, dies with the tab, never persisted to disk like `localStorage`. |
169
+ | Node.js uses a real loopback HTTP server on `127.0.0.1`/`localhost` | Only honest way to receive the callback without browser APIs; bound to loopback only, closed in `finally`. |
170
+ | `openAuthorizationUrl` required for Node interactive login | The library must not pretend to open a browser; without the hook it throws `SebaNoUIError`. |
171
+ | ID token validated with `jose` + remote JWKS, `requiredClaims: exp/iat/iss/aud/sub` | Signature and claim validation before any identity value is trusted. |
172
+ | `htu` = `new URL(target)` with search and hash removed | Matches RFC 9449's "URI without query and fragment" plus RFC 3986 syntax normalization. |
173
+ | Errors are typed classes with stable codes | Applications branch on `error.code`; no string matching, no raw string throws. |
174
+
175
+ ## Dependency decision
176
+
177
+ Runtime dependencies are exactly `jose` (JWS/JWK/JWKS primitives) and `idb` (IndexedDB).
178
+ No RDF library was added: the required WebID assertion is a single triple, and the subset
179
+ parser is ~200 lines, fully tested, with JSON-LD and `Link` fallbacks. No UI framework is
180
+ included; the browser provider selector is vanilla DOM in `src/browser/provider-modal.ts`.