Skip to content

Commit c186183

Browse files
committed
feat: Add sub value to response access tokens
1 parent f800940 commit c186183

7 files changed

Lines changed: 69 additions & 19 deletions

File tree

packages/uma/src/dialog/BaseNegotiator.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BadRequestHttpError, ForbiddenHttpError, HttpErrorClass, KeyValueStorage } from '@solid/community-server';
22
import { getLoggerFor } from 'global-logger-factory';
33
import { randomUUID } from 'node:crypto';
4+
import { WEBID } from '../credentials/Claims';
45
import { Verifier } from '../credentials/verify/Verifier';
56
import { NeedInfoError, RequiredClaim } from '../errors/NeedInfoError';
67
import { getOperationLogger } from '../logging/OperationLogger';
@@ -59,7 +60,10 @@ export class BaseNegotiator implements Negotiator {
5960
const partial = this.isPartialResult(updatedTicket.permissions, resolved.value);
6061

6162
// Retrieve / create instantiated policy
62-
const { token, tokenType } = await this.tokenFactory.serialize({ permissions: resolved.value });
63+
const { token, tokenType } = await this.tokenFactory.serialize({
64+
permissions: resolved.value,
65+
...(typeof updatedTicket.provided[WEBID] === 'string' ? { sub: updatedTicket.provided[WEBID] } : {}),
66+
});
6367
this.logger.debug(`Minted token ${JSON.stringify(token)}`);
6468

6569
// TODO:: test logging

packages/uma/src/dialog/ContractNegotiator.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createErrorMessage, KeyValueStorage } from '@solid/community-server';
22
import { getLoggerFor } from 'global-logger-factory';
3+
import { WEBID } from '../credentials/Claims';
34
import { Verifier } from '../credentials/verify/Verifier';
45
import { RequiredClaim } from '../errors/NeedInfoError';
56
import { ContractManager } from '../policies/contracts/ContractManager';
@@ -66,7 +67,7 @@ export class ContractNegotiator extends BaseNegotiator {
6667

6768
if (result.success) {
6869
// TODO:
69-
return this.toResponse(result.value);
70+
return this.toResponse(result.value, updatedTicket);
7071
}
7172

7273
// ... on failure, deny if no solvable requirements
@@ -121,7 +122,7 @@ export class ContractNegotiator extends BaseNegotiator {
121122
}
122123

123124
// TODO: name
124-
protected async toResponse(contract: ODRLContract): Promise<DialogOutput> {
125+
protected async toResponse(contract: ODRLContract, ticket: Ticket): Promise<DialogOutput> {
125126

126127
this.logger.debug(JSON.stringify(contract, null, 2))
127128

@@ -149,7 +150,11 @@ export class ContractNegotiator extends BaseNegotiator {
149150
this.logger.debug(`granting permissions: ${JSON.stringify(permissions)}`);
150151

151152
// Create response
152-
const tokenContents: AccessToken = { permissions, contract }
153+
const tokenContents: AccessToken = {
154+
permissions,
155+
contract,
156+
...(typeof ticket.provided[WEBID] === 'string' ? { sub: ticket.provided[WEBID] } : {}),
157+
};
153158

154159
this.logger.debug(`resolved result ${JSON.stringify(contract)}`);
155160

packages/uma/src/tokens/AccessToken.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { Type, array, optional as $, string, intersection, optional } from "../u
44

55
export const AccessToken = {
66
permissions: array(Permission),
7+
sub: optional(string),
78
contract: optional(ODRLContract)
89
}
910

1011
export type AccessToken = Type<typeof AccessToken>;
11-

packages/uma/src/tokens/JwtTokenFactory.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,19 @@ export class JwtTokenFactory extends TokenFactory {
4444
public async serialize(token: AccessToken): Promise<SerializedToken> {
4545
const key = await this.keyGen.getPrivateKey();
4646
const jwk = await importJWK(key, key.alg);
47-
const jwt = await new SignJWT({ permissions: token.permissions, contract: token.contract })
47+
let signJwt = new SignJWT({ permissions: token.permissions, contract: token.contract })
4848
.setProtectedHeader({ alg: key.alg, kid: key.kid })
4949
.setIssuedAt()
5050
.setIssuer(this.issuer)
5151
.setAudience(this.params.aud ?? AUD)
5252
.setExpirationTime(this.params.expirationTime)
53-
.setJti(randomUUID())
54-
.sign(jwk);
53+
.setJti(randomUUID());
54+
55+
if (token.sub) {
56+
signJwt = signJwt.setSubject(token.sub);
57+
}
58+
59+
const jwt = await signJwt.sign(jwk);
5560

5661
this.logger.debug(`Issued new JWT Token ${JSON.stringify(token)}`);
5762
await this.tokenStore.set(jwt, token);
@@ -80,7 +85,10 @@ export class JwtTokenFactory extends TokenFactory {
8085

8186
reType(permissions, array(Permission));
8287

83-
return { permissions };
88+
return {
89+
permissions,
90+
...(typeof payload.sub === 'string' ? { sub: payload.sub } : {}),
91+
};
8492
} catch (error: unknown) {
8593
const msg = `Invalid Access Token provided, error while parsing: ${createErrorMessage(error)}`;
8694
this.logger.warn(msg);

packages/uma/test/unit/dialog/BaseNegotiator.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ForbiddenHttpError, KeyValueStorage } from '@solid/community-server';
22
import { Mocked } from 'vitest';
3+
import { WEBID } from '../../../src/credentials/Claims';
34
import { ClaimSet } from '../../../src/credentials/ClaimSet';
45
import { Verifier } from '../../../src/credentials/verify/Verifier';
56
import { BaseNegotiator } from '../../../src/dialog/BaseNegotiator';
@@ -172,6 +173,22 @@ describe('BaseNegotiator', (): void => {
172173
expect(ticketingStrategy.validateClaims).toHaveBeenCalledWith(ticket, claims);
173174
});
174175

176+
it('includes the WEBID claim in generated tokens as sub value.', async(): Promise<void> => {
177+
const webId = 'https://example.com/profile/card#me';
178+
ticketingStrategy.validateClaims.mockResolvedValueOnce({
179+
...ticket,
180+
provided: { [WEBID]: webId },
181+
});
182+
183+
await expect(negotiator.negotiate({ ...input, claim_token: 'token', claim_token_format: 'format' })).resolves
184+
.toEqual({ access_token: 'token', token_type: 'type' });
185+
186+
expect(tokenFactory.serialize).toHaveBeenLastCalledWith({
187+
permissions: [ { resource_id: 'id1', resource_scopes: [ 'scope1' ] } ],
188+
sub: webId,
189+
});
190+
});
191+
175192
it('includes partial=true when resolved permissions do not cover all requested scopes.', async(): Promise<void> => {
176193
ticketingStrategy.initializeTicket.mockResolvedValueOnce({
177194
permissions: [ { resource_id: 'id1', resource_scopes: [ 'scope1', 'scope2' ] } ],

packages/uma/test/unit/dialog/ContractNegotiator.test.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ForbiddenHttpError, KeyValueStorage } from '@solid/community-server';
22
import { Mocked, MockInstance } from 'vitest';
3+
import { WEBID } from '../../../src/credentials/Claims';
34
import { ClaimSet } from '../../../src/credentials/ClaimSet';
45
import { Verifier } from '../../../src/credentials/verify/Verifier';
56
import { ContractNegotiator } from '../../../src/dialog/ContractNegotiator';
@@ -10,13 +11,6 @@ import { Ticket } from '../../../src/ticketing/Ticket';
1011
import { SerializedToken, TokenFactory } from '../../../src/tokens/TokenFactory';
1112
import { ODRLContract } from '../../../src/views/Contract';
1213

13-
// vi.mock('../../../src/policies/contracts/ContractManager', () => ({
14-
// ContractManager: vi.fn().mockReturnValue({
15-
// createContract: vi.fn(),
16-
// findContract: vi.fn(),
17-
// }),
18-
// }))
19-
2014
describe('ContractNegotiator', (): void => {
2115
const input: DialogInput = {
2216
permissions: [
@@ -27,7 +21,6 @@ describe('ContractNegotiator', (): void => {
2721
const claims: ClaimSet = { claim1: 'value1', claim2: 'value2' };
2822
const ticket: Ticket = {
2923
permissions: [ { resource_id: 'id1', resource_scopes: [ 'scope1' ] } ],
30-
required: [],
3124
provided: { claim: 'value' },
3225
};
3326
const token: SerializedToken = { token: 'token', tokenType: 'type' };
@@ -142,4 +135,21 @@ describe('ContractNegotiator', (): void => {
142135
await expect(negotiator.negotiate(input)).rejects.toThrow(ForbiddenHttpError);
143136
expect(ticketStore.set).toHaveBeenCalledTimes(0);
144137
});
138+
139+
it('includes the WEBID claim in generated tokens as sub value.', async(): Promise<void> => {
140+
const webId = 'https://example.com/profile/card#me';
141+
ticketingStrategy.validateClaims.mockResolvedValueOnce({
142+
...ticket,
143+
provided: { [WEBID]: webId },
144+
});
145+
146+
await expect(negotiator.negotiate({ ...input, claim_token: 'token', claim_token_format: 'format' })).resolves
147+
.toEqual({ access_token: 'token', token_type: 'type' });
148+
149+
expect(tokenFactory.serialize).toHaveBeenLastCalledWith({
150+
contract,
151+
permissions: [{ resource_id: 'target', resource_scopes: [ 'https://w3id.org/oac#action' ] }],
152+
sub: webId,
153+
});
154+
});
145155
});

packages/uma/test/unit/tokens/JwtTokenFactory.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('JwtTokenFactory', (): void => {
2020

2121
const token: AccessToken = {
2222
permissions: [ { resource_id: 'id', resource_scopes: [ 'scopes' ]} ],
23+
sub: 'https://example.com/profile/card#me',
2324
contract: {
2425
uid: 'uid',
2526
permission: [{
@@ -60,7 +61,9 @@ describe('JwtTokenFactory', (): void => {
6061
expect(result.tokenType).toBe('Bearer');
6162
const parsed = await jwtVerify(result.token, keys.publicKey);
6263
expect(parsed.payload).toEqual({
63-
...token,
64+
permissions: token.permissions,
65+
contract: token.contract,
66+
sub: token.sub,
6467
iat: Math.floor(now.getTime()/1000),
6568
iss: issuer,
6669
aud: 'solid',
@@ -79,7 +82,10 @@ describe('JwtTokenFactory', (): void => {
7982
.setIssuer(issuer)
8083
.setAudience('solid')
8184
.sign(keys.privateKey);
82-
await expect(factory.deserialize(jwt)).resolves.toEqual({ permissions: token.permissions });
85+
await expect(factory.deserialize(jwt)).resolves.toEqual({
86+
permissions: token.permissions,
87+
sub: token.sub,
88+
});
8389
});
8490

8591
it('errors deserializing tokens with no aud field.', async(): Promise<void> => {

0 commit comments

Comments
 (0)