Skip to content

Commit f22e47f

Browse files
v0.1: one-button Solid SSO
A static HTML page (no build, no server) that wraps the standard Solid-OIDC flow with the smallest possible UI surface: a single "Sign in" button. The button starts an OIDC redirect to a configurable IdP (default solid.social). The IdP handles identity proof — Schnorr via Podkey/xlogin, passkey, or password — and redirects back. The page then sends the user to their pod root, resolved from the WebID returned in the session. What's in this commit: - index.html — single-button, full-screen, dark-mode aware - login.js — Solid-OIDC client via @inrupt/solid-client-authn-browser (loaded from esm.sh — no install, no build) - README — vision + run instructions + roadmap - LICENSE — MIT - .gitignore — editor/OS artefacts Configuration via URL params (persisted to localStorage so the post-redirect navigation finds them): ?idp=<oidc-issuer> — defaults to https://solid.social ?next=<destination> — defaults to the WebID's pod root Stack rationale: - No build step (matches JSS Git, #370) - No native dependencies - Servable from any static host OR from a Solid pod itself ("eat own dog food") - Same release cadence as a static site, not the JSS server
0 parents  commit f22e47f

5 files changed

Lines changed: 336 additions & 0 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Editor / OS artefacts
2+
.DS_Store
3+
.vscode/
4+
*.swp
5+
*~
6+
7+
# Optional local dev-server output
8+
node_modules/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Melvin Carvalho
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# JSS SSO
2+
3+
Universal one-click sign-in for Solid pods. One screen, one button.
4+
You click, the universe figures out who you are, you land at your pod.
5+
6+
## What it is
7+
8+
A static HTML page (no build step, no server) that wraps the standard
9+
Solid-OIDC flow with the smallest possible UI surface — a single button.
10+
The button kicks off OIDC against a configurable IdP, the IdP handles
11+
the actual identity proof (Schnorr via [Podkey](https://github.com/JavaScriptSolidServer/podkey)
12+
/ [xlogin](https://github.com/JavaScriptSolidServer/xlogin), passkey,
13+
or password), and the user lands at their pod root.
14+
15+
No username box. No IdP picker. No "what app are you logging in from?"
16+
question. Click → resolve → arrive.
17+
18+
## What it does
19+
20+
```
21+
┌────────────────────────────────────────┐
22+
│ sso.solid.social (or any host) │
23+
│ ┌──────────────────────┐ │
24+
│ │ Sign in to Solid │ ← only UI │
25+
│ └──────────────────────┘ │
26+
└────────────────┬───────────────────────┘
27+
│ Solid-OIDC redirect to configured IdP
28+
29+
┌────────────────────────────────────────┐
30+
│ JSS resolver chain (already shipped) │
31+
│ 1. CID v1 verificationMethod (#399) │
32+
│ 2. Local index (.idp/accounts) │
33+
│ 3. .well-known/did/nostr (#408, #411)│
34+
│ 4. External HTTP did:nostr resolver │
35+
│ 5. did:nostr identity (fallback) │
36+
└────────────────┬───────────────────────┘
37+
│ resolved WebID
38+
39+
┌────────────────────────────────────────┐
40+
│ Redirect to pod root │
41+
└────────────────────────────────────────┘
42+
```
43+
44+
Same-pod users hit the local index — zero typing required. Cross-pod
45+
users hit the configured HTTP resolver until #414 (relay-based resolution)
46+
lands, after which the resolution becomes fully decentralized.
47+
48+
## Run it locally
49+
50+
No build step. Pure static files. Any static server works:
51+
52+
```bash
53+
npx serve . # any port
54+
python3 -m http.server 8080
55+
```
56+
57+
Or open `index.html` directly (CDN imports resolve over the network).
58+
59+
## Configuration
60+
61+
URL params (also persisted to `localStorage` so post-redirect navigation
62+
finds them):
63+
64+
| Param | Default | Purpose |
65+
|-------|---------|---------|
66+
| `idp` | `https://solid.social` | OIDC issuer to authenticate against |
67+
| `next` | pod root derived from WebID | Where to land after sign-in |
68+
69+
Example: a Solid app wants users to authenticate and come back —
70+
71+
```
72+
https://sso.solid.social/?idp=https://solid.social&next=https://myapp.example/
73+
```
74+
75+
## Hosting
76+
77+
Three sensible deployment targets:
78+
79+
1. **GitHub Pages** on this repo (`gh-pages` branch is the default).
80+
2. **A Solid pod** — eats own dog food. Drop `index.html` + `login.js`
81+
at `<pod>/sso/` and the SSO page itself becomes a Solid resource.
82+
3. **`jss.live/sso/`** — pairs with the other JSS no-build apps
83+
([JSS Git](https://jss.live/git/)).
84+
85+
## Status
86+
87+
**v0.1 — proof of concept**. Demonstrates the one-button UX end-to-end
88+
against `solid.social` as the IdP. Same-pod resolution works zero-typing
89+
(JSS shipped the underlying resolver chain in
90+
[#408](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/408)
91+
through
92+
[#418](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/418)).
93+
94+
## Roadmap
95+
96+
- **v0.1** — Single-button OIDC against a configurable IdP. ✅ (this commit)
97+
- **v0.2** — Auto-discover IdP from a Nostr extension's pubkey (NIP-05 or
98+
did:nostr DID-doc).
99+
- **v0.3** — Direct Nostr-relay resolution (sits on top of
100+
[JSS #414](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/414)).
101+
Removes the configured-IdP dependency for cross-pod identities.
102+
- **v0.4** — Passkey-only fallback. Sign in without ever installing a
103+
Nostr extension.
104+
105+
## Why a separate repo?
106+
107+
- The SSO page is a **product**, not a feature of the server. It lives
108+
on top of JSS but isn't part of JSS's runtime.
109+
- It's a **client** — pure browser code, no Node dependencies, no test
110+
suite, no deploy pipeline. Different release cadence than the server.
111+
- It can be **hosted by anyone**, including by a Solid pod itself.
112+
- It's the visible artifact of the resolver-chain work — the part of
113+
the stack people interact with directly.
114+
115+
## License
116+
117+
MIT. See [LICENSE](./LICENSE).

index.html

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Sign in to Solid</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<meta name="description" content="Universal one-click sign-in for Solid pods. Click the button — you're at your pod.">
8+
9+
<!-- Open Graph -->
10+
<meta property="og:title" content="JSS SSO — one click, you're at your pod">
11+
<meta property="og:description" content="Universal Solid login. No username, no IdP picker, no friction.">
12+
<meta property="og:type" content="website">
13+
14+
<style>
15+
:root {
16+
--bg: #fafbfc;
17+
--fg: #1f2328;
18+
--accent: #6839e6;
19+
--accent-hover: #5328d4;
20+
--muted: #656d76;
21+
--error: #cf222e;
22+
}
23+
@media (prefers-color-scheme: dark) {
24+
:root {
25+
--bg: #0d1117;
26+
--fg: #e6edf3;
27+
--accent: #a78bfa;
28+
--accent-hover: #c4b5fd;
29+
--muted: #8b949e;
30+
}
31+
}
32+
* { box-sizing: border-box; }
33+
html, body {
34+
margin: 0; padding: 0; height: 100%;
35+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
36+
background: var(--bg); color: var(--fg);
37+
}
38+
main {
39+
min-height: 100vh;
40+
display: flex; flex-direction: column;
41+
align-items: center; justify-content: center;
42+
padding: 2rem; gap: 2rem;
43+
text-align: center;
44+
}
45+
h1 { font-size: clamp(1.5rem, 4vw, 2.5rem); margin: 0; font-weight: 600; }
46+
p.tagline { color: var(--muted); margin: 0; max-width: 32rem; }
47+
button#signin {
48+
font-size: 1.25rem;
49+
padding: 1rem 2.5rem;
50+
background: var(--accent); color: white;
51+
border: none; border-radius: 999px;
52+
cursor: pointer;
53+
font-weight: 600;
54+
transition: background 0.15s, transform 0.05s;
55+
min-width: 14rem;
56+
}
57+
button#signin:hover { background: var(--accent-hover); }
58+
button#signin:active { transform: scale(0.98); }
59+
button#signin:disabled { opacity: 0.6; cursor: progress; }
60+
#status { color: var(--muted); font-size: 0.9rem; min-height: 1.5em; }
61+
#status.error { color: var(--error); }
62+
footer {
63+
margin-top: 2rem; color: var(--muted); font-size: 0.85rem;
64+
}
65+
footer a { color: inherit; }
66+
</style>
67+
</head>
68+
<body>
69+
<main>
70+
<h1>Sign in to Solid</h1>
71+
<p class="tagline">One click. We resolve your Nostr / WebID. You land at your pod.</p>
72+
<button id="signin">Sign in</button>
73+
<p id="status" aria-live="polite"></p>
74+
<footer>
75+
<a href="https://github.com/JavaScriptSolidServer/sso">source</a>
76+
&nbsp;·&nbsp;
77+
<a href="https://github.com/JavaScriptSolidServer/JavaScriptSolidServer">JSS</a>
78+
</footer>
79+
</main>
80+
<script type="module" src="./login.js"></script>
81+
</body>
82+
</html>

login.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// JSS SSO — one-click Solid sign-in.
2+
//
3+
// Flow:
4+
// 1. Page loads. If a Solid-OIDC session was already restored
5+
// (handleIncomingRedirect), redirect straight to the user's
6+
// pod root. Skip the button.
7+
// 2. Otherwise: button click → `login()` triggers a redirect to
8+
// the configured IdP's /auth endpoint. The IdP handles the
9+
// actual identity proof (Schnorr / passkey / password) and
10+
// redirects back here with auth state.
11+
// 3. On return: step 1 fires, the session is hydrated, and we
12+
// redirect to the resolved WebID's pod root.
13+
//
14+
// Configurable via URL params or localStorage:
15+
// ?idp=<oidc-issuer> — defaults to https://solid.social
16+
// ?next=<destination-url> — defaults to the WebID's pod root
17+
//
18+
// Both are persisted so a freshly-redirected-back page knows
19+
// where to send the user even if the params got stripped on the
20+
// IdP round-trip.
21+
22+
import {
23+
login,
24+
handleIncomingRedirect,
25+
getDefaultSession,
26+
} from 'https://esm.sh/@inrupt/[email protected]';
27+
28+
const DEFAULT_IDP = 'https://solid.social';
29+
30+
function readConfig() {
31+
const params = new URLSearchParams(location.search);
32+
const idp = params.get('idp')
33+
|| localStorage.getItem('jss-sso:idp')
34+
|| DEFAULT_IDP;
35+
const next = params.get('next')
36+
|| localStorage.getItem('jss-sso:next')
37+
|| '';
38+
return { idp, next };
39+
}
40+
41+
function setStatus(msg, isError = false) {
42+
const el = document.querySelector('#status');
43+
if (!el) return;
44+
el.textContent = msg;
45+
el.classList.toggle('error', isError);
46+
}
47+
48+
function podRootFromWebId(webId) {
49+
// WebID is typically `https://<pod-host>/profile/card.jsonld#me`
50+
// or `https://<pod-host>/<name>/profile/card.jsonld#me`. The pod
51+
// root is the origin (+ pod-name segment if present). For v0.1
52+
// we just go to the origin — works for both subdomain-mode pods
53+
// and path-mode root pods. Path-mode named pods land at the
54+
// server root and can navigate from there.
55+
try {
56+
return new URL('/', webId).href;
57+
} catch {
58+
return null;
59+
}
60+
}
61+
62+
async function init() {
63+
const { idp, next } = readConfig();
64+
65+
// If we're coming back from the IdP, hydrate the session.
66+
try {
67+
await handleIncomingRedirect({
68+
restorePreviousSession: true,
69+
});
70+
} catch (err) {
71+
setStatus(`Sign-in failed: ${err.message}`, true);
72+
return;
73+
}
74+
75+
const session = getDefaultSession();
76+
if (session.info.isLoggedIn && session.info.webId) {
77+
const webId = session.info.webId;
78+
const dest = next || podRootFromWebId(webId);
79+
setStatus(`Signed in as ${webId}. Taking you to your pod…`);
80+
// Clear the saved `next` so a subsequent visit doesn't re-use it.
81+
localStorage.removeItem('jss-sso:next');
82+
setTimeout(() => { location.href = dest; }, 800);
83+
return;
84+
}
85+
86+
// Fresh visit. Wire the button.
87+
const button = document.querySelector('#signin');
88+
if (!button) return;
89+
button.addEventListener('click', async () => {
90+
button.disabled = true;
91+
setStatus('Redirecting to your identity provider…');
92+
// Persist config so the redirect back can find it.
93+
localStorage.setItem('jss-sso:idp', idp);
94+
if (next) localStorage.setItem('jss-sso:next', next);
95+
try {
96+
await login({
97+
oidcIssuer: idp,
98+
redirectUrl: location.origin + location.pathname,
99+
clientName: 'JSS SSO',
100+
});
101+
} catch (err) {
102+
setStatus(`Could not start sign-in: ${err.message}`, true);
103+
button.disabled = false;
104+
}
105+
});
106+
}
107+
108+
init();

0 commit comments

Comments
 (0)