diff --git a/backend/services/app.service.js b/backend/services/app.service.js index e66bb7d..655a11b 100644 --- a/backend/services/app.service.js +++ b/backend/services/app.service.js @@ -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 = /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; + } + } } }; diff --git a/backend/services/core/core.service.js b/backend/services/core/core.service.js index 2bffd84..86630aa 100644 --- a/backend/services/core/core.service.js +++ b/backend/services/core/core.service.js @@ -24,7 +24,13 @@ module.exports = { { path: '/app', aliases: { - 'GET /': 'app.get' + 'GET /': { + action: 'app.get', + // `app.get` must see the JsonLdContext request header to answer the Solid-OIDC + // Client ID Document fetch; moleculer-web only exposes the raw request via + // ctx.params.$req when the alias opts in. + passReqResToParams: true + } }, mappingPolicy: 'restrict', authentication: false,