Serve a Solid-OIDC Client ID Document at /app for the OIDC adapter fetch, keeping the app actor for SAI

This commit is contained in:
Sylvain Duchesne
2026-10-12 11:01:34 +02:00
parent 84a0882d32
commit 3aa9131a01
2 changed files with 51 additions and 1 deletions
+44
View File
@@ -2,6 +2,21 @@ const urlJoin = require('url-join');
const { AppService } = require('@activitypods/app');
const CONFIG = require('../config/config');
// `GET /app` has two personalities, discriminated by the request's `JsonLdContext` header.
//
// 1. Solid-OIDC Client ID Document. The pod provider's OIDC adapter (RedisAdapter.find)
// resolves an unknown `client_id` that starts with `http` by fetching it, sending
// `Accept: application/ld+json` and `JsonLdContext: https://www.w3.org/ns/solid/oidc-context.jsonld`.
// The pod provider runs the login with `client_id = <HOME_URL>/app`, so this same URL must,
// for that fetch, return a document whose `@context` literally includes the Solid-OIDC
// context, whose `client_id` equals the fetched URI exactly, and which carries `redirect_uris`.
// Anything else fails `invalid_client_metadata / client_id is mandatory property`.
//
// 2. SAI app description (the actor). The pod provider's auth-agent fetches this URL
// WITHOUT the `JsonLdContext` header (`ldp.remote.getNetwork`) to install/describe the app,
// and expects the app actor document served today (`type: [Application, interop:Application]`).
//
// The `JsonLdContext` header is the only discriminator: same path, two representations.
module.exports = {
mixins: [AppService],
settings: {
@@ -47,5 +62,34 @@ module.exports = {
optional: []
},
queueServiceUrl: CONFIG.QUEUE_SERVICE_URL
},
actions: {
// Overrides the mixin's `get` (which returns `this.appActor` for every request) to
// answer the Solid-OIDC Client ID Document fetch as well. `this.appActor` is set by the
// mixin's `started()`, i.e. long before any request is served.
get: {
handler(ctx) {
// moleculer-web exposes the raw request only when the route alias sets
// passReqResToParams (see services/core/core.service.js). Internal calls
// (ctx.call('app.get')) have no `$req`, so they always get the actor.
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) {
return {
'@context': ['https://www.w3.org/ns/solid/oidc-context.jsonld'],
client_id: urlJoin(CONFIG.HOME_URL, 'app'),
redirect_uris: [urlJoin(CONFIG.FRONT_URL, 'login')],
client_name: CONFIG.APP_NAME,
client_uri: CONFIG.FRONT_URL
};
}
return this.appActor;
}
}
}
};