Skip to content

Commit 9c506a2

Browse files
committed
feat: Make sub in access tokens an optional feature
1 parent f7336eb commit 9c506a2

5 files changed

Lines changed: 63 additions & 18 deletions

File tree

documentation/getting-started.md

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ so some information might change depending on which version and branch you're us
4343
- [Authentication methods](#authentication-methods)
4444
- [Customizing OIDC verification](#customizing-oidc-verification)
4545
+ [Generate token](#generate-token)
46+
- [Enabling optional token features](#enabling-optional-token-features)
4647
- [Partial permission tokens](#partial-permission-tokens)
48+
- [Include `sub` claim in access token](#include-sub-claim-in-access-token)
4749
+ [Use token](#use-token)
4850
* [Policies](#policies)
4951
+ [Client application identification](#client-application-identification)
@@ -381,37 +383,47 @@ How these policies work will be covered later on.
381383
If successful, the server will return a 200 response with a JSON body containing, among others,
382384
an `access_token` field containing the access token, and a `token_type` field describing the token type.
383385

384-
The generated access token will also contain a `sub` claim.
385-
This value indicates the identity from the original identification input that was provided during token exchange.
386-
387386
If the claims are insufficient, a 403 response will be given instead.
388387

389-
#### Partial permission tokens
390-
391-
It is possible to set up the server so it also returns tokens
392-
if only some of the requested permissions are granted,
393-
instead of returning a 403 response.
394-
This can be useful for setups where the RS requires only one of the requested permissions to perform a request.
395-
The disadvantage is that the client might receive a token
396-
that does not have all permissions to perform the intended action.
388+
#### Enabling optional token features
397389

398-
To enable this, start the UMA server with both `default.json` and `enable-partial.json`.
390+
Some token-related features are optional and can be enabled by adding extra configuration files
391+
when starting the UMA server, in addition to `default.json`.
399392

400393
From the repository root:
401394
```bash
402-
yarn start:uma -- -c ./config/default.json -c ./config/enable-partial.json
395+
yarn start:uma -- -c ./config/default.json -c ./config/<feature-config>.json
403396
```
404397

405398
From `packages/uma`:
406399
```bash
407-
yarn start -c ./config/default.json -c ./config/enable-partial.json
400+
yarn start -c ./config/default.json -c ./config/<feature-config>.json
408401
```
409402

403+
#### Partial permission tokens
404+
405+
It is possible to set up the server so it also returns tokens
406+
if only some of the requested permissions are granted,
407+
instead of returning a 403 response.
408+
This can be useful for setups where the RS requires only one of the requested permissions to perform a request.
409+
The disadvantage is that the client might receive a token
410+
that does not have all permissions to perform the intended action.
411+
412+
To enable this, use `enable-partial.json` as the feature config.
413+
410414
With this enabled:
411415
- If at least one requested permission can be authorized, the AS returns `200` with an access token.
412416
- If not all requested permissions are granted, that response body includes `partial: true`.
413417
- If no requested permission can be authorized, the AS returns `403`.
414418

419+
#### Include `sub` claim in access token
420+
421+
By default, generated access tokens do not include a `sub` claim.
422+
To include it, use `enable-sub.json` as the feature config.
423+
424+
With this enabled:
425+
- Generated access tokens include `sub`, set to the identity extracted during token exchange.
426+
415427
### Use token
416428

417429
When receiving the access token, the client can perform the same request as it did in the first step,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"@context": [
3+
"https://linkedsoftwaredependencies.org/bundles/npm/@solidlab/uma/^0.0.0/components/context.jsonld"
4+
],
5+
"@graph": [
6+
{
7+
"@id": "urn:uma:default:TokenFactory",
8+
"@type": "JwtTokenFactory",
9+
"addSub": true
10+
}
11+
]
12+
}

packages/uma/src/tokens/JwtTokenFactory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ export class JwtTokenFactory extends TokenFactory {
2626
* @param issuer - server URL to assign to the issuer field
2727
* @param tokenStore - stores the link between JWT and access token
2828
* @param params - additional parameters for the generated JWT
29+
* @param addSub - if true, adds a sub claim to the JWT if available in the input token
2930
*/
3031
constructor(
3132
protected readonly keyGen: JwkGenerator,
3233
protected readonly issuer: string,
3334
protected readonly tokenStore: KeyValueStorage<string, AccessToken>,
3435
protected readonly params: JwtTokenParams = { expirationTime: '30m', aud: 'solid' },
36+
protected readonly addSub = false,
3537
) {
3638
super();
3739
}
@@ -52,7 +54,7 @@ export class JwtTokenFactory extends TokenFactory {
5254
.setExpirationTime(this.params.expirationTime)
5355
.setJti(randomUUID());
5456

55-
if (token.sub) {
57+
if (this.addSub && token.sub) {
5658
signJwt = signJwt.setSubject(token.sub);
5759
}
5860

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AlgJwk, JwkGenerator, KeyValueStorage } from '@solid/community-server';
2-
import { exportJWK, generateKeyPair, GenerateKeyPairResult, jwtVerify, KeyLike, SignJWT } from 'jose';
2+
import { exportJWK, generateKeyPair, GenerateKeyPairResult, jwtVerify, SignJWT } from 'jose';
33
import { beforeAll, Mocked } from 'vitest';
44
import { AccessToken } from '../../../src/tokens/AccessToken';
55
import { JwtTokenFactory } from '../../../src/tokens/JwtTokenFactory';
@@ -63,7 +63,6 @@ describe('JwtTokenFactory', (): void => {
6363
expect(parsed.payload).toEqual({
6464
permissions: token.permissions,
6565
contract: token.contract,
66-
sub: token.sub,
6766
iat: Math.floor(now.getTime()/1000),
6867
iss: issuer,
6968
aud: 'solid',
@@ -116,4 +115,21 @@ describe('JwtTokenFactory', (): void => {
116115
await expect(factory.deserialize(jwt)).rejects
117116
.toThrow('Invalid Access Token provided, error while parsing: value is not an array');
118117
});
118+
119+
it('includes the sub claim when addSub is true.', async(): Promise<void> => {
120+
const factoryWithSub = new JwtTokenFactory(keyGen, issuer, tokenStore, undefined, true);
121+
const result = await factoryWithSub.serialize(token);
122+
const parsed = await jwtVerify(result.token, keys.publicKey);
123+
expect(parsed.payload.sub).toBe(token.sub);
124+
expect(parsed.payload).toEqual({
125+
permissions: token.permissions,
126+
contract: token.contract,
127+
sub: token.sub,
128+
iat: Math.floor(now.getTime()/1000),
129+
iss: issuer,
130+
aud: 'solid',
131+
exp: Math.floor(now.getTime()/1000) + 30 * 60,
132+
jti: '1-2-3-4-5',
133+
});
134+
});
119135
});

test/integration/Oidc.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ describe('A server supporting OIDC tokens', (): void => {
2626

2727
umaApp = await instantiateFromConfig(
2828
'urn:uma:default:App',
29-
path.join(__dirname, '../../packages/uma/config/default.json'),
29+
[
30+
path.join(__dirname, '../../packages/uma/config/default.json'),
31+
path.join(__dirname, '../../packages/uma/config/enable-sub.json')
32+
],
3033
{
3134
'urn:uma:variables:port': umaPort,
3235
'urn:uma:variables:baseUrl': `http://localhost:${umaPort}/uma`,

0 commit comments

Comments
 (0)