javascript-solid-server 0.0.140 → 0.0.142
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/package.json +1 -1
- package/src/idp/index.js +34 -1
- package/src/idp/views.js +76 -1
- package/src/server.js +2 -0
- package/test/idp.test.js +54 -0
package/package.json
CHANGED
package/src/idp/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from './credentials.js';
|
|
25
25
|
import * as passkey from './passkey.js';
|
|
26
26
|
import { addTrustedIssuer } from '../auth/solid-oidc.js';
|
|
27
|
+
import { landingPage } from './views.js';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* IdP Fastify Plugin
|
|
@@ -140,7 +141,7 @@ export async function idpPlugin(fastify, options) {
|
|
|
140
141
|
|
|
141
142
|
// Catch-all route for oidc-provider paths
|
|
142
143
|
// Must be registered BEFORE specific routes to be matched as fallback
|
|
143
|
-
const oidcPaths = ['/idp/
|
|
144
|
+
const oidcPaths = ['/idp/token', '/idp/reg', '/idp/me', '/idp/session', '/idp/session/*'];
|
|
144
145
|
|
|
145
146
|
for (const path of oidcPaths) {
|
|
146
147
|
fastify.route({
|
|
@@ -150,9 +151,41 @@ export async function idpPlugin(fastify, options) {
|
|
|
150
151
|
});
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
// /idp/auth: guard against the human-fat-fingered case where someone opens
|
|
155
|
+
// the URL directly with no `client_id`. oidc-provider would otherwise
|
|
156
|
+
// return a raw `invalid_request` error page; we 302 to the friendly
|
|
157
|
+
// landing instead. Real OIDC requests (with client_id) pass through.
|
|
158
|
+
fastify.route({
|
|
159
|
+
// HEAD is listed explicitly so the route's contract doesn't depend on
|
|
160
|
+
// Fastify's auto-HEAD-from-GET (which `exposeHeadRoutes` can disable);
|
|
161
|
+
// the handler below treats HEAD the same as GET for the bare-client_id
|
|
162
|
+
// redirect.
|
|
163
|
+
method: ['GET', 'HEAD', 'POST', 'DELETE', 'OPTIONS'],
|
|
164
|
+
url: '/idp/auth',
|
|
165
|
+
handler: async (request, reply) => {
|
|
166
|
+
// Only catch the truly-bare case (no `client_id` param at all). An
|
|
167
|
+
// explicit empty string is a malformed OIDC request — let
|
|
168
|
+
// oidc-provider surface the spec error instead of redirecting.
|
|
169
|
+
if (
|
|
170
|
+
(request.method === 'GET' || request.method === 'HEAD') &&
|
|
171
|
+
request.query?.client_id === undefined
|
|
172
|
+
) {
|
|
173
|
+
return reply.redirect('/idp');
|
|
174
|
+
}
|
|
175
|
+
return forwardToProvider(request, reply);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
153
179
|
// Also handle /idp/auth/:uid for continued authorization after login
|
|
154
180
|
fastify.get('/idp/auth/:uid', forwardToProvider);
|
|
155
181
|
|
|
182
|
+
// Friendly landing — Create Account button + sign-in note.
|
|
183
|
+
// Pairs with the /idp/auth guard above so a human visitor lands here
|
|
184
|
+
// rather than on a raw OIDC error.
|
|
185
|
+
fastify.get('/idp', async (request, reply) => {
|
|
186
|
+
return reply.type('text/html').send(landingPage({ baseUri: issuer }));
|
|
187
|
+
});
|
|
188
|
+
|
|
156
189
|
// Token sub-paths
|
|
157
190
|
fastify.route({
|
|
158
191
|
method: ['GET', 'POST'],
|
package/src/idp/views.js
CHANGED
|
@@ -550,6 +550,79 @@ export function errorPage(title, message) {
|
|
|
550
550
|
`;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
+
/**
|
|
554
|
+
* Friendly landing page for the IdP root.
|
|
555
|
+
*
|
|
556
|
+
* The OIDC authorization endpoint (/idp/auth) requires a client_id; opening
|
|
557
|
+
* /idp manually used to drop the user into a raw OIDC error. This page is
|
|
558
|
+
* the human-navigable entry point — Create Account is wired up; Sign In is
|
|
559
|
+
* intentionally a description for now (PR-B / #286 will introduce a
|
|
560
|
+
* standalone /idp/login form).
|
|
561
|
+
*/
|
|
562
|
+
export function landingPage(ctx = {}) {
|
|
563
|
+
const issuer = ctx.baseUri || '';
|
|
564
|
+
return `
|
|
565
|
+
<!DOCTYPE html>
|
|
566
|
+
<html lang="en">
|
|
567
|
+
<head>
|
|
568
|
+
<meta charset="UTF-8">
|
|
569
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
570
|
+
<title>Solid Pod Server</title>
|
|
571
|
+
<style>${styles}
|
|
572
|
+
/* landingPage local polish (#286) */
|
|
573
|
+
.container.landing { padding-top: 32px; }
|
|
574
|
+
.landing-header {
|
|
575
|
+
margin: -40px -40px 24px;
|
|
576
|
+
padding: 32px 40px 26px;
|
|
577
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
578
|
+
color: #fff;
|
|
579
|
+
border-radius: 12px 12px 0 0;
|
|
580
|
+
text-align: center;
|
|
581
|
+
}
|
|
582
|
+
.landing-header h1 { color: #fff; margin: 0 0 6px; font-size: 24px; }
|
|
583
|
+
.landing-header .subtitle { color: rgba(255,255,255,.85); margin: 0; font-size: 14px; }
|
|
584
|
+
.landing .signin-note {
|
|
585
|
+
margin-top: 18px;
|
|
586
|
+
padding: 14px 16px;
|
|
587
|
+
background: #f8fafc;
|
|
588
|
+
border: 1px solid #e2e8f0;
|
|
589
|
+
border-radius: 8px;
|
|
590
|
+
color: #475569;
|
|
591
|
+
font-size: 13px;
|
|
592
|
+
line-height: 1.55;
|
|
593
|
+
}
|
|
594
|
+
.landing .signin-note strong { color: #1e293b; }
|
|
595
|
+
.landing .signin-note a { color: #4f46e5; text-decoration: none; font-weight: 500; }
|
|
596
|
+
.landing .signin-note a:hover { text-decoration: underline; }
|
|
597
|
+
.landing .issuer {
|
|
598
|
+
margin-top: 18px;
|
|
599
|
+
text-align: center;
|
|
600
|
+
color: #94a3b8;
|
|
601
|
+
font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
602
|
+
word-break: break-all;
|
|
603
|
+
}
|
|
604
|
+
</style>
|
|
605
|
+
</head>
|
|
606
|
+
<body>
|
|
607
|
+
<div class="container landing">
|
|
608
|
+
<div class="landing-header">
|
|
609
|
+
<h1>Solid Pod Server</h1>
|
|
610
|
+
<p class="subtitle">Create an account, then sign in from any Solid app.</p>
|
|
611
|
+
</div>
|
|
612
|
+
|
|
613
|
+
<a href="/idp/register" class="btn btn-primary" style="text-decoration: none;">Create Account</a>
|
|
614
|
+
|
|
615
|
+
<div class="signin-note">
|
|
616
|
+
<strong>Already have an account?</strong> Sign in from a Solid app — for example, <a href="https://solid-apps.github.io/pilot/" target="_blank" rel="noopener">pilot</a> is a minimal console you can open right now. Point it at this server and click Sign In.
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
${issuer ? `<div class="issuer">Issuer: ${escapeHtml(issuer.replace(/\/$/, ''))}</div>` : ''}
|
|
620
|
+
</div>
|
|
621
|
+
</body>
|
|
622
|
+
</html>
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
|
|
553
626
|
/**
|
|
554
627
|
* Registration page HTML
|
|
555
628
|
*/
|
|
@@ -652,7 +725,9 @@ export function registerPage(uid = null, error = null, success = null, inviteOnl
|
|
|
652
725
|
</form>
|
|
653
726
|
|
|
654
727
|
<p style="text-align: center; margin-top: 24px; color: #666; font-size: 14px;">
|
|
655
|
-
|
|
728
|
+
${uid
|
|
729
|
+
? `Already have an account? <a href="/idp/interaction/${uid}" style="color: #0066cc;">Sign In</a>`
|
|
730
|
+
: `<a href="/idp" style="color: #0066cc;">Back to home</a>`}
|
|
656
731
|
</p>
|
|
657
732
|
</div>
|
|
658
733
|
|
package/src/server.js
CHANGED
|
@@ -436,7 +436,9 @@ export function createServer(options = {}) {
|
|
|
436
436
|
if (request.url === '/.pods' ||
|
|
437
437
|
request.url === '/.notifications' ||
|
|
438
438
|
request.method === 'OPTIONS' ||
|
|
439
|
+
request.url === '/idp' ||
|
|
439
440
|
request.url.startsWith('/idp/') ||
|
|
441
|
+
request.url.startsWith('/idp?') ||
|
|
440
442
|
request.url.startsWith('/.well-known/') ||
|
|
441
443
|
(nostrEnabled && request.url.startsWith(nostrPath)) ||
|
|
442
444
|
(gitEnabled && isGitRequest(request.url)) ||
|
package/test/idp.test.js
CHANGED
|
@@ -182,6 +182,60 @@ describe('Identity Provider', () => {
|
|
|
182
182
|
});
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
+
// Regression coverage for #286 — friendly /idp landing + /idp/auth guard.
|
|
186
|
+
describe('Landing page', () => {
|
|
187
|
+
it('GET /idp returns the landing HTML', async () => {
|
|
188
|
+
const res = await fetch(`${baseUrl}/idp`);
|
|
189
|
+
assert.strictEqual(res.status, 200);
|
|
190
|
+
assert.match(res.headers.get('content-type') || '', /text\/html/);
|
|
191
|
+
const body = await res.text();
|
|
192
|
+
assert.match(body, /Solid Pod Server/);
|
|
193
|
+
assert.match(body, /Create Account/);
|
|
194
|
+
assert.match(body, /href="\/idp\/register"/);
|
|
195
|
+
// Sign-in note names pilot as the example client (#288).
|
|
196
|
+
assert.match(body, /solid-apps\.github\.io\/pilot/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('GET /idp/auth without client_id redirects to /idp', async () => {
|
|
200
|
+
const res = await fetch(`${baseUrl}/idp/auth`, { redirect: 'manual' });
|
|
201
|
+
assert.strictEqual(res.status, 302);
|
|
202
|
+
assert.strictEqual(res.headers.get('location'), '/idp');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('HEAD /idp/auth without client_id also redirects to /idp', async () => {
|
|
206
|
+
// Fastify auto-creates HEAD handlers for GET routes; the guard must
|
|
207
|
+
// catch HEAD too so probing tools land on the friendly page rather
|
|
208
|
+
// than the raw OIDC error.
|
|
209
|
+
const res = await fetch(`${baseUrl}/idp/auth`, { method: 'HEAD', redirect: 'manual' });
|
|
210
|
+
assert.strictEqual(res.status, 302);
|
|
211
|
+
assert.strictEqual(res.headers.get('location'), '/idp');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('GET /idp/auth WITH client_id still reaches oidc-provider', async () => {
|
|
215
|
+
// Sanity: the guard must not block real OIDC requests. The provider
|
|
216
|
+
// may legitimately redirect (e.g. to /idp/interaction/:uid) for
|
|
217
|
+
// valid client/parameter combinations, so we test the precise
|
|
218
|
+
// contract — the guard's specific 302→/idp response — rather than
|
|
219
|
+
// "no redirect at all".
|
|
220
|
+
const res = await fetch(
|
|
221
|
+
`${baseUrl}/idp/auth?client_id=test&redirect_uri=http://localhost&response_type=code&scope=openid`,
|
|
222
|
+
{ redirect: 'manual' }
|
|
223
|
+
);
|
|
224
|
+
const location = res.headers.get('location');
|
|
225
|
+
assert.ok(
|
|
226
|
+
!(res.status === 302 && location === '/idp'),
|
|
227
|
+
`request should bypass the /idp guard, got ${res.status} → ${location}`
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('GET /idp/auth with empty client_id is a malformed OIDC request, not bare', async () => {
|
|
232
|
+
// Tightened guard (=== undefined) lets ?client_id= pass through to
|
|
233
|
+
// oidc-provider rather than redirecting to /idp.
|
|
234
|
+
const res = await fetch(`${baseUrl}/idp/auth?client_id=`, { redirect: 'manual' });
|
|
235
|
+
assert.notStrictEqual(res.headers.get('location'), '/idp');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
185
239
|
// Regression coverage for #284 — relaxed username regex + `..` rejection.
|
|
186
240
|
// Each register call below also exercises the .jsonld pod-creation flow
|
|
187
241
|
// from #283, since handleRegisterPost calls createPodStructure on success.
|