diff --git a/documentation/getting-started.md b/documentation/getting-started.md
index 3356e800..f61ec12b 100644
--- a/documentation/getting-started.md
+++ b/documentation/getting-started.md
@@ -31,7 +31,7 @@ so some information might change depending on which version and branch you're us
* [Authenticating as Resource Owner](#authenticating-as-resource-owner)
* [Authenticating as Resource Server](#authenticating-as-resource-server)
+ [Requesting client credentials](#requesting-client-credentials)
- + [Sending the credentials to the RS (CSS specific)](#sending-the-credentials-to-the-rs--css-specific-)
+ + [Sending the credentials to the RS (CSS specific)](#sending-the-credentials-to-the-rs-css-specific)
+ [Requesting a PAT as RS](#requesting-a-pat-as-rs)
* [Resource registration](#resource-registration)
+ [About identifiers](#about-identifiers)
@@ -49,7 +49,10 @@ so some information might change depending on which version and branch you're us
- [Include `sub` claim in access token](#include-sub-claim-in-access-token)
+ [Use token](#use-token)
* [Policies](#policies)
- + [Client application identification](#client-application-identification)
+ + [Additional constraints](#additional-constraints)
+ - [Client application identification](#client-application-identification)
+ - [Purpose](#purpose)
+ - [Verifiable Credentials](#verifiable-credentials)
* [Adding or changing policies](#adding-or-changing-policies)
* [Policy backups](#policy-backups)
* [Data aggregation](#data-aggregation)
@@ -363,9 +366,47 @@ This base URL will be updated in the future once we have settled on a fixed valu
##### Additional claims
Besides claims that identify the user and client, additional claims can also be provided containing additional information.
-Currently, the only additional claim that is supported is the `purpose` claim.
-This can be provided with a `claim_token_format` of `http://www.w3.org/ns/odrl/2/purpose`,
-and with the `claim_token` being the IRI of the purpose.
+These claims can be used to add extra constraints to ODRL policies.
+
+The purpose claim can be provided with a `claim_token_format` of `http://www.w3.org/ns/odrl/2/purpose`,
+and with the `claim_token` being the IRI of the purpose,
+e.g., `"https://w3id.org/dpv#ScientificResearch"`.
+
+Verifiable Credentials (VCs) can also be provided as additional claims using the token format `urn:solidlab:uma:credentials:jwt:vc`.
+The `claim_token` should be a signed JWT containing a `vc` claim with the credential data.
+For example, a decoded payload could be:
+```json
+{
+ "exp": 1784989353,
+ "nbf": 1784902953,
+ "jti": "urn:uuid:fecdb1de-d2d8-40a1-90ff-0e3cc8bfa3e9",
+ "iss": "http://localhost:28080/alice",
+ "vc": {
+ "type": [
+ "VerifiableCredential",
+ "http://localhost:28080/UserHcpRelationVC"
+ ],
+ "issuer": "http://localhost:28080/alice",
+ "issuanceDate": "2026-07-24T14:22:33.924Z",
+ "expirationDate": "2026-07-25T14:22:33.924Z",
+ "credentialSubject": {
+ "principal": "alice",
+ "rs:HCPs": {
+ "@id": "ex:users/0",
+ "rs:assignedPatientId": "https://ex.example.com/users/123"
+ },
+ "@id": "https://rs.example.com/named-graphs#qr-data",
+ "@context": {
+ "rs": "https://rs.example.com/vocab#",
+ "ex": "https://ex.example.com/"
+ }
+ },
+ "@context": [
+ "https://www.w3.org/ns/credentials/v1"
+ ]
+ }
+}
+```
#### Customizing OIDC verification
@@ -481,15 +522,21 @@ ex:permission a odrl:Permission ;
```
This policy says that the above WebID has access to the `create` scope on ``.
-### Client application identification
+### Additional constraints
+
+Beyond specifying which user and resource a permission applies to,
+policies can also include constraints that add further conditions.
+Constraints use the `odrl:constraint` predicate and follow the general pattern of
+a `odrl:leftOperand`, `odrl:operator`, and `odrl:rightOperand`,
+where the left operand identifies what to check, the operator defines how to compare,
+and the right operand provides the expected value.
-It is possible to create policies that restrict access based on the client application being used.
-This can only be done when using an OIDC ID token for authentication.
-The `azp` claim of the token will be used.
+#### Client application identification
-To restrict a policy to a certain client application,
-a constraint needs to be added to the policy.
-For this, we use the odrl:deliveryChannel left operand.
+It is possible to restrict access based on the client application being used.
+This can only be done when using an OIDC ID token for authentication,
+and uses the `azp` or `client_id` claim of the token.
+The left operand `odrl:deliveryChannel` is used to refer to the client application.
To restrict a policy to only permit access when using the application `http://example.com/client`,
the policy should look as follows:
@@ -510,6 +557,75 @@ ex:constraint odrl:leftOperand odrl:deliveryChannel ;
odrl:rightOperand .
```
+#### Purpose
+
+It is possible to restrict access based on the purpose for which the resource is being accessed.
+This requires that the client provides a purpose claim during the token exchange,
+as described in the [Additional claims](#additional-claims) section above.
+The left operand `odrl:purpose` is used to refer to the provided purpose.
+
+To restrict a policy to only permit access for the purpose `https://w3id.org/dpv#ScientificResearch`:
+```ttl
+@prefix ex: .
+@prefix odrl: .
+
+ex:usagePolicy a odrl:Agreement ;
+ odrl:uid ex:usagePolicy ;
+ odrl:permission ex:permission .
+ex:permission a odrl:Permission ;
+ odrl:action odrl:read ;
+ odrl:target ;
+ odrl:assignee ;
+ odrl:constraint ex:constraint .
+ex:constraint odrl:leftOperand odrl:purpose ;
+ odrl:operator odrl:eq ;
+ odrl:rightOperand .
+```
+
+#### Verifiable Credentials
+
+Policies can also restrict access based on attributes contained in a Verifiable Credential.
+This requires that the client provides a VC claim during the token exchange,
+as described in the [Additional claims](#additional-claims) section above.
+
+VC constraints follow the [ODRL VC profile](https://gitlab.com/gaia-x/lab/policy-reasoning/odrl-vc-profile)
+and use a slightly different vocabulary.
+The predicate `ovc:constraint` is used instead of `odrl:constraint`,
+and `ovc:leftOperand` instead of `odrl:leftOperand`,
+where the left operand value is a JSONPath expression evaluated against the credential data.
+The `ovc:rightOperand` still works the same as in standard ODRL constraints.
+
+To restrict access to users whose VC contains
+`rs:assignedPatientId: "https://ex.example.com/users/123"`
+inside the `rs:HCPs` object of the credential subject:
+```ttl
+@prefix ex: .
+@prefix odrl: .
+@prefix ovc: .
+
+ex:usagePolicy a odrl:Agreement ;
+ odrl:uid ex:usagePolicy ;
+ odrl:permission ex:permission .
+ex:permission a odrl:Permission ;
+ odrl:action odrl:read ;
+ odrl:target ;
+ ovc:constraint ex:constraint .
+ex:constraint ovc:leftOperand "$.credentialSubject['rs:HCPs']['rs:assignedPatientId']" ;
+ odrl:operator odrl:eq ;
+ odrl:rightOperand "https://ex.example.com/users/123" .
+```
+
+Optionally, you can also require the credential to be of a specific type by adding
+the `ovc:credentialSubjectType` property.
+This requires the `type` field in the credential to contain the specified value:
+```ttl
+ex:constraint ovc:leftOperand "$.credentialSubject['rs:HCPs']['rs:assignedPatientId']" ;
+ odrl:operator odrl:eq ;
+ odrl:rightOperand "https://ex.example.com/users/123" ;
+ ovc:credentialSubjectType "http://localhost:28080/UserHcpRelationVC" .
+```
+
+
## Adding or changing policies
For more details, see the [policy management API documentation](policy-management.md).
diff --git a/packages/css/src/index.ts b/packages/css/src/index.ts
index 1a67b1d6..2e166076 100644
--- a/packages/css/src/index.ts
+++ b/packages/css/src/index.ts
@@ -28,6 +28,5 @@ export * from './util/fetch/Fetcher';
export * from './util/fetch/BaseFetcher';
export * from './util/fetch/PausableFetcher';
export * from './util/fetch/RetryingFetcher';
-export * from './util/fetch/SignedFetcher';
export * from './util/fetch/StatusDependant';
export * from './util/fetch/StatusDependantServerConfigurator';
diff --git a/packages/css/src/util/fetch/SignedFetcher.ts b/packages/css/src/util/fetch/SignedFetcher.ts
deleted file mode 100644
index 5a14f4cc..00000000
--- a/packages/css/src/util/fetch/SignedFetcher.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { InternalServerError, type JwkGenerator } from '@solid/community-server';
-import { getLoggerFor } from 'global-logger-factory';
-import { httpbis, type SigningKey } from 'http-message-signatures';
-import { BufferSource } from 'node:stream/web';
-import type { Fetcher, FetchParams } from './Fetcher';
-
-const algMap = {
- 'Ed25519': { name: 'Ed25519' },
- 'ES256': { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' },
- 'ES384': { name: 'ECDSA', namedCurve: 'P-384', hash: 'SHA-384' },
- 'ES512': { name: 'ECDSA', namedCurve: 'P-512', hash: 'SHA-512' },
- 'HS256': { name: 'HMAC', hash: 'SHA-256' },
- 'HS384': { name: 'HMAC', hash: 'SHA-384' },
- 'HS512': { name: 'HMAC', hash: 'SHA-512' },
- 'PS256': { name: 'RSASSA-PSS', hash: 'SHA-256' },
- 'PS384': { name: 'RSASSA-PSS', hash: 'SHA-384' },
- 'PS512': { name: 'RSASSA-PSS', hash: 'SHA-512' },
- 'RS256': { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
- 'RS384': { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-384' },
- 'RS512': { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-512' },
-} as const;
-
-/**
- * A {@link Fetcher} wrapper that signes requests.
- */
-export class SignedFetcher implements Fetcher {
- protected readonly logger = getLoggerFor(this);
-
- constructor(
- protected fetcher: Fetcher,
- protected baseUrl: string,
- protected keyGen: JwkGenerator,
- ) {}
-
- public async fetch(...[ input, init ]: FetchParams): Promise {
- const jwk = await this.keyGen.getPrivateKey();
-
- const { alg, kid } = jwk;
- if (alg === 'EdDSA') throw new InternalServerError('EdDSA signing is not supported');
- if (alg === 'ML-DSA-44') throw new InternalServerError('ML-DSA-44 signing is not supported');
- if (alg === 'ML-DSA-65') throw new InternalServerError('ML-DSA-65 signing is not supported');
- if (alg === 'ML-DSA-87') throw new InternalServerError('ML-DSA-87 signing is not supported');
-
- const key: SigningKey = {
- id: kid,
- alg: alg,
- async sign(data: BufferSource) {
- const params = algMap[alg];
- const key = await crypto.subtle.importKey('jwk', jwk, params, false, ['sign']);
- return Buffer.from(await crypto.subtle.sign(params, key, data));
- },
- };
-
- const url = input instanceof URL ? input.href : input instanceof Request ? input.url : input as string;
-
- const request = {
- ...init ?? {},
- url,
- method: init?.method ?? 'GET',
- headers: {} as Record,
- };
- new Headers(init?.headers).forEach((value, key) => request.headers[key] = value);
- request.headers['Authorization'] = `HttpSig cred="${this.baseUrl}"`;
-
- const signed = await httpbis.signMessage({
- key,
- fields: [ '@target-uri', '@method' ],
- paramValues: { keyid: 'TODO' }
- }, request);
-
- return await this.fetcher.fetch(url, signed);
- }
-}
diff --git a/packages/css/test/unit/util/fetch/SignedFetcher.test.ts b/packages/css/test/unit/util/fetch/SignedFetcher.test.ts
deleted file mode 100644
index c4958fb5..00000000
--- a/packages/css/test/unit/util/fetch/SignedFetcher.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { JwkGenerator } from '@solid/community-server';
-import { httpbis, SignConfig } from 'http-message-signatures';
-import { Mocked } from 'vitest';
-import { Fetcher } from '../../../../src/util/fetch/Fetcher';
-import { SignedFetcher } from '../../../../src/util/fetch/SignedFetcher';
-
-vi.mock('http-message-signatures', () => ({
- httpbis: { signMessage: vi.fn() }
-}));
-
-vi.mock('node:crypto', () => ({
- webcrypto: {},
-}));
-
-describe('SignedFetcher', (): void => {
- const baseUrl = 'http://example.com';
- const jwk = { alg: 'ES256', kid: 'kid' };
- const signedMessage = 'signed';
- const signMessage = vi.mocked(httpbis.signMessage);
- let keyGen: Mocked;
- let source: Mocked;
- let fetcher: SignedFetcher;
-
- beforeEach(async(): Promise => {
- signMessage.mockClear();
- signMessage.mockResolvedValue(signedMessage as any);
-
- keyGen = {
- alg: 'ES256',
- getPrivateKey: vi.fn().mockResolvedValue(jwk),
- getPublicKey: vi.fn(),
- };
-
- source = {
- fetch: vi.fn().mockResolvedValue('result'),
- };
-
- fetcher = new SignedFetcher(source, baseUrl, keyGen);
- });
-
- it('performs the fetch with the signed request.', async(): Promise => {
- await expect(fetcher.fetch('http://example.com', { method: 'DELETE', headers: { accept: 'text/turtle' } })).resolves.toBe('result');
- expect(signMessage).toHaveBeenCalledTimes(1);
- expect(signMessage).toHaveBeenLastCalledWith(
- {
- key: expect.objectContaining({ alg: 'ES256', id: 'kid' }),
- fields: [ '@target-uri', '@method' ],
- paramValues: { keyid: 'TODO' }
- },
- {
- url: 'http://example.com',
- method: 'DELETE',
- headers: { accept: 'text/turtle', Authorization: 'HttpSig cred="http://example.com"' },
- },
- );
- expect(source.fetch).toHaveBeenCalledTimes(1);
- expect(source.fetch).toHaveBeenLastCalledWith('http://example.com', 'signed');
-
- // Testing the internal sign function
- const importKeyMock = vitest.spyOn(crypto.subtle, 'importKey').mockResolvedValueOnce('key!' as any);
- const signMock = vitest.spyOn(crypto.subtle, 'sign').mockResolvedValueOnce('signed!' as any);
- const params = { name: 'ECDSA', namedCurve: 'P-256', hash: 'SHA-256' };
-
- const sign = (signMessage.mock.calls[0][0] as SignConfig).key.sign;
- const data = Buffer.from('data');
- await expect(sign(data)).resolves.toEqual(Buffer.from('signed!'));
- expect(importKeyMock).toHaveBeenCalledTimes(1);
- expect(importKeyMock)
- .toHaveBeenLastCalledWith('jwk', jwk, params, false, ['sign']);
- expect(signMock).toHaveBeenCalledTimes(1);
- expect(signMock).toHaveBeenLastCalledWith(params, 'key!', data);
- });
-});
diff --git a/packages/uma/config/credentials/validators/http-message.json b/packages/uma/config/credentials/validators/http-message.json
deleted file mode 100644
index da88fd28..00000000
--- a/packages/uma/config/credentials/validators/http-message.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "@context": [
- "https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma/^0.0.0/components/context.jsonld"
- ],
- "@graph": [
- {
- "@id": "urn:uma:default:RequestValidator",
- "@type": "HttpMessageValidator"
- }
- ]
-}
diff --git a/packages/uma/config/credentials/verifiers/default.json b/packages/uma/config/credentials/verifiers/default.json
index b8787f02..7f4d8ef0 100644
--- a/packages/uma/config/credentials/verifiers/default.json
+++ b/packages/uma/config/credentials/verifiers/default.json
@@ -32,10 +32,16 @@
"TypedVerifier:_verifiers_value": {
"@id": "urn:uma:default:OidcVerifier",
"@type": "OidcVerifier",
- "baseUrl": { "@id": "urn:uma:variables:baseUrl" },
"derivationStore": { "@id": "urn:uma:default:DerivationStore" }
}
},
+ {
+ "TypedVerifier:_verifiers_key": "application/vc+jwt",
+ "TypedVerifier:_verifiers_value": {
+ "@id": "urn:uma:default:VcVerifier",
+ "@type": "VcVerifier"
+ }
+ },
{
"TypedVerifier:_verifiers_key": "urn:ietf:params:oauth:token-type:access_token",
"TypedVerifier:_verifiers_value": { "@id": "urn:uma:default:OidcVerifier" }
diff --git a/packages/uma/package.json b/packages/uma/package.json
index 04d0ef18..8028f778 100644
--- a/packages/uma/package.json
+++ b/packages/uma/package.json
@@ -62,14 +62,15 @@
"@httpland/authorization-parser": "^1.1.0",
"@solid/access-token-verifier": "^1.2.0",
"@solid/community-server": "^8.0.0-alpha.1",
+ "@types/jsonpath": "^0.2.4",
"@types/ms": "^2.1.0",
"@types/n3": "^1.16.4",
"asynchronous-handlers": "^1.0.2",
"componentsjs": "^6.3.0",
- "get-jwks": "^9.0.1",
"global-logger-factory": "^1.0.0",
"http-message-signatures": "^1.0.4",
"jose": "^5.2.2",
+ "jsonpath": "^1.3.0",
"logform": "^2.6.0",
"ms": "^2.1.3",
"n3": "^1.17.2",
diff --git a/packages/uma/src/credentials/Claims.ts b/packages/uma/src/credentials/Claims.ts
index fd16d158..5059b6e1 100644
--- a/packages/uma/src/credentials/Claims.ts
+++ b/packages/uma/src/credentials/Claims.ts
@@ -5,6 +5,7 @@ export const ORIGINAL = 'urn:solidlab:uma:claims:types:original';
export const PURPOSE = 'http://www.w3.org/ns/odrl/2/purpose';
export const LEGAL_BASIS = 'https://w3id.org/oac#LegalBasis';
export const ACCESS = 'urn:solidlab:uma:claims:types:access';
+export const VC = 'urn:solidlab:uma:claims:types:vc';
/**
* Resolves a claim value by preferring an ORIGINAL claim-set entry when present.
@@ -12,8 +13,8 @@ export const ACCESS = 'urn:solidlab:uma:claims:types:access';
export function getOriginalClaimValue(claims: NodeJS.Dict, claimType: string): unknown {
const original = claims[ORIGINAL];
if (typeof original === 'object' && original !== null) {
- const originalClaims = original as Record;
- return originalClaims[claimType];
+ const originalClaims = original as Record;
+ return originalClaims[claimType];
}
return claims[claimType];
diff --git a/packages/uma/src/credentials/Formats.ts b/packages/uma/src/credentials/Formats.ts
index 36fc4623..72773322 100644
--- a/packages/uma/src/credentials/Formats.ts
+++ b/packages/uma/src/credentials/Formats.ts
@@ -4,3 +4,4 @@ export const UNSECURE = 'urn:solidlab:uma:claims:formats:webid';
export const OIDC = 'http://openid.net/specs/openid-connect-core-1_0.html#IDToken';
export const ACCESS_TOKEN = 'urn:ietf:params:oauth:token-type:access_token';
export const REFRESH_TOKEN = 'urn:ietf:params:oauth:token-type:refresh_token';
+export const VC_JWT = 'application/vc+jwt';
diff --git a/packages/uma/src/credentials/verify/JwtVerifier.ts b/packages/uma/src/credentials/verify/JwtVerifier.ts
index 5675526a..df265d8d 100644
--- a/packages/uma/src/credentials/verify/JwtVerifier.ts
+++ b/packages/uma/src/credentials/verify/JwtVerifier.ts
@@ -1,6 +1,6 @@
-import buildGetJwks, { GetJwks } from 'get-jwks';
import { getLoggerFor } from 'global-logger-factory';
-import { decodeJwt, decodeProtectedHeader, jwtVerify } from 'jose';
+import { decodeJwt, jwtVerify } from 'jose';
+import { getJwks } from '../../util/JwtUtil';
import { ClaimSet } from '../ClaimSet';
import { Credential } from '../Credential';
import { JWT } from '../Formats';
@@ -12,7 +12,6 @@ import { Verifier } from './Verifier';
*/
export class JwtVerifier implements Verifier {
protected readonly logger = getLoggerFor(this);
- protected jwks:GetJwks = buildGetJwks();
constructor(
private readonly allowedClaims: string[],
@@ -34,23 +33,8 @@ export class JwtVerifier implements Verifier {
throw new Error(`JWT should contain 'iss' claim.`);
}
- const params = decodeProtectedHeader(credential.token);
-
- if (!params.alg) {
- throw new Error(`JWT should contain 'alg' header.`);
- }
-
- if (!params.kid) {
- throw new Error(`JWT should contain 'kid' header.`);
- }
-
- const jwk = await this.jwks.getJwk({
- domain: claims.iss,
- alg: params.alg,
- kid: params.kid,
- });
-
- await jwtVerify(credential.token, Object.assign(jwk, { type: 'JWK' }));
+ const jwkSet = await getJwks(claims.iss);
+ await jwtVerify(credential.token, jwkSet);
}
for (const claim of Object.keys(claims)) {
diff --git a/packages/uma/src/credentials/verify/OidcVerifier.ts b/packages/uma/src/credentials/verify/OidcVerifier.ts
index 8e5f4c6d..caefcc66 100644
--- a/packages/uma/src/credentials/verify/OidcVerifier.ts
+++ b/packages/uma/src/credentials/verify/OidcVerifier.ts
@@ -1,15 +1,10 @@
import { createSolidTokenVerifier } from '@solid/access-token-verifier';
-import {
- BadRequestHttpError,
- ForbiddenHttpError,
- InternalServerError,
- joinUrl,
- KeyValueStorage
-} from '@solid/community-server';
+import { BadRequestHttpError, ForbiddenHttpError, InternalServerError, KeyValueStorage } from '@solid/community-server';
import { getLoggerFor } from 'global-logger-factory';
-import { createRemoteJWKSet, decodeJwt, JWTPayload, jwtVerify } from 'jose';
+import { decodeJwt, jwtVerify } from 'jose';
import { AccessToken } from '../../tokens/AccessToken';
import { UMA_SCOPES } from '../../ucp/util/Vocabularies';
+import { getJwks } from '../../util/JwtUtil';
import { reType } from '../../util/ReType';
import { Permission } from '../../views/Permission';
import { ACCESS, CLIENTID, WEBID } from '../Claims';
@@ -21,8 +16,7 @@ import { Verifier } from './Verifier';
/**
* A Verifier for OIDC Tokens.
*
- * The `allowedIssuers` list can be used to only allow tokens from these issuers.
- * Default is an empty list, which allows all issuers.
+ * To only allow tokens from certain issuers, set `verifyOptions` to { issuer: [ 'http://example.com/' ] }.
*/
export class OidcVerifier implements Verifier {
protected readonly logger = getLoggerFor(this);
@@ -30,10 +24,8 @@ export class OidcVerifier implements Verifier {
private readonly verifyToken = createSolidTokenVerifier();
public constructor(
- protected readonly baseUrl: string,
protected readonly derivationStore: KeyValueStorage,
- protected readonly allowedIssuers: string[] = [],
- protected readonly verifyOptions: Record = {},
+ protected readonly verifyOptions: Record = {}, // JWTVerifyOptions
) {}
/** @inheritdoc */
@@ -46,11 +38,10 @@ export class OidcVerifier implements Verifier {
// We first need to determine if this is a Solid OIDC token or a standard one
const unsafeDecoded = decodeJwt(credential.token);
const isSolidToken = (unsafeDecoded.aud === 'solid' ||
- (Array.isArray(unsafeDecoded.aud) && unsafeDecoded.aud.includes('solid')))
+ (Array.isArray(unsafeDecoded.aud) && unsafeDecoded.aud.includes('solid')))
&& typeof unsafeDecoded.webid === 'string';
try {
- this.validateToken(unsafeDecoded);
if (isSolidToken) {
return await this.verifySolidToken(credential.token);
} else {
@@ -64,18 +55,13 @@ export class OidcVerifier implements Verifier {
}
}
- protected validateToken(payload: JWTPayload): void {
- // TODO: disable audience check for now, need to investigate required values further
- // if (payload.aud !== this.baseUrl && !(Array.isArray(payload.aud) && payload.aud.includes(this.baseUrl))) {
- // throw new BadRequestHttpError('This server is not valid audience for the token');
- // }
- if (!payload.iss || this.allowedIssuers.length > 0 && !this.allowedIssuers.includes(payload.iss)) {
- throw new BadRequestHttpError('Unsupported issuer');
- }
- }
-
protected async verifySolidToken(token: string): Promise<{ [WEBID]: string, [CLIENTID]?: string }> {
const claims = await this.verifyToken(`Bearer ${token}`);
+ const issuers = this.verifyOptions.issuer;
+ const allowedIssuers = issuers !== undefined && (typeof issuers === 'string' ? [issuers] : issuers as string[]);
+ if (!claims.iss || (allowedIssuers && !allowedIssuers.includes(claims.iss))) {
+ throw new BadRequestHttpError('Unsupported issuer');
+ }
// Depends on the spec version which field to use
const clientId = (claims as { azp?: string }).azp ?? claims.client_id;
@@ -91,16 +77,7 @@ export class OidcVerifier implements Verifier {
protected async verifyStandardToken(token: string, format: string, issuer: string):
Promise<{ [WEBID]?: string, [CLIENTID]?: string, [ACCESS]?: Permission[] }> {
- const configUrl = joinUrl(issuer, '/.well-known/openid-configuration');
- const configResponse = await fetch(configUrl);
- if (configResponse.status !== 200) {
- throw new BadRequestHttpError(`Unable to access ${configUrl}`);
- }
- const config = await configResponse.json() as { jwks_uri?: string };
- if (!config.jwks_uri) {
- throw new BadRequestHttpError(`Missing jwks_uri from ${configUrl}`);
- }
- const jwkSet = createRemoteJWKSet(new URL(config.jwks_uri));
+ const jwkSet = await getJwks(issuer);
const decoded = await jwtVerify(token, jwkSet, this.verifyOptions);
if (format === OIDC) {
diff --git a/packages/uma/src/credentials/verify/VcVerifier.ts b/packages/uma/src/credentials/verify/VcVerifier.ts
new file mode 100644
index 00000000..ccac874f
--- /dev/null
+++ b/packages/uma/src/credentials/verify/VcVerifier.ts
@@ -0,0 +1,76 @@
+import { BadRequestHttpError } from '@solid/community-server';
+import { getLoggerFor } from 'global-logger-factory';
+import { decodeJwt, JWTPayload, jwtVerify } from 'jose';
+import { getJwks } from '../../util/JwtUtil';
+import { VC } from '../Claims';
+import { ClaimSet } from '../ClaimSet';
+import { Credential } from '../Credential';
+import { VC_JWT } from '../Formats';
+import { Verifier } from './Verifier';
+
+// TODO:
+
+// TODO: implementation probably based too much on current example format
+
+/**
+ * A Verifier for VC Tokens.
+ *
+ * To only allow tokens from certain options, set `verifyOptions` to { issuer: [ 'http://example.com/' ] }.
+ */
+export class VcVerifier implements Verifier {
+ protected readonly logger = getLoggerFor(this);
+
+ public constructor(
+ protected readonly verifyOptions: Record = {},
+ ) {}
+
+ public async verify(credential: Credential): Promise {
+ this.logger.debug(`Verifying credential ${JSON.stringify(credential)}`);
+ if (credential.format !== VC_JWT) {
+ throw new BadRequestHttpError(`Token format ${credential.format} does not match this processor's format.`);
+ }
+
+ const unsafeDecoded = decodeJwt(credential.token);
+ if (!unsafeDecoded.iss) {
+ throw new BadRequestHttpError(`Token is missing the issuer claim.`);
+ }
+
+ const jwkSet = await getJwks(unsafeDecoded.iss);
+ const decoded = await jwtVerify(credential.token, jwkSet, this.verifyOptions);
+
+ // TODO: could extract all entries as separate jsonpath claims
+ // TODO: currently only a single VC as input is accepted,
+ // if the client provides multiple these would override each other
+ const claims = this.extractVcClaims(decoded.payload);
+ return { [VC]: claims };
+ }
+
+ protected extractVcClaims(payload: JWTPayload): ClaimSet {
+ if (!payload.vc || typeof payload.vc !== 'object') {
+ throw new BadRequestHttpError(`Token is missing the vc claim.`);
+ }
+
+ const vc = payload.vc as Record;
+ if (typeof vc.issuanceDate === 'string' && new Date(vc.issuanceDate) > new Date()) {
+ throw new BadRequestHttpError(`VC is not yet valid, issued at ${vc.issuanceDate}.`);
+ }
+ if (typeof vc.validFrom === 'string' && new Date(vc.validFrom) > new Date()) {
+ throw new BadRequestHttpError(`VC is not yet valid, valid from ${vc.validFrom}.`);
+ }
+
+ if (typeof vc.expirationDate === 'string' && new Date(vc.expirationDate) < new Date()) {
+ throw new BadRequestHttpError(`VC expired at ${vc.expirationDate}.`);
+ }
+ if (typeof vc.validUntil === 'string' && new Date(vc.validUntil) < new Date()) {
+ throw new BadRequestHttpError(`VC expired at ${vc.validUntil}.`);
+ }
+
+ if (typeof vc.credentialSubject !== 'object') {
+ throw new BadRequestHttpError(`VC is missing the credentialSubject claim.`);
+ }
+
+ this.logger.debug(`Validated VC claims ${JSON.stringify(payload)}`);
+
+ return vc;
+ }
+}
diff --git a/packages/uma/src/index.ts b/packages/uma/src/index.ts
index 1b821485..afa9e5e0 100644
--- a/packages/uma/src/index.ts
+++ b/packages/uma/src/index.ts
@@ -17,6 +17,7 @@ export * from './credentials/verify/JwtVerifier';
export * from './credentials/verify/IriVerifier';
export * from './credentials/verify/RefreshTokenVerifier';
export * from './credentials/verify/KeyValueVerifier';
+export * from './credentials/verify/VcVerifier';
// Dialog
export * from './dialog/AggregatorNegotiator';
@@ -87,7 +88,6 @@ export * from './util/http/server/JsonHttpErrorHandler';
export * from './util/http/server/JsonFormHttpHandler';
export * from './util/http/server/NodeHttpRequestResponseHandler';
export * from './util/http/server/RoutedHttpRequestHandler';
-export * from './util/http/validate/HttpMessageValidator';
export * from './util/http/validate/PatRequestValidator';
export * from './util/http/validate/RequestValidator';
@@ -107,7 +107,7 @@ export * from './ucp/util/Vocabularies';
// Util
export * from './util/AggregatorUtil';
export * from './util/ConvertUtil';
-export * from './util/HttpMessageSignatures';
+export * from './util/JwtUtil';
export * from './util/RegistrationStore';
export * from './util/Result';
export * from './util/ReType';
diff --git a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts
index afb7ddcf..33dbb118 100644
--- a/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts
+++ b/packages/uma/src/policies/authorizers/OdrlAuthorizer.ts
@@ -9,6 +9,7 @@ import { PrioritizeProhibitionStrategy } from '../../ucp/policy/PrioritizeProhib
import { Strategy } from '../../ucp/policy/Strategy';
import { UCPPolicy } from '../../ucp/policy/UsageControlPolicy';
import { UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
+import { isIri } from '../../util/ConvertUtil';
import { Permission } from '../../views/Permission';
import { Authorizer } from './Authorizer';
@@ -76,9 +77,9 @@ export class OdrlAuthorizer implements Authorizer {
const subject = typeof claims[WEBID] === 'string' ? claims[WEBID] : 'urn:solidlab:uma:id:anonymous';
const claimContextConstraints: { subject: ReturnType; quads: Quad[] }[] = [];
- for (const [ claimKey, leftOperand ] of Object.entries(claimOperandMap)) {
- const claimValue = claims[claimKey];
- if (typeof claimValue !== 'string') {
+ for (const [ key, value ] of Object.entries(claims)) {
+ const leftOperand = claimOperandMap[key] ?? key;
+ if (!isIri(leftOperand) || typeof value !== 'string') {
continue;
}
const claimSubject = blankNode();
@@ -88,7 +89,7 @@ export class OdrlAuthorizer implements Authorizer {
quad(claimSubject, RDF.terms.type, ODRL.terms.Constraint),
quad(claimSubject, ODRL.terms.leftOperand, namedNode(leftOperand)),
quad(claimSubject, ODRL.terms.operator, ODRL.terms.eq),
- quad(claimSubject, ODRL.terms.rightOperand, namedNode(claimValue)),
+ quad(claimSubject, ODRL.terms.rightOperand, namedNode(value)),
],
});
}
diff --git a/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts b/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts
index 1f08c6f5..3963a4f7 100644
--- a/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts
+++ b/packages/uma/src/policies/authorizers/SimpleOdrlAuthorizer.ts
@@ -1,10 +1,12 @@
import { NamedNode } from '@rdfjs/types';
import { getLoggerFor } from 'global-logger-factory';
-import { DataFactory as DF, Quad_Subject, Store } from 'n3';
+import jp from 'jsonpath';
+import { DataFactory as DF, Quad_Subject } from 'n3';
import { ODRL } from 'odrl-evaluator';
-import { CLIENTID, PURPOSE, WEBID } from '../../credentials/Claims';
+import { CLIENTID, VC, WEBID } from '../../credentials/Claims';
import { ClaimSet } from '../../credentials/ClaimSet';
import { ReadOnlyStore, UCRulesStorage } from '../../ucp/storage/UCRulesStorage';
+import { OVC } from '../../ucp/util/Vocabularies';
import { Permission } from '../../views/Permission';
import { Authorizer } from './Authorizer';
@@ -28,8 +30,7 @@ const dateComparators: NodeJS.Dict<(a: Date, b: Date) => boolean> = {
};
const claimOperandMap: Record = {
- [ODRL.deliveryChannel]: CLIENTID,
- [ODRL.purpose]: PURPOSE
+ [ODRL.deliveryChannel]: CLIENTID,
} as const;
/**
@@ -110,8 +111,8 @@ export class SimpleOdrlAuthorizer implements Authorizer {
}
return ruleAssignees.some(ruleAssignee => assignees.some(assignee => assignee.equals(ruleAssignee)));
});
- this.logger.warn('Rejecting request because no rules with a matching assignee or party collection were found');
if (rules.length === 0) {
+ this.logger.warn('Rejecting request because no rules with a matching assignee or party collection were found');
return [];
}
@@ -119,9 +120,10 @@ export class SimpleOdrlAuthorizer implements Authorizer {
const validRules: Quad_Subject[] = [];
for (const rule of rules) {
const constraintResponse = this.validateConstraints(rule, policies, claims);
- if (constraintResponse === true) {
+ const vcConstraintResponse = this.validateOvcConstraints(rule, policies, claims);
+ if (constraintResponse && vcConstraintResponse) {
validRules.push(rule);
- } else if (constraintResponse === undefined) {
+ } else if (constraintResponse === undefined || vcConstraintResponse === undefined) {
return;
}
}
@@ -168,6 +170,8 @@ export class SimpleOdrlAuthorizer implements Authorizer {
if (constraints.some(({ leftOperand, operator, rightOperand }) => !leftOperand || !operator || !rightOperand)) {
return;
}
+ // TODO: would want middleware step where credentials and other stuff are already extracted into RDF values
+ // so both ODRL authorizers don't have to bother with this
for (const constraint of constraints) {
// Return undefined if any of these are too complex or unknown
if (constraint.leftOperand.equals(ODRL.terms.dateTime)) {
@@ -188,6 +192,11 @@ export class SimpleOdrlAuthorizer implements Authorizer {
if (typeof claimValue !== 'string' || constraint.rightOperand.value !== claimValue) {
return false;
}
+ } else if (typeof claims[constraint.leftOperand.value] === 'string'
+ && constraint.operator.equals(ODRL.terms.eq)) {
+ if (constraint.rightOperand.value !== claims[constraint.leftOperand.value]) {
+ return false;
+ }
} else {
// Unsupported constraint
return;
@@ -195,4 +204,42 @@ export class SimpleOdrlAuthorizer implements Authorizer {
}
return true;
}
+
+ // https://gitlab.com/gaia-x/lab/policy-reasoning/odrl-vc-profile
+ protected validateOvcConstraints(rule: Quad_Subject, policies: ReadOnlyStore, claims: ClaimSet): boolean | undefined {
+ const constraints = policies.getObjects(rule, OVC.terms.constraint, null).map(constraint => ({
+ leftOperand: policies.getObjects(constraint, OVC.terms.leftOperand, null)[0],
+ operator: policies.getObjects(constraint, ODRL.terms.operator, null)[0],
+ rightOperand: policies.getObjects(constraint, ODRL.terms.rightOperand, null)[0],
+ credentialSubjectType: policies.getObjects(constraint, OVC.terms.credentialSubjectType, null)[0],
+ }));
+ // If any of these are undefined this is too complex to handle here (credentialSubjectType can be undefined)
+ if (constraints.some(({ leftOperand, operator, rightOperand }) => !leftOperand || !operator || !rightOperand)) {
+ return;
+ }
+ // Can't match a VC constraint if there is no VC input
+ const vc = claims[VC];
+ if (constraints.length > 0 && typeof vc !== 'object') {
+ return false;
+ }
+
+ for (const constraint of constraints) {
+ // Only support odrl:eq for now
+ if (!constraint.operator.equals(ODRL.terms.eq)) {
+ return;
+ }
+ const results = jp.query(vc, constraint.leftOperand.value).flat();
+ if (!results.some(result => result === constraint.rightOperand.value)) {
+ return false;
+ }
+ if (constraint.credentialSubjectType) {
+ const types = jp.query(vc, '$.type').flat();
+ if (!types.some(typ => constraint.credentialSubjectType.value === typ)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
}
diff --git a/packages/uma/src/ucp/util/Vocabularies.ts b/packages/uma/src/ucp/util/Vocabularies.ts
index 2226bedd..c37d80f7 100644
--- a/packages/uma/src/ucp/util/Vocabularies.ts
+++ b/packages/uma/src/ucp/util/Vocabularies.ts
@@ -4,35 +4,35 @@ import { createVocabulary, extendVocabulary } from 'rdf-vocabulary';
export const DC = extendVocabulary(DC_CSS,'creator');
export const ODRL = createVocabulary(
- 'http://www.w3.org/ns/odrl/2/',
- 'AssetCollection',
- 'Agreement',
- 'Offer',
- 'Permission',
- 'Prohibition',
- 'Duty',
- 'Request',
- 'Constraint',
- 'source',
- 'partOf',
- 'action',
- 'target',
- 'assignee',
- 'assigner',
- 'constraint',
- 'operator',
- 'permission',
- 'prohibition',
- 'duty',
- 'dateTime',
- 'purpose',
- 'leftOperand',
- 'rightOperand',
- 'gt',
- 'lt',
- 'eq',
- 'uid',
- 'read',
+ 'http://www.w3.org/ns/odrl/2/',
+ 'AssetCollection',
+ 'Agreement',
+ 'Offer',
+ 'Permission',
+ 'Prohibition',
+ 'Duty',
+ 'Request',
+ 'Constraint',
+ 'source',
+ 'partOf',
+ 'action',
+ 'target',
+ 'assignee',
+ 'assigner',
+ 'constraint',
+ 'operator',
+ 'permission',
+ 'prohibition',
+ 'duty',
+ 'dateTime',
+ 'purpose',
+ 'leftOperand',
+ 'rightOperand',
+ 'gt',
+ 'lt',
+ 'eq',
+ 'uid',
+ 'read',
);
export const ODRL_P = createVocabulary(
@@ -40,6 +40,13 @@ export const ODRL_P = createVocabulary(
'relation',
);
+export const OVC = createVocabulary(
+ 'https://w3id.org/gaia-x/ovc/1/',
+ 'constraint',
+ 'credentialSubjectType',
+ 'leftOperand',
+);
+
export const OWL = createVocabulary(
'http://www.w3.org/2002/07/owl#',
'inverseOf',
diff --git a/packages/uma/src/util/HttpMessageSignatures.ts b/packages/uma/src/util/HttpMessageSignatures.ts
deleted file mode 100644
index c04aed9b..00000000
--- a/packages/uma/src/util/HttpMessageSignatures.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { type AlgJwk } from '@solid/community-server';
-import { httpbis, type Request as SignRequest, type SigningKey } from 'http-message-signatures';
-import crypto, { webcrypto } from 'node:crypto';
-import { BufferSource } from 'node:stream/web';
-
-export async function signRequest(
- url: string,
- request: RequestInit & Omit,
- jwk: AlgJwk
-): Promise {
- const key: SigningKey = {
- id: jwk.kid,
- alg: jwk.alg,
- async sign(data: BufferSource) {
- const params = algMap[jwk.alg];
- const key = await crypto.subtle.importKey('jwk', jwk, params, false, ['sign']);
- return Buffer.from(await crypto.subtle.sign(params, key, data));
- },
- };
-
- return await httpbis.signMessage({ key, fields: [ '@target-uri', '@method' ] }, { ...request, url });
-}
-
-type AlgParams = webcrypto.RsaHashedImportParams | webcrypto.EcKeyImportParams | webcrypto.HmacImportParams
-
-export const algMap: Record = {
- 'ES256': { name: 'ECDSA', hash: 'SHA-256', namedCurve: 'P-256' },
- 'ES384': { name: 'ECDSA', hash: 'SHA-384', namedCurve: 'P-384' },
- 'ES512': { name: 'ECDSA', hash: 'SHA-512', namedCurve: 'P-512' },
- 'HS256': { name: 'HMAC', hash: 'SHA-256' },
- 'HS384': { name: 'HMAC', hash: 'SHA-384' },
- 'HS512': { name: 'HMAC', hash: 'SHA-512' },
- 'PS256': { name: 'RSASSA-PSS', hash: 'SHA-256' },
- 'PS384': { name: 'RSASSA-PSS', hash: 'SHA-384' },
- 'PS512': { name: 'RSASSA-PSS', hash: 'SHA-512' },
- 'RS256': { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
- 'RS384': { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-384' },
- 'RS512': { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-512' },
-}
diff --git a/packages/uma/src/util/JwtUtil.ts b/packages/uma/src/util/JwtUtil.ts
new file mode 100644
index 00000000..2c59913b
--- /dev/null
+++ b/packages/uma/src/util/JwtUtil.ts
@@ -0,0 +1,32 @@
+import { BadRequestHttpError, joinUrl } from '@solid/community-server';
+import { createRemoteJWKSet, jwtVerify, JWTVerifyOptions, JWTVerifyResult } from 'jose';
+
+export type JwkSet = ReturnType;
+
+/**
+ * Cache for JWKS records.
+ */
+export const jwksRecord: Record = {};
+
+/**
+ * Builds a JWKS for the given issuer URl.
+ */
+export async function getJwks(issuer: string): Promise {
+ if (jwksRecord[issuer]) {
+ return jwksRecord[issuer];
+ }
+ const configUrl = joinUrl(issuer, '/.well-known/openid-configuration');
+ const configResponse = await fetch(configUrl);
+ if (configResponse.status !== 200) {
+ throw new BadRequestHttpError(`Unable to access ${configUrl}`);
+ }
+ const config = await configResponse.json() as { jwks_uri?: string, issuer?: string };
+ if (config.issuer !== issuer) {
+ throw new BadRequestHttpError(`Issuer mismatch: expected ${issuer}, got ${config.issuer}`);
+ }
+ if (!config.jwks_uri) {
+ throw new BadRequestHttpError(`Missing jwks_uri from ${configUrl}`);
+ }
+ jwksRecord[issuer] = createRemoteJWKSet(new URL(config.jwks_uri));
+ return jwksRecord[issuer];
+}
diff --git a/packages/uma/src/util/http/validate/HttpMessageValidator.ts b/packages/uma/src/util/http/validate/HttpMessageValidator.ts
deleted file mode 100644
index cb421424..00000000
--- a/packages/uma/src/util/http/validate/HttpMessageValidator.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { BadRequestHttpError, UnauthorizedHttpError } from '@solid/community-server';
-import buildGetJwks from 'get-jwks';
-import { verifyMessage } from 'http-message-signatures/lib/httpbis';
-import { SignatureParameters, VerifierFinder, VerifyingKey } from 'http-message-signatures/lib/types';
-import crypto from 'node:crypto';
-import { algMap } from '../../HttpMessageSignatures';
-import { HttpHandlerRequest } from '../models/HttpHandler';
-import { RequestValidator, RequestValidatorInput, RequestValidatorOutput } from './RequestValidator';
-
-const authParserMod = import('@httpland/authorization-parser');
-
-/**
- * Validates requests using HTTP Message Signatures.
- * This validator can not differentiate between individual owners
- * and returns the server who signed the request as owner instead.
- */
-export class HttpMessageValidator extends RequestValidator {
- public async handle(input: RequestValidatorInput): Promise {
- const signer = await this.extractRequestSigner(input.request);
- if (!await this.verifyRequest(input.request, signer)) {
- throw new UnauthorizedHttpError('Failed to verify signature');
- }
- return { owner: signer };
- }
-
- protected async extractRequestSigner(request: HttpHandlerRequest): Promise {
- const { authorization } = request.headers;
- if (!authorization) {
- throw new UnauthorizedHttpError('Missing authorization header in request.');
- }
-
- const { authScheme, params } = (await authParserMod).parseAuthorization(authorization);
- if (authScheme !== 'HttpSig') {
- throw new UnauthorizedHttpError();
- }
-
- if (!params || typeof params !== 'object' || !params.cred) {
- throw new UnauthorizedHttpError();
- }
-
- let signer = params.cred;
- if (signer.startsWith('"')) signer = signer.slice(1);
- if (signer.endsWith('"')) signer = signer.slice(0,-1);
-
- return signer;
- }
-
- protected async verifyRequest(
- request: HttpHandlerRequest,
- signer: string,
- ): Promise {
- const jwks = buildGetJwks();
-
- const keyLookup: VerifierFinder = async (params: SignatureParameters) => {
- const { alg, keyid } = params;
-
- try {
- const jwk = await jwks.getJwk({
- domain: signer!,
- alg: alg ?? '',
- kid: keyid ?? '',
- });
-
- if (!alg) throw new BadRequestHttpError('Invalid HTTP message Signature parameters.');
-
- const verifier: VerifyingKey = {
- id: keyid,
- algs: alg ? [ alg ] : [],
- async verify(data: Buffer, signature: Buffer) {
- try {
- const params = algMap[alg];
- const key = await crypto.subtle.importKey('jwk', jwk, params, false, ['verify']);
- return await crypto.subtle.verify(params, key, signature, data);
- } catch (err) { console.log(err); return null }
- },
- };
-
- return verifier;
-
- } catch (err) {
- throw new Error(`Something went wrong during signature checking: ${err.message}`)
- }
- };
-
- const verified = await verifyMessage({ keyLookup }, request);
- return verified ?? false;
- }
-}
diff --git a/packages/uma/test/unit/credentials/verify/JwtVerifier.test.ts b/packages/uma/test/unit/credentials/verify/JwtVerifier.test.ts
index 83155cc0..8fcbf30d 100644
--- a/packages/uma/test/unit/credentials/verify/JwtVerifier.test.ts
+++ b/packages/uma/test/unit/credentials/verify/JwtVerifier.test.ts
@@ -1,21 +1,14 @@
-import * as getJwks from 'get-jwks';
import * as jose from 'jose';
import { Credential } from '../../../../src/credentials/Credential';
import { JwtVerifier } from '../../../../src/credentials/verify/JwtVerifier';
-vi.mock('get-jwks');
vi.mock('jose');
describe('JwtVerifier', (): void => {
- const getJwkMock = vi.fn();
- const getJwksMock = vi.spyOn(getJwks, 'default').mockReturnValue({
- getJwk: getJwkMock,
- } as any);
const decodeMock = vi.spyOn(jose, 'decodeJwt');
- const decodeHeaderMock = vi.spyOn(jose, 'decodeProtectedHeader');
const verifyMock = vi.spyOn(jose, 'jwtVerify');
- const issuer = 'issuer';
+ const issuer = 'http://example.com/issuer';
const credential: Credential = {
format: 'urn:solidlab:uma:claims:formats:jwt',
token: 'token',
@@ -32,13 +25,6 @@ describe('JwtVerifier', (): void => {
claim2: 'val2',
});
- decodeHeaderMock.mockReturnValue({
- alg: 'alg',
- kid: 'kid',
- });
-
- getJwkMock.mockResolvedValue({ key: 'value' });
-
verifier = new JwtVerifier(allowedClaims, false, false);
});
@@ -53,8 +39,6 @@ describe('JwtVerifier', (): void => {
expect(decodeMock).toHaveBeenLastCalledWith(credential.token);
// Verification is off
- expect(decodeHeaderMock).toHaveBeenCalledTimes(0);
- expect(getJwkMock).toHaveBeenCalledTimes(0);
expect(verifyMock).toHaveBeenCalledTimes(0);
});
@@ -64,8 +48,20 @@ describe('JwtVerifier', (): void => {
});
describe('with verification enabled.', (): void => {
+ const remoteKeySet = 'remoteKeySet';
+ const fetchMock = vi.spyOn(global, 'fetch');
+ const createRemoteJWKSet = vi.spyOn(jose, 'createRemoteJWKSet');
beforeEach(async(): Promise => {
+ fetchMock.mockResolvedValue({
+ status: 200,
+ json: vi.fn().mockResolvedValue({
+ issuer,
+ jwks_uri: `${issuer}/jwks_uri`
+ }),
+ } as any);
+ createRemoteJWKSet.mockReturnValue(remoteKeySet as any);
+
verifier = new JwtVerifier(allowedClaims, false, true);
});
@@ -74,26 +70,12 @@ describe('JwtVerifier', (): void => {
await expect(verifier.verify(credential)).rejects.toThrow("JWT should contain 'iss' claim.");
});
- it('errors if the header does not contain an alg.', async(): Promise => {
- decodeHeaderMock.mockReturnValueOnce({ kid: 'kid' });
- await expect(verifier.verify(credential)).rejects.toThrow("JWT should contain 'alg' header.");
- });
-
- it('errors if the header does not contain a kid.', async(): Promise => {
- decodeHeaderMock.mockReturnValueOnce({ alg: 'alg' });
- await expect(verifier.verify(credential)).rejects.toThrow("JWT should contain 'kid' header.");
- });
-
it('verifies the token.', async(): Promise => {
await expect(verifier.verify(credential)).resolves.toEqual({ iss: issuer, claim1: 'val1', });
expect(decodeMock).toHaveBeenCalledTimes(1);
expect(decodeMock).toHaveBeenLastCalledWith(credential.token);
- expect(decodeHeaderMock).toHaveBeenCalledTimes(1);
- expect(decodeHeaderMock).toHaveBeenLastCalledWith(credential.token);
- expect(getJwkMock).toHaveBeenCalledTimes(1);
- expect(getJwkMock).toHaveBeenLastCalledWith({ domain: 'issuer', alg: 'alg', kid: 'kid' });
expect(verifyMock).toHaveBeenCalledTimes(1);
- expect(verifyMock).toHaveBeenLastCalledWith(credential.token, { key: 'value', type: 'JWK' });
+ expect(verifyMock).toHaveBeenLastCalledWith(credential.token, remoteKeySet);
});
});
});
diff --git a/packages/uma/test/unit/credentials/verify/OidcVerifier.test.ts b/packages/uma/test/unit/credentials/verify/OidcVerifier.test.ts
index 735084e0..07df824f 100644
--- a/packages/uma/test/unit/credentials/verify/OidcVerifier.test.ts
+++ b/packages/uma/test/unit/credentials/verify/OidcVerifier.test.ts
@@ -1,11 +1,9 @@
import * as accessTokenVerifier from '@solid/access-token-verifier';
import { KeyValueStorage } from '@solid/community-server';
-import { JWTPayload } from 'jose';
import * as jose from 'jose';
-import { Mocked, MockInstance } from 'vitest';
-import { ACCESS } from '../../../../src/credentials/Claims';
+import { JWTPayload } from 'jose';
+import { Mocked } from 'vitest';
import { Credential } from '../../../../src/credentials/Credential';
-import { ACCESS_TOKEN } from '../../../../src/credentials/Formats';
import { OidcVerifier } from '../../../../src/credentials/verify/OidcVerifier';
vi.mock('jose', () => ({
@@ -16,7 +14,6 @@ vi.mock('jose', () => ({
describe('OidcVerifier', (): void => {
const issuer = 'http://example.org/issuer';
- const baseUrl = 'http://example.com/uma';
let credential: Credential;
let decodedToken: JWTPayload;
@@ -39,13 +36,15 @@ describe('OidcVerifier', (): void => {
decodedToken = {
sub: 'sub',
iss: issuer,
- aud: baseUrl,
};
vi.clearAllMocks();
fetchMock.mockResolvedValue({
status: 200,
- json: vi.fn().mockResolvedValue({ jwks_uri: `${issuer}/jwks_uri` }),
+ json: vi.fn().mockResolvedValue({
+ issuer,
+ jwks_uri: `${issuer}/jwks_uri`
+ }),
} as any);
decodeJwt.mockReturnValue(decodedToken);
jwtVerify.mockResolvedValue({ payload: decodedToken } as any);
@@ -53,14 +52,15 @@ describe('OidcVerifier', (): void => {
verifierMock.mockResolvedValue({
webid: 'webId',
- client_id: 'clientId'
+ client_id: 'clientId',
+ iss: issuer,
});
derivationStore = {
get: vi.fn(),
} satisfies Partial> as any;
- verifier = new OidcVerifier(baseUrl, derivationStore);
+ verifier = new OidcVerifier(derivationStore);
});
it('errors on non-OIDC credentials.', async(): Promise => {
@@ -68,19 +68,9 @@ describe('OidcVerifier', (): void => {
.toThrow("Token format wrong does not match this processor's format.");
});
- it('errors if the issuer is not allowed.', async(): Promise => {
- verifier = new OidcVerifier(baseUrl, derivationStore, [ 'otherIssuer' ]);
- await expect(verifier.verify(credential)).rejects.toThrow('Unsupported issuer');
-
- verifier = new OidcVerifier(baseUrl, derivationStore, [ issuer ]);
- await expect(verifier.verify(credential)).resolves.toEqual({
- ['urn:solidlab:uma:claims:types:webid']: 'sub',
- });
- });
-
describe('parsing a Solid OIDC token', (): void => {
beforeEach(async(): Promise => {
- decodeJwt.mockReturnValue({ ...decodedToken, aud: [ baseUrl, 'solid' ], webid: 'webId' });
+ decodeJwt.mockReturnValue({ ...decodedToken, aud: [ 'solid' ], webid: 'webId' });
});
it('returns the extracted WebID.', async(): Promise => {
@@ -94,6 +84,17 @@ describe('OidcVerifier', (): void => {
verifierMock.mockRejectedValueOnce(new Error('bad data'));
await expect(verifier.verify(credential)).rejects.toThrow('Error verifying OIDC Token: bad data');
});
+
+ it('errors if the issuer is not allowed.', async(): Promise => {
+ verifier = new OidcVerifier(derivationStore, { issuer: [ 'otherIssuer' ] });
+ await expect(verifier.verify(credential)).rejects.toThrow('Unsupported issuer');
+
+ verifier = new OidcVerifier(derivationStore, { issuer: [ issuer ] });
+ await expect(verifier.verify(credential)).resolves.toEqual({
+ ['urn:solidlab:uma:claims:types:webid']: 'webId',
+ ['urn:solidlab:uma:claims:types:clientid']: 'clientId',
+ });
+ });
});
describe('parsing a standard OIDC token', (): void => {
@@ -116,6 +117,14 @@ describe('OidcVerifier', (): void => {
['urn:solidlab:uma:claims:types:clientid']: 'client',
});
});
+
+ it('uses verification options.', async(): Promise => {
+ verifier = new OidcVerifier(derivationStore, { issuer: [ issuer ] });
+ await expect(verifier.verify(credential)).resolves.toEqual({
+ ['urn:solidlab:uma:claims:types:webid']: 'sub',
+ });
+ expect(jwtVerify).toHaveBeenCalledExactlyOnceWith('token', remoteKeySet, { issuer: [ issuer ] });
+ });
});
describe('parsing access tokens', (): void => {
@@ -129,8 +138,7 @@ describe('OidcVerifier', (): void => {
{ resource_id: 'id2', resource_scopes: [ 'scope2', 'urn:knows:uma:scopes:derivation-read' ] },
{ resource_id: 'id3', resource_scopes: [ 'scope3' ] },
];
- decodedToken.iss = 'issuer';
- derivationStore.get.mockImplementation(async (id) => 'issuer');
+ derivationStore.get.mockImplementation(async (id) => issuer);
await expect(verifier.verify(credential)).resolves.toEqual({
['urn:solidlab:uma:claims:types:access']: [
{ resource_id: 'id1', resource_scopes: [ 'urn:knows:uma:scopes:derivation-read' ] },
@@ -143,10 +151,9 @@ describe('OidcVerifier', (): void => {
decodedToken.permissions = [
{ resource_id: 'id1', resource_scopes: [ 'scope1', 'urn:knows:uma:scopes:derivation-read' ] },
];
- decodedToken.iss = 'wrong-issuer';
- derivationStore.get.mockImplementation(async (id) => 'issuer');
+ derivationStore.get.mockImplementation(async (id) => 'other-issuer');
await expect(verifier.verify(credential)).rejects
- .toThrow('Invalid issuer for id1, expected issuer but got wrong-issuer');
+ .toThrow('Invalid issuer for id1, expected other-issuer but got http://example.org/issuer');
});
});
});
diff --git a/packages/uma/test/unit/credentials/verify/VcVerifier.test.ts b/packages/uma/test/unit/credentials/verify/VcVerifier.test.ts
new file mode 100644
index 00000000..71560fc3
--- /dev/null
+++ b/packages/uma/test/unit/credentials/verify/VcVerifier.test.ts
@@ -0,0 +1,97 @@
+import * as jose from 'jose';
+import { Credential } from '../../../../src/credentials/Credential';
+import { VcVerifier } from '../../../../src/credentials/verify/VcVerifier';
+
+vi.mock('jose', () => ({
+ createRemoteJWKSet: vi.fn(),
+ decodeJwt: vi.fn(),
+ jwtVerify: vi.fn(),
+}));
+
+describe('VcVerifier', (): void => {
+ const issuer = 'http://example.org/issuer';
+ let credential: Credential;
+ let decodedToken: any;
+ const remoteKeySet = 'remoteKeySet';
+
+ const decodeJwt = vi.spyOn(jose, 'decodeJwt');
+ const jwtVerify = vi.spyOn(jose, 'jwtVerify');
+ const createRemoteJWKSet = vi.spyOn(jose, 'createRemoteJWKSet');
+ const fetchMock = vi.spyOn(global, 'fetch');
+
+ let verifier: VcVerifier;
+
+ beforeEach(async(): Promise => {
+ credential = {
+ format: 'application/vc+jwt',
+ token: 'token',
+ };
+
+ decodedToken = {
+ iss: issuer,
+ vc: {
+ issuanceDate: new Date(Date.now() - 5000).toISOString(),
+ expirationDate: new Date(Date.now() + 5000).toISOString(),
+ credentialSubject: { garden: { fruit: 'apple' } },
+ }
+ };
+
+ vi.clearAllMocks();
+ fetchMock.mockResolvedValue({
+ status: 200,
+ json: vi.fn().mockResolvedValue({
+ issuer,
+ jwks_uri: `${issuer}/jwks_uri`
+ }),
+ } as any);
+ decodeJwt.mockReturnValue(decodedToken);
+ jwtVerify.mockResolvedValue({ payload: decodedToken } as any);
+ createRemoteJWKSet.mockReturnValue(remoteKeySet as any);
+
+ verifier = new VcVerifier();
+ });
+
+ it('errors on non-VC credentials.', async(): Promise => {
+ await expect(verifier.verify({ format: 'wrong', token: 'token' })).rejects
+ .toThrow(`Token format wrong does not match this processor's format.`);
+ });
+
+ it('errors if the token is missing the issuer claim.', async(): Promise => {
+ decodedToken.iss = undefined;
+
+ await expect(verifier.verify(credential)).rejects
+ .toThrow('Token is missing the issuer claim.');
+ });
+
+ it('errors if the token is missing the vc claim.', async(): Promise => {
+ decodedToken.vc = undefined;
+
+ await expect(verifier.verify(credential)).rejects
+ .toThrow('Token is missing the vc claim.');
+ });
+
+ it('errors if the VC is not yet valid.', async(): Promise => {
+ decodedToken.vc.issuanceDate = new Date(Date.now() + 5000).toISOString();
+
+ await expect(verifier.verify(credential))
+ .rejects.toThrow(`VC is not yet valid, issued at ${decodedToken.vc.issuanceDate}.`);
+ });
+
+ it('errors if the VC is expired.', async(): Promise => {
+ decodedToken.vc.expirationDate = new Date(Date.now() - 5000).toISOString();
+
+ await expect(verifier.verify(credential)).rejects.toThrow(`VC expired at ${decodedToken.vc.expirationDate}.`);
+ });
+
+ it('errors if there is no credentialSubject.', async(): Promise => {
+ decodedToken.vc.credentialSubject = undefined;
+
+ await expect(verifier.verify(credential)).rejects.toThrow('VC is missing the credentialSubject claim.');
+ });
+
+ it('returns the VC as claim.', async(): Promise => {
+ await expect(verifier.verify(credential)).resolves.toEqual({
+ ['urn:solidlab:uma:claims:types:vc']: decodedToken.vc,
+ });
+ });
+});
diff --git a/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts b/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts
index 2385dd29..0d584b26 100644
--- a/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts
+++ b/packages/uma/test/unit/policies/authorizers/OdrlAuthorizer.test.ts
@@ -94,7 +94,8 @@ describe('OdrlAuthorizer', (): void => {
expect(evaluate).toHaveBeenCalledTimes(1);
expect(evaluate).toHaveBeenLastCalledWith(
policyStore.getQuads(null, null, null, null),
- [ ...new Store(requestQuads) ],
+ // Also contains sotw data generated by claims being present
+ expect.any(Array),
sotw,
);
});
@@ -123,7 +124,7 @@ describe('OdrlAuthorizer', (): void => {
]);
});
- it('adds purpose claim context using odrl:purpose', async(): Promise => {
+ it('adds other claims to constraints', async(): Promise => {
const claims = { [PURPOSE]: 'https://w3id.org/dpv#ScientificResearch' };
const query: Permission[] = [{ resource_id: 'rid', resource_scopes: [ 'urn:example:css:modes:read' ] }];
diff --git a/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts b/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts
index 67b29622..77b3afbe 100644
--- a/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts
+++ b/packages/uma/test/unit/policies/authorizers/SimpleOdrlAuthorizer.test.ts
@@ -3,11 +3,13 @@ import { DataFactory as DF, Store } from 'n3';
import { randomUUID } from 'node:crypto';
import { ODRL } from 'odrl-evaluator';
import { Mocked } from 'vitest';
+import { CLIENTID, PURPOSE, VC, WEBID } from '../../../../src/credentials/Claims';
+import { ClaimSet } from '../../../../src/credentials/ClaimSet';
import { Authorizer } from '../../../../src/policies/authorizers/Authorizer';
import { SimpleOdrlAuthorizer } from '../../../../src/policies/authorizers/SimpleOdrlAuthorizer';
import { UCRulesStorage } from '../../../../src/ucp/storage/UCRulesStorage';
+import { OVC } from '../../../../src/ucp/util/Vocabularies';
import { Permission } from '../../../../src/views/Permission';
-import { WEBID, CLIENTID, PURPOSE } from '../../../../src/credentials/Claims';
describe('SimpleOdrlAuthorizer', () => {
const resource = 'res';
@@ -76,14 +78,12 @@ describe('SimpleOdrlAuthorizer', () => {
});
it('delegates to fallback if no query is provided', async () => {
- const result = await authorizer.permissions({});
- expect(result).toEqual(fallbackPermissions);
+ await expect(authorizer.permissions({})).resolves.toEqual(fallbackPermissions);
expect(fallback.permissions).toHaveBeenCalledWith({}, undefined);
});
it('returns empty if no rules match the resource', async () => {
- const result = await authorizer.permissions({}, query);
- expect(result).toEqual([]);
+ await expect(authorizer.permissions({}, query)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -91,18 +91,16 @@ describe('SimpleOdrlAuthorizer', () => {
addRule({ assignee: 'user' });
const claims = { [WEBID]: 'user' };
- const result = await authorizer.permissions(claims, query);
-
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
+ await expect(authorizer.permissions(claims, query))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
it('returns permission for public access (no assignee)', async () => {
addRule({});
- const result = await authorizer.permissions({}, query);
-
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
+ await expect(authorizer.permissions({}, query))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -110,27 +108,21 @@ describe('SimpleOdrlAuthorizer', () => {
addRule({ assignee: 'other' });
const claims = { [WEBID]: 'user' };
- const result = await authorizer.permissions(claims, query);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions(claims, query)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
it('returns empty if rule is a prohibition', async () => {
addRule({ linkPredicate: ODRL.terms.prohibition });
- const result = await authorizer.permissions({}, query);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions({}, query)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
it('delegates to fallback if rule has unsupported type', async () => {
addRule({ linkPredicate: DF.namedNode('unsupported') });
- const result = await authorizer.permissions({}, query);
-
- expect(result).toEqual(fallbackPermissions);
+ await expect(authorizer.permissions({}, query)).resolves.toEqual(fallbackPermissions);
expect(fallback.permissions).toHaveBeenCalledWith({}, query);
});
@@ -144,9 +136,7 @@ describe('SimpleOdrlAuthorizer', () => {
});
const claims = { [CLIENTID]: 'clientB' };
- const result = await authorizer.permissions(claims, query);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions(claims, query)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -160,13 +150,12 @@ describe('SimpleOdrlAuthorizer', () => {
});
const claims = { [CLIENTID]: 'clientA' };
- const result = await authorizer.permissions(claims, query);
-
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
+ await expect(authorizer.permissions(claims, query))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
- it('returns permission if purpose constraint is satisfied', async () => {
+ it('returns permission if other constraints are satisfied', async () => {
const rule = addRule({});
addConstraint({
rule,
@@ -176,13 +165,12 @@ describe('SimpleOdrlAuthorizer', () => {
});
const claims = { [PURPOSE]: 'https://w3id.org/dpv#ScientificResearch' };
- const result = await authorizer.permissions(claims, query);
-
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
+ await expect(authorizer.permissions(claims, query))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
- it('returns empty if claim constraint is not satisfied', async () => {
+ it('returns empty if other constraints are not satisfied', async () => {
const rule = addRule({});
addConstraint({
rule,
@@ -192,9 +180,7 @@ describe('SimpleOdrlAuthorizer', () => {
});
const claims = { [PURPOSE]: 'http://example.com/purpose-b' };
- const result = await authorizer.permissions(claims, query);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions(claims, query)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -202,9 +188,7 @@ describe('SimpleOdrlAuthorizer', () => {
const rule = addRule({});
store.addQuad(rule, ODRL.terms.constraint, DF.namedNode('constraint3'));
- const result = await authorizer.permissions({}, query);
-
- expect(result).toEqual(fallbackPermissions);
+ await expect(authorizer.permissions({}, query)).resolves.toEqual(fallbackPermissions);
expect(fallback.permissions).toHaveBeenCalledWith({}, query);
});
@@ -217,9 +201,7 @@ describe('SimpleOdrlAuthorizer', () => {
rightOperand: new Date(Date.now() + 1000000).toISOString(),
});
- const result = await authorizer.permissions({}, query);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions({}, query)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -232,9 +214,74 @@ describe('SimpleOdrlAuthorizer', () => {
rightOperand: new Date(Date.now() + 1000000).toISOString(),
});
- const result = await authorizer.permissions({}, query);
+ await expect(authorizer.permissions({}, query))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
+ expect(fallback.permissions).not.toHaveBeenCalled();
+ });
+
+ it('delegates to fallback if OVC constraint is too complex', async () => {
+ const rule = addRule({});
+ store.addQuad(rule, OVC.terms.constraint, DF.namedNode('constraint3'));
+
+ await expect(authorizer.permissions({}, query)).resolves.toEqual(fallbackPermissions);
+ expect(fallback.permissions).toHaveBeenCalledWith({}, query);
+ });
+
+ it('returns empty if there is an OVC constraint but no VC claim.', async(): Promise => {
+ const rule = addRule({});
+
+ const jsonPath = '$.credentialSubject.garden.fruit';
+ const constraint = DF.namedNode(`ovc-constraint-${randomUUID()}`);
+ store.addQuad(rule, OVC.terms.constraint, constraint);
+ store.addQuad(constraint, OVC.terms.leftOperand, DF.namedNode(jsonPath));
+ store.addQuad(constraint, ODRL.terms.operator, ODRL.terms.eq);
+ store.addQuad(constraint, ODRL.terms.rightOperand, DF.literal('apple'));
+
+ await expect(authorizer.permissions({}, query)).resolves.toEqual([]);
+ expect(fallback.permissions).not.toHaveBeenCalled();
+ });
+
+ it('returns permissions if the OVC constraint is satisfied.', async(): Promise => {
+ const rule = addRule({});
+
+ const jsonPath = '$.credentialSubject.garden.fruit';
+ const constraint = DF.namedNode(`ovc-constraint-${randomUUID()}`);
+ store.addQuad(rule, OVC.terms.constraint, constraint);
+ store.addQuad(constraint, OVC.terms.leftOperand, DF.namedNode(jsonPath));
+ store.addQuad(constraint, ODRL.terms.operator, ODRL.terms.eq);
+ store.addQuad(constraint, ODRL.terms.rightOperand, DF.literal('apple'));
+ store.addQuad(constraint, OVC.terms.credentialSubjectType, DF.namedNode('http://example.com/type'));
+
+ const claims = { [VC]: {
+ type: [ 'http://example.com/type' ],
+ credentialSubject: { garden: { fruit: 'apple' }}
+ }};
+ await expect(authorizer.permissions(claims, query))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
+ expect(fallback.permissions).not.toHaveBeenCalled();
+ });
+
+ it('returns empty if the OVC constraint is not satisfied.', async(): Promise => {
+ const rule = addRule({});
+
+ const jsonPath = '$.credentialSubject.garden.fruit';
+ const constraint = DF.namedNode(`ovc-constraint-${randomUUID()}`);
+ store.addQuad(rule, OVC.terms.constraint, constraint);
+ store.addQuad(constraint, OVC.terms.leftOperand, DF.namedNode(jsonPath));
+ store.addQuad(constraint, ODRL.terms.operator, ODRL.terms.eq);
+ store.addQuad(constraint, ODRL.terms.rightOperand, DF.literal('apple'));
+ store.addQuad(constraint, OVC.terms.credentialSubjectType, DF.namedNode('http://example.com/type'));
+
+ let claims: ClaimSet = { [VC]: {
+ credentialSubject: { garden: { fruit: 'apple' }}
+ }};
+ await expect(authorizer.permissions(claims, query)).resolves.toEqual([]);
+ claims = { [VC]: {
+ type: [ 'http://example.com/type' ],
+ credentialSubject: { garden: { fruit: 'pear' }}
+ }};
+ await expect(authorizer.permissions(claims, query)).resolves.toEqual([]);
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [scope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -249,9 +296,7 @@ describe('SimpleOdrlAuthorizer', () => {
addRule({});
addRule({ target: resource2, action: odrlScope2 });
- const result = await authorizer.permissions({}, multiQuery);
-
- expect(result).toEqual([
+ await expect(authorizer.permissions({}, multiQuery)).resolves.toEqual([
{ resource_id: resource, resource_scopes: [scope] },
{ resource_id: resource2, resource_scopes: [scope2] },
]);
@@ -268,9 +313,7 @@ describe('SimpleOdrlAuthorizer', () => {
// rule for resource2 has an unsupported link predicate, triggering fallback
addRule({ target: resource2, linkPredicate: DF.namedNode('unsupported') });
- const result = await authorizer.permissions({}, multiQuery);
-
- expect(result).toEqual(fallbackPermissions);
+ await expect(authorizer.permissions({}, multiQuery)).resolves.toEqual(fallbackPermissions);
expect(fallback.permissions).toHaveBeenCalledWith({}, multiQuery);
});
@@ -279,9 +322,8 @@ describe('SimpleOdrlAuthorizer', () => {
const writeQuery: Permission[] = [{ resource_id: resource, resource_scopes: [writeScope] }];
addRule({ action: 'http://www.w3.org/ns/odrl/2/modify' });
- const result = await authorizer.permissions({}, writeQuery);
-
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [writeScope] }]);
+ await expect(authorizer.permissions({}, writeQuery))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [writeScope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -290,9 +332,8 @@ describe('SimpleOdrlAuthorizer', () => {
const appendQuery: Permission[] = [{ resource_id: resource, resource_scopes: [appendScope] }];
addRule({ action: 'http://www.w3.org/ns/odrl/2/modify' });
- const result = await authorizer.permissions({}, appendQuery);
-
- expect(result).toEqual([{ resource_id: resource, resource_scopes: [appendScope] }]);
+ await expect(authorizer.permissions({}, appendQuery))
+ .resolves.toEqual([{ resource_id: resource, resource_scopes: [appendScope] }]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
@@ -301,20 +342,15 @@ describe('SimpleOdrlAuthorizer', () => {
const writeQuery: Permission[] = [{ resource_id: resource, resource_scopes: [writeScope] }];
addRule({ action: 'http://www.w3.org/ns/odrl/2/append' });
- const result = await authorizer.permissions({}, writeQuery);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions({}, writeQuery)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
it('does not grant odrl:modify scope when rule only has odrl:write action', async () => {
- const modifyScope = 'urn:example:css:modes:write';
const rawModifyQuery: Permission[] = [{ resource_id: resource, resource_scopes: ['http://www.w3.org/ns/odrl/2/modify'] }];
addRule({ action: 'http://www.w3.org/ns/odrl/2/write' });
- const result = await authorizer.permissions({}, rawModifyQuery);
-
- expect(result).toEqual([]);
+ await expect(authorizer.permissions({}, rawModifyQuery)).resolves.toEqual([]);
expect(fallback.permissions).not.toHaveBeenCalled();
});
});
diff --git a/packages/uma/test/unit/util/HttpMessageSignatures.test.ts b/packages/uma/test/unit/util/HttpMessageSignatures.test.ts
deleted file mode 100644
index 6809e1ec..00000000
--- a/packages/uma/test/unit/util/HttpMessageSignatures.test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { AlgJwk } from '@solid/community-server';
-import { httpbis } from 'http-message-signatures';
-import { exportJWK, generateKeyPair, GenerateKeyPairResult } from 'jose';
-import crypto from 'node:crypto';
-import { beforeAll } from 'vitest';
-import { signRequest } from '../../../src/util/HttpMessageSignatures';
-
-describe('HttpMessageSignatures', (): void => {
- const url = 'https://example.com/foo';
- const alg = 'ES256';
- let keys: GenerateKeyPairResult;
- let publicKey: AlgJwk;
- let privateKey: AlgJwk;
-
- beforeAll(async(): Promise => {
- keys = await generateKeyPair(alg);
- publicKey = { ...await exportJWK(keys.publicKey), alg, kid: 'public' };
- privateKey = { ...await exportJWK(keys.privateKey), alg, kid: 'private' };
- });
-
- describe('#signRequest', (): void => {
- it('adds the signature headers to the request.', async(): Promise => {
- const request = { method: 'GET', headers: { accept: 'text/plain' }, body: 'text' };
- const signedRequest = await signRequest(url, request, privateKey);
- expect(signedRequest).toMatchObject(request);
- expect(signedRequest.headers['Signature-Input']).includes('sig=("@target-uri" "@method")');
- const verified = await httpbis.verifyMessage({
- keyLookup: async() => ({
- async verify(data: Buffer, signature: Buffer) {
- const params = { name: 'ECDSA', hash: 'SHA-256', namedCurve: 'P-256' };
- const key = await crypto.subtle.importKey('jwk', publicKey, params, false, ['verify']);
- return await crypto.subtle.verify(params, key, signature, data);
- },
- })},
- signedRequest,
- );
- expect(verified).toBe(true);
- });
- });
-});
diff --git a/packages/uma/test/unit/util/JwtUtil.test.ts b/packages/uma/test/unit/util/JwtUtil.test.ts
new file mode 100644
index 00000000..c4647e5f
--- /dev/null
+++ b/packages/uma/test/unit/util/JwtUtil.test.ts
@@ -0,0 +1,74 @@
+import * as jose from 'jose';
+import { getJwks, jwksRecord } from '../../../src/util/JwtUtil';
+
+vi.mock('jose', () => ({
+ createRemoteJWKSet: vi.fn(),
+ decodeJwt: vi.fn(),
+ jwtVerify: vi.fn(),
+}));
+
+describe('JwtUtil', (): void => {
+ const issuer = 'http://example.org/issuer';
+ const remoteKeySet = 'remoteKeySet';
+ const fetchMock = vi.spyOn(global, 'fetch');
+ const createRemoteJWKSet = vi.spyOn(jose, 'createRemoteJWKSet');
+
+ beforeEach(async(): Promise => {
+ vi.clearAllMocks();
+ delete jwksRecord[issuer];
+
+ fetchMock.mockResolvedValue({
+ status: 200,
+ json: vi.fn().mockResolvedValue({
+ issuer,
+ jwks_uri: `${issuer}/jwks_uri`
+ }),
+ } as any);
+
+ createRemoteJWKSet.mockReturnValue(remoteKeySet as any);
+ });
+
+ describe('#getJwks', (): void => {
+ it('returns a JWKSet for the given issuer.', async(): Promise => {
+ await expect(getJwks(issuer)).resolves.toBe(remoteKeySet);
+ expect(fetchMock).toHaveBeenCalledExactlyOnceWith('http://example.org/issuer/.well-known/openid-configuration');
+ expect(createRemoteJWKSet).toHaveBeenCalledExactlyOnceWith(new URL('http://example.org/issuer/jwks_uri'));
+ });
+
+ it('errors if the OpenID config cannot be fetched.', async(): Promise => {
+ fetchMock.mockResolvedValue({ status: 500 } as any);
+
+ await expect(getJwks(issuer)).rejects.toThrow('Unable to access http://example.org/issuer/.well-known/openid-configuration');
+ });
+
+ it('caches results.', async(): Promise => {
+ await expect(getJwks(issuer)).resolves.toBe(remoteKeySet);
+ fetchMock.mockResolvedValue({ status: 500 } as any);
+ await expect(getJwks(issuer)).resolves.toBe(remoteKeySet);
+ expect(fetchMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('errors if there is an issuer mismatch.', async(): Promise => {
+ fetchMock.mockResolvedValue({
+ status: 200,
+ json: vi.fn().mockResolvedValue({
+ issuer: 'http://example.org/other-issuer',
+ jwks_uri: `${issuer}/jwks_uri`
+ }),
+ } as any);
+
+ await expect(getJwks(issuer)).rejects.toThrow(`Issuer mismatch: expected http://example.org/issuer, got http://example.org/other-issuer`);
+ });
+
+ it('errors if there is no jwks_uri in the OpenID configuration.', async(): Promise => {
+ fetchMock.mockResolvedValue({
+ status: 200,
+ json: vi.fn().mockResolvedValue({
+ issuer,
+ }),
+ } as any);
+
+ await expect(getJwks(issuer)).rejects.toThrow(`Missing jwks_uri from http://example.org/issuer/.well-known/openid-configuration`);
+ });
+ });
+});
diff --git a/packages/uma/test/unit/util/http/validate/HttpMessageValidator.test.ts b/packages/uma/test/unit/util/http/validate/HttpMessageValidator.test.ts
deleted file mode 100644
index 1a998b7b..00000000
--- a/packages/uma/test/unit/util/http/validate/HttpMessageValidator.test.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { AlgJwk, UnauthorizedHttpError } from '@solid/community-server';
-import { exportJWK, generateKeyPair, GenerateKeyPairResult } from 'jose';
-import { beforeAll, Mock } from 'vitest';
-import { HttpHandlerRequest } from '../../../../../src/util/http/models/HttpHandler';
-import { HttpMessageValidator } from '../../../../../src/util/http/validate/HttpMessageValidator';
-import { signRequest } from '../../../../../src/util/HttpMessageSignatures';
-
-vi.mock('get-jwks');
-
-describe('HttpMessageValidator', (): void => {
- const url = 'https://example.com/foo';
- const alg = 'ES256';
- let keys: GenerateKeyPairResult;
- let publicKey: AlgJwk;
- let privateKey: AlgJwk;
- let getJwk: Mock<() => unknown>;
- let request: HttpHandlerRequest;
-
- const validator = new HttpMessageValidator();
-
- beforeAll(async(): Promise => {
- keys = await generateKeyPair(alg);
- publicKey = { ...await exportJWK(keys.publicKey), alg, kid: 'public' };
- privateKey = { ...await exportJWK(keys.privateKey), alg, kid: 'private' };
-
- const getJwks = await import('get-jwks');
- getJwk = vi.fn().mockResolvedValue(publicKey);
- (getJwks.default as unknown as Mock).mockReturnValue({ getJwk } as any);
-
- const baseRequest = {
- method: 'POST',
- headers: { authorization: 'HttpSig cred="https://example.com/bar"' },
- body: 'text'
- };
- const signedRequest = await signRequest(url, baseRequest, privateKey);
- request = {
- url: new URL(url),
- method: signedRequest.method,
- headers: signedRequest.headers as any,
- parameters: {},
- }
- });
-
- it('returns the signer as the owner.', async(): Promise => {
- await expect(validator.handle({ request })).resolves.toEqual({ owner: 'https://example.com/bar' });
- expect(getJwk).toHaveBeenCalledTimes(1);
- expect(getJwk).toHaveBeenLastCalledWith({
- domain: 'https://example.com/bar',
- alg,
- kid: 'private',
- });
- });
-
- it('errors if the authorization header is missing.', async(): Promise => {
- request.headers = {};
- await expect(validator.handle({ request })).rejects.toThrow('Missing authorization header in request.');
- });
-
- it('errors if the authorization scheme is not HttpSig.', async(): Promise => {
- request.headers.authorization = 'Basic 123';
- await expect(validator.handle({ request })).rejects.toThrow(UnauthorizedHttpError);
- });
-
- it('errors if no `cred` parameter could be extracted from the header.', async(): Promise => {
- request.headers.authorization = 'HttpSig pear';
- await expect(validator.handle({ request })).rejects.toThrow(UnauthorizedHttpError);
- });
-
- it('errors if the signature used a wrong key.', async(): Promise => {
- keys = await generateKeyPair(alg);
- const otherKey = { ...await exportJWK(keys.privateKey), alg, kid: 'private' };
- const baseRequest = {
- method: 'POST',
- headers: { authorization: 'HttpSig cred="https://example.com/bar"' },
- body: 'text'
- };
- const signedRequest = await signRequest(url, baseRequest, otherKey as AlgJwk);
- request = {
- url: new URL(url),
- method: signedRequest.method,
- headers: signedRequest.headers as any,
- parameters: {},
- }
- await expect(validator.handle({ request })).rejects.toThrow('Failed to verify signature');
- });
-});
diff --git a/test/integration/Demo.test.ts b/test/integration/Demo.test.ts
index 6b88bf06..45002e3b 100644
--- a/test/integration/Demo.test.ts
+++ b/test/integration/Demo.test.ts
@@ -1,9 +1,9 @@
import { App } from '@solid/community-server';
import { setGlobalLoggerFactory, WinstonLoggerFactory } from 'global-logger-factory';
-import { Parser, Store } from 'n3';
+import { UnsecuredJWT } from 'jose';
import * as path from 'node:path';
import { getDefaultCssVariables, getPorts, instantiateFromConfig } from '../util/ServerUtil';
-import { findTokenEndpoint, getToken, noTokenFetch, generateCredentials, tokenFetch, umaFetch } from '../util/UmaUtil';
+import { findTokenEndpoint, generateCredentials, getToken, noTokenFetch, tokenFetch, umaFetch } from '../util/UmaUtil';
const [ cssPort, umaPort ] = getPorts('Demo');
@@ -142,69 +142,40 @@ _:rename a solid:InsertDeletePatch;
}.`,
}, terms.agents.ruben);
expect(response.status).toBe(205);
-
- // TODO: Do I need this though
- // Add necessary triples to WebID
- response = await fetch(terms.agents.ruben, {
- method: 'PATCH',
- headers: { 'content-type': 'text/n3' },
- body: `
-@prefix solid: .
-
-_:rename a solid:InsertDeletePatch;
- solid:inserts {
- <${terms.agents.ruben}> solid:umaServer
- }.`,
- });
- expect(response.status).toBe(205);
- });
-
- it('finds the UMA server of the user in their WebID.', async(): Promise => {
- // TODO: what is the point of any of this? the as_uri response should have this data?
- // TODO: find out why it doesn't work though as the term does get added at the end of the previous test
- const response = await fetch(terms.agents.ruben, {
- headers: { 'accept': 'text/turtle' },
- });
- expect(response.status).toBe(200);
- const parser = new Parser({ baseIRI: terms.agents.ruben });
- const store = new Store(parser.parse(await response.text()));
- expect(store.countQuads(terms.agents.ruben, terms.solid.umaServer, null, null)).toBe(1);
- const umaServer = store.getObjects(terms.agents.ruben, terms.solid.umaServer, null)[0].value;
});
it('can add a healthcare policy to the server.', async(): Promise => {
- // TODO: policy currently not linking to constraints as these need to be added to the ODRL evaluator
- // odrl:constraint ,
- // .
const healthcare_patient_policy =
`PREFIX dcterms:
-PREFIX eu-gdpr:
-PREFIX oac:
-PREFIX odrl:
-PREFIX xsd:
-
-PREFIX ex:
-
- a odrl:Agreement ;
- odrl:uid ex:HCPX-agreement ;
- odrl:profile oac: ;
- odrl:permission .
-
- a odrl:Permission ;
- odrl:action odrl:read ;
- odrl:target <${terms.resources.smartwatch}> ;
- odrl:assigner <${terms.agents.ruben}> ;
- odrl:assignee <${terms.agents.alice}> .
-
- a odrl:Constraint ;
- odrl:leftOperand odrl:purpose ; # can also be oac:Purpose, to conform with OAC profile
- odrl:operator odrl:eq ;
- odrl:rightOperand ex:bariatric-care .
-
- a odrl:Constraint ;
- odrl:leftOperand oac:LegalBasis ;
- odrl:operator odrl:eq ;
- odrl:rightOperand eu-gdpr:A9-2-a .`
+ PREFIX eu-gdpr:
+ PREFIX oac:
+ PREFIX odrl:
+ PREFIX xsd:
+
+ PREFIX ex:
+
+ a odrl:Agreement ;
+ odrl:uid ex:HCPX-agreement ;
+ odrl:profile oac: ;
+ odrl:permission .
+
+ a odrl:Permission ;
+ odrl:action odrl:read ;
+ odrl:target <${terms.resources.smartwatch}> ;
+ odrl:assigner <${terms.agents.ruben}> ;
+ odrl:assignee <${terms.agents.alice}> ;
+ odrl:constraint ,
+ .
+
+ a odrl:Constraint ;
+ odrl:leftOperand odrl:purpose ;
+ odrl:operator odrl:eq ;
+ odrl:rightOperand ex:bariatric-care .
+
+ a odrl:Constraint ;
+ odrl:leftOperand oac:LegalBasis ;
+ odrl:operator odrl:eq ;
+ odrl:rightOperand eu-gdpr:A9-2-a .`
const medicalPolicyCreationResponse = await fetch(policyContainer, {
method: 'POST',
@@ -215,15 +186,25 @@ PREFIX ex:
});
it('requires authorized access for patient data.', async(): Promise => {
- // TODO: should do the steps individually here so we can check the contents of the tokens/tickets
// Parse ticket and UMA server URL from header
const parsedHeader = await noTokenFetch(terms.resources.smartwatch);
// Find UMA server token endpoint
const tokenEndpoint = await findTokenEndpoint(parsedHeader.as_uri);
+ const jwt = new UnsecuredJWT({
+ 'http://www.w3.org/ns/odrl/2/purpose': 'http://example.org/bariatric-care',
+ 'urn:solidlab:uma:claims:types:webid': terms.agents.alice,
+ 'https://w3id.org/oac#LegalBasis': 'https://w3id.org/dpv/legal/eu/gdpr#A9-2-a'
+ }).encode();
+
// Send ticket request to UMA server and extract token from response
- const token = await getToken(parsedHeader.ticket, tokenEndpoint, terms.agents.alice);
+ const token = await getToken(
+ parsedHeader.ticket,
+ tokenEndpoint,
+ undefined,
+ undefined,
+ [{ claim_token: jwt, claim_token_format: 'urn:solidlab:uma:claims:formats:jwt' }]);
const accessToken = JSON.parse(Buffer.from(token.access_token.split('.')[1], 'base64').toString());
expect(accessToken).toMatchObject({
permissions:[{
@@ -235,7 +216,7 @@ PREFIX ex:
aud: 'solid',
exp: expect.any(Number),
jti: expect.any(String),
- })
+ });
// Perform new call with token
const response = await tokenFetch(token, terms.resources.smartwatch);
diff --git a/test/integration/Oidc.test.ts b/test/integration/Oidc.test.ts
index f8521d2b..42ad7f68 100644
--- a/test/integration/Oidc.test.ts
+++ b/test/integration/Oidc.test.ts
@@ -73,7 +73,7 @@ describe('A server supporting OIDC tokens', (): void => {
return;
}
if (req.url!.endsWith('/.well-known/openid-configuration')) {
- res.end(JSON.stringify({ jwks_uri: idpUrl }));
+ res.end(JSON.stringify({ jwks_uri: idpUrl, issuer: idpUrl }));
return;
}
// Exposing private keys is fine right
@@ -216,7 +216,6 @@ describe('A server supporting OIDC tokens', (): void => {
});
const endpoint = await findTokenEndpoint(as_uri);
- // TODO: also add token that fails
const jwk = await importJWK(privateKey, privateKey.alg);
const jwt = await new SignJWT({ azp: client })
.setSubject(sub)
@@ -389,4 +388,90 @@ describe('A server supporting OIDC tokens', (): void => {
expect(response.status).toBe(204);
});
});
+
+ describe('accessing a resource using a VC.', (): void => {
+ const resource = `http://localhost:${cssPort}/alice/vc`;
+ const policy = `
+ @prefix ex: .
+ @prefix odrl: .
+ @prefix ovc: .
+
+ ex:policyVc a odrl:Set;
+ odrl:uid ex:policyVc ;
+ odrl:permission ex:permissionVc .
+
+ ex:permissionVc a odrl:Permission ;
+ odrl:assigner <${webId}>;
+ odrl:action odrl:read , odrl:create , odrl:modify ;
+ odrl:target ;
+ ovc:constraint ex:constraintVc .
+
+ ex:constraintVc a odrl:Constraint ;
+ ovc:leftOperand "$.credentialSubject['kss:HCPs']['kss:assignedPatientId']" ;
+ odrl:operator odrl:eq ;
+ odrl:rightOperand ;
+ ovc:credentialSubjectType .`;
+
+ it('can set up the policy.', async(): Promise => {
+ const response = await fetch(policyEndpoint, {
+ method: 'POST',
+ headers: { authorization: `WebID ${encodeURIComponent(webId)}`, 'content-type': 'text/turtle' },
+ body: policy,
+ });
+ expect(response.status).toBe(201);
+ });
+
+ it('can get an access token.', async(): Promise => {
+ const { as_uri, ticket } = await noTokenFetch(resource, {
+ method: 'PUT',
+ headers: { 'content-type': 'text/plain' },
+ body: 'hello',
+ });
+ const endpoint = await findTokenEndpoint(as_uri);
+
+ const jwk = await importJWK(privateKey, privateKey.alg);
+ const jwt = await new SignJWT({
+ vc: {
+ type: [
+ 'VerifiableCredential',
+ 'http://example.org/UserHcpRelationVC',
+ ],
+ issuanceDate: new Date(Date.now() - 5000).toISOString(),
+ expirationDate: new Date(Date.now() + 5000).toISOString(),
+ credentialSubject: {
+ 'kss:HCPs': {
+ 'kss:assignedPatientId': 'http://example.org/patient-id',
+ },
+ },
+ },
+ }).setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid })
+ .setIssuedAt()
+ .setIssuer(idpUrl)
+ .setAudience(`http://localhost:${umaPort}/uma`)
+ .setJti(randomUUID())
+ .sign(jwk);
+
+ const content: Record = {
+ grant_type: 'urn:ietf:params:oauth:grant-type:uma-ticket',
+ ticket: ticket,
+ claim_token: jwt,
+ claim_token_format: 'application/vc+jwt',
+ };
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify(content),
+ });
+ expect(response.status).toBe(200);
+ });
+
+ it('can remove the policy.', async(): Promise => {
+ const response = await fetch(joinUrl(policyEndpoint, encodeURIComponent('http://example.org/policyVc')), {
+ method: 'DELETE',
+ headers: { authorization: `WebID ${encodeURIComponent(webId)}`, 'content-type': 'text/turtle' },
+ });
+ expect(response.status).toBe(204);
+ });
+ });
});
diff --git a/yarn.lock b/yarn.lock
index 9a7bb991..c0373ff3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5668,14 +5668,15 @@ __metadata:
"@httpland/authorization-parser": "npm:^1.1.0"
"@solid/access-token-verifier": "npm:^1.2.0"
"@solid/community-server": "npm:^8.0.0-alpha.1"
+ "@types/jsonpath": "npm:^0.2.4"
"@types/ms": "npm:^2.1.0"
"@types/n3": "npm:^1.16.4"
asynchronous-handlers: "npm:^1.0.2"
componentsjs: "npm:^6.3.0"
- get-jwks: "npm:^9.0.1"
global-logger-factory: "npm:^1.0.0"
http-message-signatures: "npm:^1.0.4"
jose: "npm:^5.2.2"
+ jsonpath: "npm:^1.3.0"
logform: "npm:^2.6.0"
ms: "npm:^2.1.3"
n3: "npm:^1.17.2"
@@ -5975,6 +5976,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/jsonpath@npm:^0.2.4":
+ version: 0.2.4
+ resolution: "@types/jsonpath@npm:0.2.4"
+ checksum: 10c0/3fdf725e5db61a0abb0afa95667e561a0e5f3604f30b609497af135606f4721449d7235ca8304da4984fb2ac4f6d290ad49de5ad6878740555542ddd28ee0b69
+ languageName: node
+ linkType: hard
+
"@types/keygrip@npm:*":
version: 1.0.6
resolution: "@types/keygrip@npm:1.0.6"
@@ -6900,18 +6908,6 @@ __metadata:
languageName: node
linkType: hard
-"asn1.js@npm:^5.3.0":
- version: 5.4.1
- resolution: "asn1.js@npm:5.4.1"
- dependencies:
- bn.js: "npm:^4.0.0"
- inherits: "npm:^2.0.1"
- minimalistic-assert: "npm:^1.0.0"
- safer-buffer: "npm:^2.1.0"
- checksum: 10c0/b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324
- languageName: node
- linkType: hard
-
"assertion-error@npm:^2.0.1":
version: 2.0.1
resolution: "assertion-error@npm:2.0.1"
@@ -7022,13 +7018,6 @@ __metadata:
languageName: node
linkType: hard
-"bn.js@npm:^4.0.0, bn.js@npm:^4.11.9":
- version: 4.12.0
- resolution: "bn.js@npm:4.12.0"
- checksum: 10c0/9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21
- languageName: node
- linkType: hard
-
"brace-expansion@npm:^1.1.7":
version: 1.1.11
resolution: "brace-expansion@npm:1.1.11"
@@ -7057,13 +7046,6 @@ __metadata:
languageName: node
linkType: hard
-"brorand@npm:^1.1.0":
- version: 1.1.0
- resolution: "brorand@npm:1.1.0"
- checksum: 10c0/6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571
- languageName: node
- linkType: hard
-
"buffer-from@npm:^1.0.0":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
@@ -8122,21 +8104,6 @@ __metadata:
languageName: node
linkType: hard
-"elliptic@npm:^6.5.4":
- version: 6.5.5
- resolution: "elliptic@npm:6.5.5"
- dependencies:
- bn.js: "npm:^4.11.9"
- brorand: "npm:^1.1.0"
- hash.js: "npm:^1.0.0"
- hmac-drbg: "npm:^1.0.1"
- inherits: "npm:^2.0.4"
- minimalistic-assert: "npm:^1.0.1"
- minimalistic-crypto-utils: "npm:^1.0.1"
- checksum: 10c0/3e591e93783a1b66f234ebf5bd3a8a9a8e063a75073a35a671e03e3b25253b6e33ac121f7efe9b8808890fffb17b40596cc19d01e6e8d1fa13b9a56ff65597c8
- languageName: node
- linkType: hard
-
"emoji-regex@npm:^10.3.0":
version: 10.4.0
resolution: "emoji-regex@npm:10.4.0"
@@ -8400,6 +8367,24 @@ __metadata:
languageName: node
linkType: hard
+"escodegen@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "escodegen@npm:2.1.0"
+ dependencies:
+ esprima: "npm:^4.0.1"
+ estraverse: "npm:^5.2.0"
+ esutils: "npm:^2.0.2"
+ source-map: "npm:~0.6.1"
+ dependenciesMeta:
+ source-map:
+ optional: true
+ bin:
+ escodegen: bin/escodegen.js
+ esgenerate: bin/esgenerate.js
+ checksum: 10c0/e1450a1f75f67d35c061bf0d60888b15f62ab63aef9df1901cffc81cffbbb9e8b3de237c5502cf8613a017c1df3a3003881307c78835a1ab54d8c8d2206e01d3
+ languageName: node
+ linkType: hard
+
"eslint-scope@npm:^5.1.1":
version: 5.1.1
resolution: "eslint-scope@npm:5.1.1"
@@ -8486,6 +8471,26 @@ __metadata:
languageName: node
linkType: hard
+"esprima@npm:1.2.5":
+ version: 1.2.5
+ resolution: "esprima@npm:1.2.5"
+ bin:
+ esparse: ./bin/esparse.js
+ esvalidate: ./bin/esvalidate.js
+ checksum: 10c0/634f272901b48174b84fd59ae6d9fbe03af38cfaa60501d8c0c7227d96ac53aef232e6fafda7c9760e50997f675e1c828e0b29aa39b092982960acf5e937db8f
+ languageName: node
+ linkType: hard
+
+"esprima@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "esprima@npm:4.0.1"
+ bin:
+ esparse: ./bin/esparse.js
+ esvalidate: ./bin/esvalidate.js
+ checksum: 10c0/ad4bab9ead0808cf56501750fd9d3fb276f6b105f987707d059005d57e182d18a7c9ec7f3a01794ebddcca676773e42ca48a32d67a250c9d35e009ca613caba3
+ languageName: node
+ linkType: hard
+
"esquery@npm:^1.4.2":
version: 1.5.0
resolution: "esquery@npm:1.5.0"
@@ -8979,17 +8984,6 @@ __metadata:
languageName: node
linkType: hard
-"get-jwks@npm:^9.0.1":
- version: 9.0.1
- resolution: "get-jwks@npm:9.0.1"
- dependencies:
- jwk-to-pem: "npm:^2.0.4"
- lru-cache: "npm:^10.0.0"
- node-fetch: "npm:^2.6.1"
- checksum: 10c0/3afc372721f71c19dfeba9d8c1c1215799d2940eb6d2f0b09cc6f1ef7d6ff5d1707b9ed42e231dbef1741f537ecca14691bba48426510966fee174f1fc427201
- languageName: node
- linkType: hard
-
"get-pkg-repo@npm:^4.2.1":
version: 4.2.1
resolution: "get-pkg-repo@npm:4.2.1"
@@ -9292,7 +9286,7 @@ __metadata:
languageName: node
linkType: hard
-"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3, hash.js@npm:^1.1.7":
+"hash.js@npm:^1.1.7":
version: 1.1.7
resolution: "hash.js@npm:1.1.7"
dependencies:
@@ -9320,17 +9314,6 @@ __metadata:
languageName: node
linkType: hard
-"hmac-drbg@npm:^1.0.1":
- version: 1.0.1
- resolution: "hmac-drbg@npm:1.0.1"
- dependencies:
- hash.js: "npm:^1.0.3"
- minimalistic-assert: "npm:^1.0.0"
- minimalistic-crypto-utils: "npm:^1.0.1"
- checksum: 10c0/f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d
- languageName: node
- linkType: hard
-
"hosted-git-info@npm:^2.1.4":
version: 2.8.9
resolution: "hosted-git-info@npm:2.8.9"
@@ -9594,7 +9577,7 @@ __metadata:
languageName: node
linkType: hard
-"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.3":
+"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.3, inherits@npm:~2.0.3":
version: 2.0.4
resolution: "inherits@npm:2.0.4"
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
@@ -10140,14 +10123,14 @@ __metadata:
languageName: node
linkType: hard
-"jwk-to-pem@npm:^2.0.4":
- version: 2.0.5
- resolution: "jwk-to-pem@npm:2.0.5"
+"jsonpath@npm:^1.3.0":
+ version: 1.3.0
+ resolution: "jsonpath@npm:1.3.0"
dependencies:
- asn1.js: "npm:^5.3.0"
- elliptic: "npm:^6.5.4"
- safe-buffer: "npm:^5.0.1"
- checksum: 10c0/307cacfbf4f38b4c4c77bcf3e578ef0d1d9536a9323e4f5be7e55e777d9df4fb5e89abbbfaf7c080b9f9da1241217f10391e4114c7a72cac66411d374427eeb9
+ esprima: "npm:1.2.5"
+ static-eval: "npm:2.1.1"
+ underscore: "npm:1.13.6"
+ checksum: 10c0/c7464919dd012837091febc393f4f743b82c422e3e8b4f2677119e7f8a37d8fd32f4969699c029fce3f31569a400b275e9ef089047a06548235089989be35bbe
languageName: node
linkType: hard
@@ -10624,20 +10607,13 @@ __metadata:
languageName: node
linkType: hard
-"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1":
+"minimalistic-assert@npm:^1.0.1":
version: 1.0.1
resolution: "minimalistic-assert@npm:1.0.1"
checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd
languageName: node
linkType: hard
-"minimalistic-crypto-utils@npm:^1.0.1":
- version: 1.0.1
- resolution: "minimalistic-crypto-utils@npm:1.0.1"
- checksum: 10c0/790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8
- languageName: node
- linkType: hard
-
"minimatch@npm:9.0.5, minimatch@npm:^9.0.4":
version: 9.0.5
resolution: "minimatch@npm:9.0.5"
@@ -10870,7 +10846,7 @@ __metadata:
languageName: node
linkType: hard
-"node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0":
+"node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0":
version: 2.7.0
resolution: "node-fetch@npm:2.7.0"
dependencies:
@@ -12439,7 +12415,7 @@ __metadata:
languageName: node
linkType: hard
-"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
+"safe-buffer@npm:5.2.1, safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
@@ -12460,7 +12436,7 @@ __metadata:
languageName: node
linkType: hard
-"safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0":
+"safer-buffer@npm:>= 2.1.2 < 3.0.0":
version: 2.1.2
resolution: "safer-buffer@npm:2.1.2"
checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4
@@ -12674,7 +12650,7 @@ __metadata:
languageName: node
linkType: hard
-"source-map@npm:^0.6.1":
+"source-map@npm:^0.6.1, source-map@npm:~0.6.1":
version: 0.6.1
resolution: "source-map@npm:0.6.1"
checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011
@@ -12866,6 +12842,15 @@ __metadata:
languageName: node
linkType: hard
+"static-eval@npm:2.1.1":
+ version: 2.1.1
+ resolution: "static-eval@npm:2.1.1"
+ dependencies:
+ escodegen: "npm:^2.1.0"
+ checksum: 10c0/ad8ab8f86e6f82e3ff6c80d60a4c0ccfb086082cf594fa71a5c38cb6ed59607660e7a3e0103f3583ca38716744321d487bbe55a5ae2c6a9c965b5cf4d4cf2960
+ languageName: node
+ linkType: hard
+
"statuses@npm:2.0.1":
version: 2.0.1
resolution: "statuses@npm:2.0.1"
@@ -13571,6 +13556,13 @@ __metadata:
languageName: node
linkType: hard
+"underscore@npm:1.13.6":
+ version: 1.13.6
+ resolution: "underscore@npm:1.13.6"
+ checksum: 10c0/5f57047f47273044c045fddeb8b141dafa703aa487afd84b319c2495de2e685cecd0b74abec098292320d518b267c0c4598e45aa47d4c3628d0d4020966ba521
+ languageName: node
+ linkType: hard
+
"undici-types@npm:~6.21.0":
version: 6.21.0
resolution: "undici-types@npm:6.21.0"