# Solid-OIDC — Authentication for Solid Apps Authenticate users with Solid pods using Solid-OIDC. Zero dependencies, zero build step, single file. ## What This Skill Does Implements the complete Solid-OIDC authentication flow in the browser: login, token refresh, logout, and authenticated fetch with DPoP proof-of-possession tokens. ## Quick Start ```html ``` ## Authentication Flow ``` 1. session.login(idp, redirectUri) ├── Discover OIDC config: GET {idp}/.well-known/openid-configuration ├── Register client: POST registration_endpoint ├── Generate PKCE: code_verifier + code_challenge (S256) └── Redirect to: authorization_endpoint?response_type=code&... 2. User authenticates at identity provider 3. Redirect back with ?code=...&iss=...&state=... 4. session.handleRedirectFromLogin() ├── Verify issuer (RFC 9207) ├── Verify CSRF state ├── Generate DPoP key pair (ES256 / P-256) ├── Exchange code for tokens: POST token_endpoint ├── Validate access token (signature, issuer, audience, DPoP binding) ├── Store refresh token in IndexedDB └── Emit sessionStateChange event 5. session.authFetch(url) ├── Create DPoP proof (RFC 9449) ├── Set Authorization: DPoP {access_token} ├── Set DPoP: {proof_jwt} └── fetch(url, { headers }) ``` ## Session API ### Constructor ```js const session = new Session({ clientId: null, // Pre-registered client ID (skips dynamic registration) database: new SessionDatabase(), // IndexedDB store for session persistence onStateChange: (e) => {}, // { isActive, webId } onExpirationWarning: (e) => {}, // { expires_in } onExpiration: () => {} // Token expired, refresh failed }) ``` ### Methods | Method | Description | |--------|-------------| | `session.login(idp, redirectUri)` | Redirect to identity provider | | `session.handleRedirectFromLogin()` | Handle redirect, exchange code for tokens | | `session.restore()` | Restore session from stored refresh token | | `session.logout()` | Clear session and stored data | | `session.authFetch(url, options)` | Authenticated fetch with DPoP | ### Properties | Property | Type | Description | |----------|------|-------------| | `session.isActive` | `boolean` | User is logged in | | `session.webId` | `string` | User's WebID URI | | `session.isExpired()` | `boolean` | Access token expired | | `session.getExpiresIn()` | `number` | Seconds until expiry | ### Events | Event | Detail | When | |-------|--------|------| | `sessionStateChange` | `{ isActive, webId }` | Login or logout | | `sessionExpirationWarning` | `{ expires_in }` | Refresh failed, not yet expired | | `sessionExpiration` | — | Expired and refresh failed | ## OIDC Discovery Every Solid identity provider exposes: ``` GET {idp}/.well-known/openid-configuration ``` Key fields: | Field | Purpose | |-------|---------| | `issuer` | Canonical issuer URI | | `authorization_endpoint` | Where to send user for login | | `token_endpoint` | Where to exchange code for tokens | | `registration_endpoint` | Dynamic client registration | | `jwks_uri` | Public keys for token verification | ## Dynamic Client Registration If no `clientId` is provided, the library registers automatically: ```json { "application_type": "web", "redirect_uris": ["https://myapp.example/callback"], "token_endpoint_auth_method": "none", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "scope": "openid offline_access webid" } ``` ## DPoP (Demonstration of Proof-of-Possession) Every authenticated request includes a DPoP proof JWT: ``` Authorization: DPoP {access_token} DPoP: {proof_jwt} ``` The DPoP proof contains: | Claim | Value | |-------|-------| | `htu` | Target URL (origin + path) | | `htm` | HTTP method (GET, PUT, etc.) | | `iat` | Current timestamp | | `jti` | Unique identifier | | `ath` | Access token hash (when bound) | The access token's `cnf.jkt` claim must match the DPoP key's thumbprint (RFC 7638). ## PKCE (Proof Key for Code Exchange) Prevents authorization code interception: | Parameter | Value | |-----------|-------| | `code_challenge_method` | `S256` | | `code_challenge` | `BASE64URL(SHA256(code_verifier))` | | `code_verifier` | Random string sent at token exchange | ## Access Token Claims The Solid-OIDC access token contains: | Claim | Purpose | |-------|---------| | `webid` | User's WebID URI | | `iss` | Issuer (identity provider) | | `aud` | Audience (`solid`) | | `exp` | Expiration timestamp | | `iat` | Issued at timestamp | | `client_id` | Registered client identifier | | `cnf.jkt` | DPoP key thumbprint (proof-of-possession binding) | ## Token Refresh Sessions persist across page reloads via IndexedDB: ```js // On page load, try to restore try { await session.restore() // Uses stored refresh_token } catch { // No session to restore } ``` The refresh flow: 1. Read `refresh_token`, `token_endpoint`, `client_id`, `dpop_keypair` from IndexedDB 2. `POST token_endpoint` with `grant_type=refresh_token` 3. Store new refresh token 4. Update session state ## Common Patterns ### Login / Logout Buttons ```js document.getElementById('login').onclick = () => session.login('https://solidcommunity.net', window.location.href) document.getElementById('logout').onclick = () => session.logout() ``` ### Read Private Data ```js const response = await session.authFetch('https://alice.pod/private/notes.ttl') const data = await response.text() ``` ### Write to Pod ```js await session.authFetch('https://alice.pod/public/hello.txt', { method: 'PUT', headers: { 'Content-Type': 'text/plain' }, body: 'Hello, Solid!' }) ``` ### Multiple Identity Providers ```js const providers = [ 'https://solidcommunity.net', 'https://solidweb.org', 'https://login.inrupt.com' ] await session.login(providers[0], window.location.href) ``` ## Installation ```js // CDN (recommended) import { Session } from 'https://esm.sh/gh/JavaScriptSolidServer/solid-oidc/solid-oidc.js' // npm import { Session } from 'solid-oidc' // Local copy import { Session } from './solid-oidc.js' ``` ## Browser Requirements | Requirement | Notes | |-------------|-------| | ES Modules | `