Close the login loop end-to-end: client_id is the app actor uri, actor path serves the OIDC client document via a boot-safe service, startup heals the access-need-group link, stack aligned on pod provider v2.3.12

This commit is contained in:
Sylvain Duchesne
2026-10-12 13:12:38 +02:00
parent 18fc71ff36
commit a61c8704c9
13 changed files with 882 additions and 827 deletions
+50
View File
@@ -0,0 +1,50 @@
const urlJoin = require('url-join');
const CONFIG = require('../config/config');
// Serves the app actor at its canonical uri (`/as/actor/app`) with a Solid-OIDC dual
// personality, discriminated by the request's `JsonLdContext` header — same contract as
// the `/app` route (see app.service.js for the full story), but implemented as a
// standalone service with no dependency on the `app` service.
//
// WHY A SEPARATE SERVICE: the Solid-OIDC client_id IS the actor uri (the pod provider's
// auth-agent only returns the `Link: rel="interop:registeredAgent"` header when the
// requesting app's identity — the token's azp, i.e. this client_id — matches the
// registration's `registeredAgent`, which the pod stores under the actor uri). The
// adapter therefore fetches THIS path as the client metadata document. Routing the path
// to `app.get` deadlocked boot: `@activitypods/app`'s `awaitCreateComplete` polls the
// actor over HTTP while the `app` service is still starting, and an unregistered action
// 503s the poll it waits on. This service has no `started()` work and no dependencies,
// so it is routable long before the `app` service exists.
module.exports = {
name: 'client-id-doc',
actions: {
get: {
async handler(ctx) {
const req = ctx.params && ctx.params.$req;
const headers = (req && req.headers) || {};
const jsonLdContextKey = Object.keys(headers).find(key => key.toLowerCase() === 'jsonldcontext');
const isOidcClientIdDocument =
jsonLdContextKey !== undefined && String(headers[jsonLdContextKey]).includes('solid/oidc-context.jsonld');
if (isOidcClientIdDocument) {
// The client_id must equal the fetched URI exactly. `req.url` is rewritten to
// the alias remainder by moleculer-web; the raw path survives in `originalUrl`.
const requestPath = String((req && (req.originalUrl || req.url)) || '/as/actor/app').split('?')[0];
return {
'@context': ['https://www.w3.org/ns/solid/oidc-context.jsonld'],
client_id: urlJoin(CONFIG.HOME_URL, requestPath),
redirect_uris: [urlJoin(CONFIG.FRONT_URL, 'login')],
client_name: CONFIG.APP_NAME,
client_uri: CONFIG.FRONT_URL
};
}
return await ctx.call('ldp.resource.get', {
resourceUri: urlJoin(CONFIG.HOME_URL, 'as/actor/app'),
accept: 'application/ld+json',
webId: 'system'
});
}
}
}
};