Skip to content

Commit 09ec2e4

Browse files
committed
feat: Clean up expired tokens
1 parent e1f25c5 commit 09ec2e4

8 files changed

Lines changed: 22 additions & 26 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"TypedVerifier:_verifiers_value": {
3939
"@id": "urn:uma:default:RefreshTokenVerifier",
4040
"@type": "RefreshTokenVerifier",
41-
"refreshStore": { "@id": "urn:uma:default:RefreshTokenStore" }
41+
"refreshStore": { "@id": "urn:uma:default:TokenStore" }
4242
}
4343
},
4444
{

packages/uma/config/dialog/negotiators/default.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"refreshTokenIssuer": {
1919
"@id": "urn:uma:default:RefreshTokenIssuer",
2020
"@type": "StoredRefreshTokenIssuer",
21-
"refreshStore": { "@id": "urn:uma:default:RefreshTokenStore" }
21+
"refreshStore": { "@id": "urn:uma:default:TokenStore" }
2222
}
2323
}
2424
}

packages/uma/config/routes/tokens.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"refreshTokenHandler": {
2121
"@id": "urn:uma:default:RefreshTokenHandler",
2222
"@type": "RefreshTokenHandler",
23-
"refreshStore": { "@id": "urn:uma:default:RefreshTokenStore" },
23+
"refreshStore": { "@id": "urn:uma:default:TokenStore" },
2424
"negotiator": { "@id": "urn:uma:default:Negotiator" }
2525
}
2626
},

packages/uma/config/tokens/storage/default.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
"@graph": [
66
{
77
"@id": "urn:uma:default:TokenStore",
8-
"@type": "MemoryMapStorage"
9-
},
10-
{
11-
"@id": "urn:uma:default:RefreshTokenStore",
12-
"@type": "MemoryMapStorage"
8+
"@type": "WrappedExpiringStorage",
9+
"source": {
10+
"@type": "MemoryMapStorage"
11+
}
1312
}
1413
]
1514
}

