Skip to content

Commit

Permalink
feat: Add support for client_id WebIDs
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh committed Jul 23, 2021
1 parent 60ebf54 commit 3bb7a32
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 268 deletions.
Expand Up @@ -4,12 +4,13 @@
{
"comment": "An adapter is responsible for storing all interaction metadata.",
"@id": "urn:solid-server:default:IdpAdapterFactory",
"@type": "WrappedFetchAdapterFactory",
"@type": "WebIdAdapterFactory",
"source": {
"@type": "ExpiringAdapterFactory",
"args_storageName": "/idp/oidc",
"args_storage": { "@id": "urn:solid-server:default:ExpiringIdpStorage" }
}
},
"converter": { "@id": "urn:solid-server:default:RepresentationConverter" }
}
]
}
2 changes: 1 addition & 1 deletion config/identity/handler/default.json
@@ -1,7 +1,7 @@
{
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^1.0.0/components/context.jsonld",
"import": [
"files-scs:config/identity/handler/adapter-factory/wrapped-fetch.json",
"files-scs:config/identity/handler/adapter-factory/webid.json",
"files-scs:config/identity/handler/interaction/handler.json",
"files-scs:config/identity/handler/key-value/resource-store.json",
"files-scs:config/identity/handler/provider-factory/identity.json"
Expand Down
1 change: 1 addition & 0 deletions config/identity/handler/provider-factory/identity.json
Expand Up @@ -37,6 +37,7 @@
"AccessToken": "jwt"
},
"scopes": [ "openid", "profile", "offline_access" ],
"subjectTypes": [ "public", "pairwise" ],
"ttl": {
"AccessToken": 3600,
"AuthorizationCode": 600,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -108,6 +108,7 @@
"bcrypt": "^5.0.1",
"componentsjs": "^4.3.0",
"cors": "^2.8.5",
"cross-fetch": "^3.1.4",
"ejs": "^3.1.6",
"end-of-stream": "^1.4.4",
"escape-string-regexp": "^4.0.0",
Expand Down Expand Up @@ -150,7 +151,6 @@
"@typescript-eslint/parser": "^4.28.1",
"cheerio": "^1.0.0-rc.10",
"componentsjs-generator": "^2.4.0",
"cross-fetch": "^3.1.4",
"eslint": "^7.29.0",
"eslint-config-es": "^3.20.3",
"eslint-import-resolver-typescript": "^2.4.0",
Expand Down
146 changes: 146 additions & 0 deletions src/identity/storage/WebIdAdapterFactory.ts
@@ -0,0 +1,146 @@
import type { Response } from 'cross-fetch';
import { fetch } from 'cross-fetch';
import { Store } from 'n3';
import type { Adapter, AdapterPayload } from 'oidc-provider';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import { getLoggerFor } from '../../logging/LogUtil';
import type { RepresentationConverter } from '../../storage/conversion/RepresentationConverter';
import { INTERNAL_QUADS } from '../../util/ContentTypes';
import { createErrorMessage } from '../../util/errors/ErrorUtil';
import type { AdapterFactory } from './AdapterFactory';

/* eslint-disable @typescript-eslint/naming-convention */

/**
* This {@link Adapter} redirects the `find` call to its source adapter.
* In case no client data was found in the source for the given WebId,
* this class will do an HTTP GET request to that WebId.
* If a valid `solid:oidcRegistration` triple is found there,
* that data will be returned instead.
*/
export class WebIdAdapter implements Adapter {
protected readonly logger = getLoggerFor(this);

private readonly name: string;
private readonly source: Adapter;
private readonly converter: RepresentationConverter;

public constructor(name: string, source: Adapter, converter: RepresentationConverter) {
this.name = name;
this.source = source;
this.converter = converter;
}

public async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise<void> {
return this.source.upsert(id, payload, expiresIn);
}

public async find(id: string): Promise<AdapterPayload | void> {
let payload = await this.source.find(id);

// No payload is stored for the given Client ID.
// Try to see if valid client metadata is found at the given Client ID.
// The oidc-provider library will check if the redirect_uri matches an entry in the list of redirect_uris,
// so no extra checks are needed from our side.
if (!payload && this.name === 'Client' && /^https?:\/\/.+/u.test(id)) {
this.logger.debug(`Looking for payload data at ${id}`);
// All checks based on https://solid.github.io/authentication-panel/solid-oidc/#clientids-webid
if (!/^https:|^http:\/\/localhost(?::\d+)?(?:\/|$)/u.test(id)) {
throw new Error(`SSL is required for client_id authentication unless working locally.`);
}
const response = await fetch(id);
if (response.status !== 200) {
throw new Error(`Unable to access data at ${id}: ${await response.text()}`);
}
const data = await response.text();
let json: any | undefined;
try {
json = JSON.parse(data);
// We can only parse as simple JSON if the @context is correct
if (json['@context'] !== 'https://www.w3.org/ns/solid/oidc-context.jsonld') {
throw new Error('Invalid context');
}
} catch (error: unknown) {
json = undefined;
this.logger.debug(`Found unexpected client WebID for ${id}: ${createErrorMessage(error)}`);
}

if (json) {
// Need to make sure the document is about the id
if (json.client_id !== id) {
throw new Error('The client registration `client_id` field must match the client WebID');
}
payload = json;
} else {
// Since the WebID does not match the default JSON-LD we try to interpret it as RDF
payload = await this.parseRdfWebId(data, id, response);
}

// `token_endpoint_auth_method: 'none'` prevents oidc-provider from requiring a client_secret
payload = { ...payload, token_endpoint_auth_method: 'none' };
}

// Will also be returned if no valid client data was found above
return payload;
}

private async parseRdfWebId(data: string, id: string, response: Response): Promise<AdapterPayload> {
const contentType = response.headers.get('content-type');
if (!contentType) {
throw new Error(`No content-type received for client WebID ${id}`);
}

// Try to convert to quads
const representation = new BasicRepresentation(data, contentType);
const preferences = { type: { [INTERNAL_QUADS]: 1 }};
const converted = await this.converter.handleSafe({ representation, identifier: { path: id }, preferences });
const quads = new Store();
const importer = quads.import(converted.data);
await new Promise((resolve, reject): void => {
importer.on('end', resolve);
importer.on('error', reject);
});

// Find the valid redirect uris
const match = quads.getObjects(id, 'http://www.w3.org/ns/solid/oidc#redirect_uris', null);

return {
client_id: id,
redirect_uris: match.map((node): string => node.value),
};
}

public async findByUserCode(userCode: string): Promise<AdapterPayload | void> {
return this.source.findByUserCode(userCode);
}

public async findByUid(uid: string): Promise<AdapterPayload | void> {
return this.source.findByUid(uid);
}

public async destroy(id: string): Promise<void> {
return this.source.destroy(id);
}

public async revokeByGrantId(grantId: string): Promise<void> {
return this.source.revokeByGrantId(grantId);
}

public async consume(id: string): Promise<void> {
return this.source.consume(id);
}
}

export class WebIdAdapterFactory implements AdapterFactory {
private readonly source: AdapterFactory;
private readonly converter: RepresentationConverter;

public constructor(source: AdapterFactory, converter: RepresentationConverter) {
this.source = source;
this.converter = converter;
}

public createStorageAdapter(name: string): Adapter {
return new WebIdAdapter(name, this.source.createStorageAdapter(name), this.converter);
}
}
121 changes: 0 additions & 121 deletions src/identity/storage/WrappedFetchAdapterFactory.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.ts
Expand Up @@ -54,7 +54,7 @@ export * from './identity/ownership/TokenOwnershipValidator';
// Identity/Storage
export * from './identity/storage/AdapterFactory';
export * from './identity/storage/ExpiringAdapterFactory';
export * from './identity/storage/WrappedFetchAdapterFactory';
export * from './identity/storage/WebIdAdapterFactory';

// Identity
export * from './identity/IdentityProviderHttpHandler';
Expand Down

0 comments on commit 3bb7a32

Please sign in to comment.