Authenticate users with Solid pods using Solid-OIDC. Zero dependencies, zero build step, single file.
Implements the complete Solid-OIDC authentication flow in the browser: login, token refresh, logout, and authenticated fetch with DPoP proof-of-possession tokens.
<script type="module">
import { Session } from 'https://esm.sh/gh/JavaScriptSolidServer/solid-oidc/solid-oidc.js'
const session = new Session({
onStateChange: (e) => {
console.log(e.detail.isActive ? `Logged in as ${e.detail.webId}` : 'Not logged in')
}
})
// Restore previous session
session.restore().catch(() => {})
// Handle redirect from identity provider
session.handleRedirectFromLogin()
</script>
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 })
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
})
| 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 |
| 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 |
| Event | Detail | When |
|---|---|---|
sessionStateChange |
{ isActive, webId } |
Login or logout |
sessionExpirationWarning |
{ expires_in } |
Refresh failed, not yet expired |
sessionExpiration |
— | Expired and refresh failed |
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 |
If no clientId is provided, the library registers automatically:
{
"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"
}
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).
Prevents authorization code interception:
| Parameter | Value |
|---|---|
code_challenge_method |
S256 |
code_challenge |
BASE64URL(SHA256(code_verifier)) |
code_verifier |
Random string sent at token exchange |
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) |
Sessions persist across page reloads via IndexedDB:
// On page load, try to restore
try {
await session.restore() // Uses stored refresh_token
} catch {
// No session to restore
}
The refresh flow:
refresh_token, token_endpoint, client_id, dpop_keypair from IndexedDBPOST token_endpoint with grant_type=refresh_tokendocument.getElementById('login').onclick = () =>
session.login('https://solidcommunity.net', window.location.href)
document.getElementById('logout').onclick = () =>
session.logout()
const response = await session.authFetch('https://alice.pod/private/notes.ttl')
const data = await response.text()
await session.authFetch('https://alice.pod/public/hello.txt', {
method: 'PUT',
headers: { 'Content-Type': 'text/plain' },
body: 'Hello, Solid!'
})
const providers = [
'https://solidcommunity.net',
'https://solidweb.org',
'https://login.inrupt.com'
]
await session.login(providers[0], window.location.href)
// 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'
| Requirement | Notes |
|---|---|
| ES Modules | <script type="module"> |
crypto.subtle |
Requires HTTPS or localhost |
indexedDB |
Session persistence |
| Spec | Description |
|---|---|
| Solid-OIDC | Solid authentication |
| RFC 6749 | OAuth 2.0 |
| RFC 7636 | PKCE |
| RFC 7638 | JWK Thumbprint |
| RFC 9207 | Authorization Server Issuer Identification |
| RFC 9449 | DPoP |
AGPL-3.0