Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions documentation/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ In that case the body is expected to be an OIDC ID token.
Both Solid and standard OIDC tokens are supported.
In case of standard tokens, the value of the `sub` field will be used to match the assignee in the policies.

The values that are extracted from the OIDC token are expected to be IRIs.
In case the `sub` or `azp`, which is discussed below, values are not IRIs,
the server wil internally convert them by URL encoding the value, and prepending them with `http://example.com/id/`.
This means that your policies should reference the converted ID.
For example, if your `sub` value is `my id`, your policy needs to target `http://example.com/id/my%20id`.
This base URL will be updated in the future once we have settled on a fixed value.

#### Customizing OIDC verification

Several configuration options can be added to further restrict authentication when using OIDC tokens,
Expand Down
6 changes: 6 additions & 0 deletions packages/uma/config/credentials/verifiers/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
"@graph": [
{
"@id": "urn:uma:default:Verifier",
"@type": "IriVerifier",
"verifier": { "@id": "urn:uma:default:TypedVerifier" },
"baseUrl": "http://example.com/id/"
},
{
"@id": "urn:uma:default:TypedVerifier",
"@type": "TypedVerifier",
"verifiers": [
{
Expand Down
32 changes: 32 additions & 0 deletions packages/uma/src/credentials/verify/IriVerifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { joinUrl } from '@solid/community-server';
import { isIri } from '../../util/ConvertUtil';
import { CLIENTID, WEBID } from '../Claims';
import { ClaimSet } from '../ClaimSet';
import { Credential } from '../Credential';
import { Verifier } from './Verifier';

/**
* Converts the user ID and client ID values to IRIs in case they are not already IRIs.
*/
export class IriVerifier implements Verifier {
public constructor(
protected readonly verifier: Verifier,
protected readonly baseUrl: string,
) {}

public async verify(credential: Credential): Promise<ClaimSet> {
const claims = await this.verifier.verify(credential);
return {
...claims,
...typeof claims[WEBID] === 'string' ? { [WEBID]: this.toIri(claims[WEBID]) } : {},
...typeof claims[CLIENTID] === 'string' ? { [CLIENTID]: this.toIri(claims[CLIENTID]) } : {},
};
}

protected toIri(value: string): string {
if (isIri(value)) {
return value;
}
return joinUrl(this.baseUrl, encodeURIComponent(value));
}
}
1 change: 1 addition & 0 deletions packages/uma/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './credentials/verify/TypedVerifier';
export * from './credentials/verify/UnsecureVerifier';
export * from './credentials/verify/OidcVerifier';
export * from './credentials/verify/JwtVerifier';
export * from './credentials/verify/IriVerifier';

// Dialog
export * from './dialog/Input';
Expand Down
9 changes: 9 additions & 0 deletions packages/uma/src/util/ConvertUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export function isPrimitive(val: unknown): val is string | number | boolean {
return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean';
}

export const IRI_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\S*$/;

/**
* Uses a heuristic to estimate if a string is a valid IRI.
*/
export function isIri(input: string): boolean {
return IRI_REGEX.test(input);
}

/**
* Write an N3 store to a string (in turtle format)
*/
Expand Down
50 changes: 50 additions & 0 deletions packages/uma/test/unit/credentials/verify/IriVerifier.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Mocked } from 'vitest';
import { CLIENTID, WEBID } from '../../../../src/credentials/Claims';
import { Credential } from '../../../../src/credentials/Credential';
import { IriVerifier } from '../../../../src/credentials/verify/IriVerifier';
import { Verifier } from '../../../../src/credentials/verify/Verifier';

describe('IriVerifier', (): void => {
const credential: Credential = { token: 'token', format: 'format' };
const baseUrl = 'http://example.com/id/';
let source: Mocked<Verifier>;
let verifier: IriVerifier;

beforeEach(async(): Promise<void> => {
source = {
verify: vi.fn(),
};

verifier = new IriVerifier(source, baseUrl);
});

it('keeps the original user and client ID if they already are IRIs', async(): Promise<void> => {
source.verify.mockResolvedValueOnce({
[WEBID]: 'http://example.org/webId',
[CLIENTID]: 'http://example.org/clientId',
fruit: 'apple',
});
await expect(verifier.verify(credential)).resolves.toEqual({
[WEBID]: 'http://example.org/webId',
[CLIENTID]: 'http://example.org/clientId',
fruit: 'apple',
});
expect(source.verify).toHaveBeenCalledTimes(1);
expect(source.verify).toHaveBeenLastCalledWith(credential);
});

it('changes the user and client ID to IRIs when required.', async(): Promise<void> => {
source.verify.mockResolvedValueOnce({
[WEBID]: 'webId',
[CLIENTID]: 'clientId',
fruit: 'http://example.org/apple',
});
await expect(verifier.verify(credential)).resolves.toEqual({
[WEBID]: 'http://example.com/id/webId',
[CLIENTID]: 'http://example.com/id/clientId',
fruit: 'http://example.org/apple',
});
expect(source.verify).toHaveBeenCalledTimes(1);
expect(source.verify).toHaveBeenLastCalledWith(credential);
});
});
10 changes: 9 additions & 1 deletion packages/uma/test/unit/util/ConvertUtil.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPrimitive, formToJson, jsonToForm } from '../../../src/util/ConvertUtil';
import { isPrimitive, formToJson, jsonToForm, isIri } from '../../../src/util/ConvertUtil';

describe('ConvertUtil', (): void => {
describe('#formToJson', (): void => {
Expand Down Expand Up @@ -33,4 +33,12 @@ describe('ConvertUtil', (): void => {
expect(isPrimitive({})).toBe(false);
});
});

describe('#isIri', (): void => {
it('estimates if the string is a valid IRI.', async(): Promise<void> => {
expect(isIri('apple')).toBe(false);
expect(isIri('urn:apple')).toBe(true);
expect(isIri('urn:ap ple')).toBe(false);
});
});
});
6 changes: 3 additions & 3 deletions test/integration/Oidc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('A server supporting OIDC tokens', (): void => {
odrl:permission ex:permissionStandard .

ex:permissionStandard a odrl:Permission ;
odrl:assignee <${sub}> ;
odrl:assignee <http://example.com/id/${sub}> ;
odrl:assigner <${webId}>;
odrl:action odrl:read , odrl:create , odrl:modify ;
odrl:target <http://localhost:${cssPort}/alice/> .`;
Expand Down Expand Up @@ -154,7 +154,7 @@ describe('A server supporting OIDC tokens', (): void => {
odrl:permission ex:permissionStandardClient .

ex:permissionStandardClient a odrl:Permission ;
odrl:assignee <${sub}> ;
odrl:assignee <http://example.com/id/${sub}> ;
odrl:assigner <${webId}> ;
odrl:action odrl:read , odrl:create , odrl:modify ;
odrl:target <http://localhost:${cssPort}/alice/> ;
Expand All @@ -163,7 +163,7 @@ describe('A server supporting OIDC tokens', (): void => {
ex:constraintStandardClient
odrl:leftOperand odrl:purpose ;
odrl:operator odrl:eq ;
odrl:rightOperand <${client}> .`;
odrl:rightOperand <http://example.com/id/${client}> .`;

it('can set up the policy.', async(): Promise<void> => {
const response = await fetch(policyEndpoint, {
Expand Down