Skip to content

Commit 1e6e6f4

Browse files
committed
feat: Provide request claims during introspection
1 parent 2b423f6 commit 1e6e6f4

7 files changed

Lines changed: 44 additions & 57 deletions

File tree

packages/css/src/uma/UmaClient.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ export interface Claims {
2424
export interface UmaPermission {
2525
resource_id: string,
2626
resource_scopes: string[],
27+
policies?: string[],
2728
exp?: number,
2829
iat?: number,
2930
nbf?: number,
3031
}
3132

3233
export type UmaClaims = JWTPayload & {
3334
permissions?: UmaPermission[],
35+
requestClaims?: NodeJS.Dict<unknown>,
3436
}
3537

3638
export interface UmaConfig {
@@ -232,7 +234,7 @@ export class UmaClient implements SingleThreaded {
232234
}
233235

234236
const jwt = await res.json();
235-
if (jwt.active !== 'true') throw new Error(`The provided UMA RPT is not active.`);
237+
if (jwt.active !== true) throw new Error(`The provided UMA RPT is not active.`);
236238

237239
return await this.verifyTokenData(jwt, config.issuer, config.jwks_uri);
238240
}

packages/uma/src/dialog/BaseNegotiator.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { randomUUID } from 'node:crypto';
2+
import { ClaimSet } from '../credentials/ClaimSet';
23
import { Ticket } from '../ticketing/Ticket';
34
import { Verifier } from '../credentials/verify/Verifier';
45
import { TokenFactory } from '../tokens/TokenFactory';
@@ -50,7 +51,7 @@ export class BaseNegotiator implements Negotiator {
5051
this.logger.debug(`Processing ticket. ${JSON.stringify(ticket)}`);
5152

5253
// Process pushed credentials
53-
const updatedTicket = await this.processCredentials(input, ticket);
54+
const { ticket: updatedTicket, claims } = await this.processCredentials(input, ticket);
5455
this.logger.debug(`resolved result ${JSON.stringify(updatedTicket)}`);
5556

5657
// Try to resolve ticket ...
@@ -61,7 +62,7 @@ export class BaseNegotiator implements Negotiator {
6162
if (resolved.success) {
6263

6364
// Retrieve / create instantiated policy
64-
const { token, tokenType } = await this.tokenFactory.serialize({ permissions: resolved.value });
65+
const { token, tokenType } = await this.tokenFactory.serialize({ permissions: resolved.value }, claims);
6566
this.logger.debug(`Minted token ${JSON.stringify(token)}`);
6667

6768
// TODO:: test logging
@@ -129,7 +130,8 @@ export class BaseNegotiator implements Negotiator {
129130
*
130131
* @returns An updated Ticket in which the Credentials have been validated.
131132
*/
132-
protected async processCredentials(input: DialogInput, ticket: Ticket): Promise<Ticket> {
133+
protected async processCredentials(input: DialogInput, ticket: Ticket):
134+
Promise<{ ticket: Ticket, claims?: ClaimSet }> {
133135
const { claim_token: token, claim_token_format: format } = input;
134136

135137
if (token || format) {
@@ -138,10 +140,10 @@ export class BaseNegotiator implements Negotiator {
138140

139141
const claims = await this.verifier.verify({ token, format });
140142

141-
return await this.ticketingStrategy.validateClaims(ticket, claims);
143+
return { ticket: await this.ticketingStrategy.validateClaims(ticket, claims), claims };
142144
}
143145

144-
return ticket;
146+
return { ticket };
145147
}
146148

147149
/**

packages/uma/src/dialog/ContractNegotiator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export class ContractNegotiator extends BaseNegotiator {
5757
this.logger.debug(`Processing ticket. ${JSON.stringify(ticket)}`);
5858

5959
// Process pushed credentials
60-
const updatedTicket = await this.processCredentials(input, ticket);
60+
const { ticket: updatedTicket } = await this.processCredentials(input, ticket);
6161
this.logger.debug(`Processed credentials ${JSON.stringify(updatedTicket)}`);
6262

6363
// TODO:

packages/uma/src/routes/Introspection.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BadRequestHttpError, getLoggerFor, UnauthorizedHttpError } from '@solid/community-server';
2+
import { ClaimSet } from '../credentials/ClaimSet';
23
import { TokenFactory } from '../tokens/TokenFactory';
34
import { HttpHandler, HttpHandlerContext, HttpHandlerResponse } from '../util/http/models/HttpHandler';
45
import { verifyRequest } from '../util/HttpMessageSignatures';
@@ -8,8 +9,10 @@ type IntrospectionResponse = {
89
active : boolean,
910
permissions: {
1011
resource_id: string,
11-
resource_scopes: string[]
12+
resource_scopes: string[],
13+
policies?: string[],
1214
}[],
15+
requestClaims?: ClaimSet,
1316
exp?: number,
1417
iat?: number,
1518
nbf?: number,
@@ -43,10 +46,10 @@ export class IntrospectionHandler extends HttpHandler {
4346
const token = new URLSearchParams(request.body as Record<string, string>).get('token');
4447
try {
4548
if (!token) throw new Error('could not extract token from request body')
46-
const unsignedToken = await this.tokenFactory.deserialize(token);
49+
const { token: unsignedToken, claims } = await this.tokenFactory.deserialize(token);
4750
return {
4851
status: 200,
49-
body: { ...unsignedToken, active: true },
52+
body: { ...unsignedToken, requestClaims: claims, active: true },
5053
};
5154
} catch (e) {
5255
this.logger.warn(`Token introspection failed: ${e}`)

packages/uma/src/tokens/JwtTokenFactory.ts

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,9 @@ import {
77
KeyValueStorage
88
} from '@solid/community-server';
99
import { randomUUID } from 'node:crypto';
10+
import { ClaimSet } from '../credentials/ClaimSet';
1011
import { SerializedToken , TokenFactory} from './TokenFactory';
1112
import { AccessToken } from './AccessToken';
12-
import { array, reType } from '../util/ReType';
13-
import { Permission } from '../views/Permission';
1413

1514
const AUD = 'solid';
1615

@@ -35,7 +34,7 @@ export class JwtTokenFactory extends TokenFactory {
3534
constructor(
3635
protected readonly keyGen: JwkGenerator,
3736
protected readonly issuer: string,
38-
protected readonly tokenStore: KeyValueStorage<string, AccessToken>,
37+
protected readonly tokenStore: KeyValueStorage<string, { token: AccessToken, claims?: ClaimSet }>,
3938
protected readonly params: JwtTokenParams = { expirationTime: '30m', aud: 'solid' },
4039
) {
4140
super();
@@ -44,9 +43,10 @@ export class JwtTokenFactory extends TokenFactory {
4443
/**
4544
* Serializes an Access Token into a JWT
4645
* @param {AccessToken} token - authenticated and authorized principal
46+
* @param claims - claims used to acquire this token
4747
* @return {Promise<SerializedToken>} - access token response
4848
*/
49-
public async serialize(token: AccessToken): Promise<SerializedToken> {
49+
public async serialize(token: AccessToken, claims?: ClaimSet): Promise<SerializedToken> {
5050
const key = await this.keyGen.getPrivateKey();
5151
const jwk = await importJWK(key, key.alg);
5252
const jwt = await new SignJWT({ permissions: token.permissions, contract: token.contract })
@@ -59,37 +59,21 @@ export class JwtTokenFactory extends TokenFactory {
5959
.sign(jwk);
6060

6161
this.logger.debug(`Issued new JWT Token ${JSON.stringify(token)}`);
62-
await this.tokenStore.set(jwt, token);
62+
await this.tokenStore.set(jwt, { token, claims });
6363
return { token: jwt, tokenType: 'Bearer' };
6464
}
6565

6666
/**
6767
* Deserializes a JWT into an Access Token
6868
* @param {string} token - JWT access token
69-
* @return {Promise<AccessToken>} - deserialized access token
69+
* @return {Promise<AccessToken>} - deserialized access token and claims
7070
*/
71-
public async deserialize(token: string): Promise<AccessToken> {
72-
const key = await this.keyGen.getPublicKey();
73-
const jwk = await importJWK(key, key.alg);
74-
try {
75-
const { payload } = await jwtVerify(token, jwk, {
76-
issuer: this.issuer,
77-
audience: this.params.aud ?? AUD,
78-
});
79-
80-
if (!payload.permissions) {
81-
throw new Error('missing required "permissions" claim.');
82-
}
83-
84-
const permissions = payload.permissions;
85-
86-
reType(permissions, array(Permission));
87-
88-
return { permissions };
89-
} catch (error: unknown) {
90-
const msg = `Invalid Access Token provided, error while parsing: ${createErrorMessage(error)}`;
91-
this.logger.warn(msg);
92-
throw new BadRequestHttpError(msg);
71+
public async deserialize(token: string): Promise<{ token: AccessToken, claims?: ClaimSet }> {
72+
// TODO: might want to move this behaviour outside of this class as it is the same for all factories
73+
const result = await this.tokenStore.get(token);
74+
if (!result) {
75+
throw new BadRequestHttpError('Invalid Access Token provided');
9376
}
77+
return result;
9478
}
9579
}
Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { KeyValueStorage } from '@solid/community-server';
1+
import { BadRequestHttpError, KeyValueStorage } from '@solid/community-server';
22
import { randomUUID } from 'node:crypto';
3+
import { ClaimSet } from '../credentials/ClaimSet';
34
import {AccessToken} from './AccessToken';
45
import {SerializedToken, TokenFactory} from './TokenFactory';
56

@@ -11,28 +12,22 @@ export class OpaqueTokenFactory extends TokenFactory {
1112
*
1213
* @param {KeyValueStorage<string, AccessToken>} tokenStore
1314
*/
14-
constructor(private tokenStore: KeyValueStorage<string, AccessToken>) {
15+
constructor(protected readonly tokenStore: KeyValueStorage<string, { token: AccessToken, claims?: ClaimSet }>) {
1516
super();
1617
}
1718

18-
/**
19-
*
20-
* @param {AccessToken} token
21-
* @return {Promise<SerializedToken>}
22-
*/
23-
public async serialize(token: AccessToken): Promise<SerializedToken> {
19+
public async serialize(token: AccessToken, claims?: ClaimSet): Promise<SerializedToken> {
2420
const serialized = randomUUID();
25-
await this.tokenStore.set(serialized, token);
21+
await this.tokenStore.set(serialized, { token, claims });
2622
return {tokenType: 'Bearer', token: serialized};
2723
}
2824

29-
/**
30-
*
31-
* @param {string} token
32-
*/
33-
public async deserialize(token: string): Promise<AccessToken> {
34-
const retrieved = await this.tokenStore.get(token);
35-
if (retrieved) return retrieved;
36-
throw new Error('Token string not recognized.');
25+
public async deserialize(token: string): Promise<{ token: AccessToken, claims?: ClaimSet }> {
26+
// TODO: might want to move this behaviour outside of this class as it is the same for all factories
27+
const result = await this.tokenStore.get(token);
28+
if (!result) {
29+
throw new BadRequestHttpError('Invalid Access Token provided');
30+
}
31+
return result;
3732
}
3833
}

packages/uma/src/tokens/TokenFactory.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ClaimSet } from '../credentials/ClaimSet';
12
import {AccessToken} from './AccessToken';
23

34
export interface SerializedToken {
@@ -11,6 +12,6 @@ export interface SerializedToken {
1112
* and deserializing gathered tokens
1213
*/
1314
export abstract class TokenFactory {
14-
public abstract serialize(token: AccessToken): Promise<SerializedToken>;
15-
public abstract deserialize(token: string): Promise<AccessToken>;
15+
public abstract serialize(token: AccessToken, claims?: ClaimSet): Promise<SerializedToken>;
16+
public abstract deserialize(token: string): Promise<{ token: AccessToken, claims?: ClaimSet }>;
1617
}

0 commit comments

Comments
 (0)