packages/uma/src/tokens/JwtTokenFactory.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { BadRequestHttpError, createErrorMessage, JwkGenerator, KeyValueStorage } from '@solid/community-server';
1+
import { BadRequestHttpError, createErrorMessage, ExpiringStorage, JwkGenerator, } from '@solid/community-server';
22
import { getLoggerFor } from 'global-logger-factory';
33
import { importJWK, jwtVerify, SignJWT } from 'jose';
4+
import ms, { StringValue } from 'ms';
45
import { randomUUID } from 'node:crypto';
56
import { array, reType } from '../util/ReType';
67
import { Permission } from '../views/Permission';
@@ -30,7 +31,7 @@ export class JwtTokenFactory extends TokenFactory {
3031
constructor(
3132
protected readonly keyGen: JwkGenerator,
3233
protected readonly issuer: string,
33-
protected readonly tokenStore: KeyValueStorage<string, AccessToken>,
34+
protected readonly tokenStore: ExpiringStorage<string, AccessToken>,
3435
protected readonly params: JwtTokenParams = { expirationTime: '30m', aud: 'solid' },
3536
) {
3637
super();
@@ -54,9 +55,7 @@ export class JwtTokenFactory extends TokenFactory {
5455
.sign(jwk);
5556

5657
this.logger.debug(`Issued new JWT Token ${JSON.stringify(token)}`);
57-
// TODO: tokenstore should expire tokens eventually; can use expiring storage,
58-
// or just normal store with timer-based cleanup.
59-
await this.tokenStore.set(jwt, token);
58+
await this.tokenStore.set(jwt, token, ms(this.params.expirationTime as StringValue));
6059
return { token: jwt, tokenType: 'Bearer' };
6160
}
6261

packages/uma/src/tokens/StoredRefreshTokenIssuer.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { KeyValueStorage } from '@solid/community-server';
1+
import { ExpiringStorage } from '@solid/community-server';
22
import ms, { StringValue } from 'ms';
33
import { randomUUID } from 'node:crypto';
44
import { ClaimSet } from '../credentials/ClaimSet';
@@ -14,21 +14,19 @@ export class StoredRefreshTokenIssuer implements RefreshTokenIssuer {
1414
protected readonly refreshExpiration: number;
1515

1616
public constructor(
17-
protected readonly refreshStore: KeyValueStorage<string, RefreshInformation>,
17+
protected readonly refreshStore: ExpiringStorage<string, RefreshInformation>,
1818
refreshExpiration: string = '7d',
1919
) {
2020
this.refreshExpiration = ms(refreshExpiration as StringValue);
2121
}
2222

2323
public async issue(claims: ClaimSet, permissions: Permission[]): Promise<string> {
2424
const refreshToken = randomUUID();
25-
// TODO: expired tokens should be removed; can use expiring storage,
26-
// or just normal store with timer-based cleanup.
2725
await this.refreshStore.set(refreshToken, {
2826
claims,
2927
permissions,
3028
expiration: Date.now() + this.refreshExpiration,
31-
});
29+
}, this.refreshExpiration);
3230
return refreshToken;
3331
}
3432
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { AlgJwk, JwkGenerator, KeyValueStorage } from '@solid/community-server';
2-
import { exportJWK, generateKeyPair, GenerateKeyPairResult, jwtVerify, KeyLike, SignJWT } from 'jose';
1+
import { AlgJwk, ExpiringStorage, JwkGenerator } from '@solid/community-server';
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';
@@ -32,7 +32,7 @@ describe('JwtTokenFactory', (): void => {
3232
};
3333

3434
let keyGen: Mocked<JwkGenerator>;
35-
let tokenStore: Mocked<KeyValueStorage<string, AccessToken>>;
35+
let tokenStore: Mocked<ExpiringStorage<string, AccessToken>>;
3636
let factory: JwtTokenFactory;
3737

3838
beforeAll(async(): Promise<void> => {
@@ -50,7 +50,7 @@ describe('JwtTokenFactory', (): void => {
5050

5151
tokenStore = {
5252
set: vi.fn(),
53-
} satisfies Partial<KeyValueStorage<string, AccessToken>> as any;
53+
} satisfies Partial<ExpiringStorage<string, AccessToken>> as any;
5454

5555
factory = new JwtTokenFactory(keyGen, issuer, tokenStore);
5656
});
@@ -70,7 +70,7 @@ describe('JwtTokenFactory', (): void => {
7070
expect(parsed.protectedHeader.alg).toBe(alg);
7171
expect(parsed.protectedHeader.kid).toBe(privateKey.kid);
7272
expect(tokenStore.set).toHaveBeenCalledTimes(1);
73-
expect(tokenStore.set).toHaveBeenLastCalledWith(result.token, token);
73+
expect(tokenStore.set).toHaveBeenLastCalledWith(result.token, token, 30 * 60 * 1000);
7474
});
7575

7676
it('returns the permissions of the deserialized token.', async(): Promise<void> => {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { KeyValueStorage } from '@solid/community-server';
1+
import { ExpiringStorage } from '@solid/community-server';
22
import { Mocked } from 'vitest';
33
import { RefreshInformation } from '../../../src/credentials/verify/RefreshTokenVerifier';
44
import { StoredRefreshTokenIssuer } from '../../../src/tokens/StoredRefreshTokenIssuer';
@@ -14,7 +14,7 @@ describe('StoredRefreshTokenIssuer', (): void => {
1414
const claims = { webid: 'https://alice.example/#me' };
1515
const permissions = [ { resource_id: 'id', resource_scopes: [ 'scope' ] } ];
1616

17-
let refreshStore: Mocked<KeyValueStorage<string, RefreshInformation>>;
17+
let refreshStore: Mocked<ExpiringStorage<string, RefreshInformation>>;
1818
let issuer: StoredRefreshTokenIssuer;
1919

2020
beforeEach(async(): Promise<void> => {
@@ -33,7 +33,7 @@ describe('StoredRefreshTokenIssuer', (): void => {
3333
claims,
3434
permissions,
3535
expiration: now.getTime() + 7 * 24 * 60 * 60 * 1000,
36-
});
36+
}, 7 * 24 * 60 * 60 * 1000);
3737
expect(result).toBe('refresh-token-id');
3838
});
3939
});

0 commit comments

Comments
 (0)