Skip to content

Commit c730281

Browse files
committed
feat: Convert non-IRI claims to IRIs
1 parent 3846a28 commit c730281

File tree

9 files changed

+118
-5
lines changed

9 files changed

+118
-5
lines changed

documentation/getting-started.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,13 @@ In that case the body is expected to be an OIDC ID token.
236236
Both Solid and standard OIDC tokens are supported.
237237
In case of standard tokens, the value of the `sub` field will be used to match the assignee in the policies.
238238

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

241248
Several configuration options can be added to further restrict authentication when using OIDC tokens,

packages/uma/config/credentials/verifiers/default.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
"@graph": [
66
{
77
"@id": "urn:uma:default:Verifier",
8+
"@type": "IriVerifier",
9+
"verifier": { "@id": "urn:uma:default:TypedVerifier" },
10+
"baseUrl": "http://example.com/id/"
11+
},
12+
{
13+
"@id": "urn:uma:default:TypedVerifier",
814
"@type": "TypedVerifier",
915
"verifiers": [
1016
{
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { joinUrl } from '@solid/community-server';
2+
import { isIri } from '../../util/ConvertUtil';
3+
import { CLIENTID, WEBID } from '../Claims';
4+
import { ClaimSet } from '../ClaimSet';
5+
import { Credential } from '../Credential';
6+
import { Verifier } from './Verifier';
7+
8+
/**
9+
* Converts the user ID and client ID values to IRIs in case they are not already IRIs.
10+
*/
11+
export class IriVerifier implements Verifier {
12+
public constructor(
13+
protected readonly verifier: Verifier,
14+
protected readonly baseUrl: string,
15+
) {}
16+
17+
public async verify(credential: Credential): Promise<ClaimSet> {
18+
const claims = await this.verifier.verify(credential);
19+
return {
20+
...claims,
21+
...typeof claims[WEBID] === 'string' ? { [WEBID]: this.toIri(claims[WEBID]) } : {},
22+
...typeof claims[CLIENTID] === 'string' ? { [CLIENTID]: this.toIri(claims[CLIENTID]) } : {},
23+
};
24+
}
25+
26+
protected toIri(value: string): string {
27+
if (isIri(value)) {
28+
return value;
29+
}
30+
return joinUrl(this.baseUrl, encodeURIComponent(value));
31+
}
32+
}

packages/uma/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './credentials/verify/TypedVerifier';
1515
export * from './credentials/verify/UnsecureVerifier';
1616
export * from './credentials/verify/OidcVerifier';
1717
export * from './credentials/verify/JwtVerifier';
18+
export * from './credentials/verify/IriVerifier';
1819

1920
// Dialog
2021
export * from './dialog/Input';

packages/uma/src/util/ConvertUtil.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ export function isPrimitive(val: unknown): val is string | number | boolean {
3636
return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean';
3737
}
3838

39+
export const IRI_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:\S*$/;
40+
41+
/**
42+
* Uses a heuristic to estimate if a string is a valid IRI.
43+
*/
44+
export function isIri(input: string): boolean {
45+
return IRI_REGEX.test(input);
46+
}
47+
3948
/**
4049
* Write an N3 store to a string (in turtle format)
4150
*/
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Mocked } from 'vitest';
2+
import { CLIENTID, WEBID } from '../../../../src/credentials/Claims';
3+
import { Credential } from '../../../../src/credentials/Credential';
4+
import { IriVerifier } from '../../../../src/credentials/verify/IriVerifier';
5+
import { Verifier } from '../../../../src/credentials/verify/Verifier';
6+
7+
describe('IriVerifier', (): void => {
8+
const credential: Credential = { token: 'token', format: 'format' };
9+
const baseUrl = 'http://example.com/id/';
10+
let source: Mocked<Verifier>;
11+
let verifier: IriVerifier;
12+
13+
beforeEach(async(): Promise<void> => {
14+
source = {
15+
verify: vi.fn(),
16+
};
17+
18+
verifier = new IriVerifier(source, baseUrl);
19+
});
20+
21+
it('keeps the original user and client ID if they already are IRIs', async(): Promise<void> => {
22+
source.verify.mockResolvedValueOnce({
23+
[WEBID]: 'http://example.org/webId',
24+
[CLIENTID]: 'http://example.org/clientId',
25+
fruit: 'apple',
26+
});
27+
await expect(verifier.verify(credential)).resolves.toEqual({
28+
[WEBID]: 'http://example.org/webId',
29+
[CLIENTID]: 'http://example.org/clientId',
30+
fruit: 'apple',
31+
});
32+
expect(source.verify).toHaveBeenCalledTimes(1);
33+
expect(source.verify).toHaveBeenLastCalledWith(credential);
34+
});
35+
36+
it('changes the user and client ID to IRIs when required.', async(): Promise<void> => {
37+
source.verify.mockResolvedValueOnce({
38+
[WEBID]: 'webId',
39+
[CLIENTID]: 'clientId',
40+
fruit: 'http://example.org/apple',
41+
});
42+
await expect(verifier.verify(credential)).resolves.toEqual({
43+
[WEBID]: 'http://example.com/id/webId',
44+
[CLIENTID]: 'http://example.com/id/clientId',
45+
fruit: 'http://example.org/apple',
46+
});
47+
expect(source.verify).toHaveBeenCalledTimes(1);
48+
expect(source.verify).toHaveBeenLastCalledWith(credential);
49+
});
50+
});

packages/uma/test/unit/util/ConvertUtil.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isPrimitive, formToJson, jsonToForm } from '../../../src/util/ConvertUtil';
1+
import { isPrimitive, formToJson, jsonToForm, isIri } from '../../../src/util/ConvertUtil';
22

33
describe('ConvertUtil', (): void => {
44
describe('#formToJson', (): void => {
@@ -33,4 +33,12 @@ describe('ConvertUtil', (): void => {
3333
expect(isPrimitive({})).toBe(false);
3434
});
3535
});
36+
37+
describe('#isIri', (): void => {
38+
it('estimates if the string is a valid IRI.', async(): Promise<void> => {
39+
expect(isIri('apple')).toBe(false);
40+
expect(isIri('urn:apple')).toBe(true);
41+
expect(isIri('urn:ap ple')).toBe(false);
42+
});
43+
});
3644
});

test/integration/Base.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ describe('A server setup', (): void => {
176176

177177
const policyResponse = await fetch(url, {
178178
method: 'POST',
179-
headers: { authorization: owner, 'content-type': 'text/turtle' },
179+
headers: { authorization: `WebID ${encodeURIComponent(owner)}`, 'content-type': 'text/turtle' },
180180
body: policy,
181181
});
182182
expect(policyResponse.status).toBe(201);

test/integration/Oidc.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('A server supporting OIDC tokens', (): void => {
9393
odrl:permission ex:permissionStandard .
9494
9595
ex:permissionStandard a odrl:Permission ;
96-
odrl:assignee <${sub}> ;
96+
odrl:assignee <http://example.com/id/${sub}> ;
9797
odrl:assigner <${webId}>;
9898
odrl:action odrl:read , odrl:create , odrl:modify ;
9999
odrl:target <http://localhost:${cssPort}/alice/> .`;
@@ -154,7 +154,7 @@ describe('A server supporting OIDC tokens', (): void => {
154154
odrl:permission ex:permissionStandardClient .
155155
156156
ex:permissionStandardClient a odrl:Permission ;
157-
odrl:assignee <${sub}> ;
157+
odrl:assignee <http://example.com/id/${sub}> ;
158158
odrl:assigner <${webId}> ;
159159
odrl:action odrl:read , odrl:create , odrl:modify ;
160160
odrl:target <http://localhost:${cssPort}/alice/> ;
@@ -163,7 +163,7 @@ describe('A server supporting OIDC tokens', (): void => {
163163
ex:constraintStandardClient
164164
odrl:leftOperand odrl:purpose ;
165165
odrl:operator odrl:eq ;
166-
odrl:rightOperand <${client}> .`;
166+
odrl:rightOperand <http://example.com/id/${client}> .`;
167167

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

0 commit comments

Comments
 (0)