Skip to content

Commit feae366

Browse files
committed
feat: Clean up expired tokens
1 parent a5df8b9 commit feae366

8 files changed

Lines changed: 21 additions & 25 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';
@@ -31,7 +32,7 @@ export class JwtTokenFactory extends TokenFactory {
3132
constructor(
3233
protected readonly keyGen: JwkGenerator,
3334
protected readonly issuer: string,
34-
protected readonly tokenStore: KeyValueStorage<string, AccessToken>,
35+
protected readonly tokenStore: ExpiringStorage<string, AccessToken>,
3536
protected readonly params: JwtTokenParams = { expirationTime: '30m', aud: 'solid' },
3637
protected readonly addSub = false,
3738
) {
@@ -61,9 +62,7 @@ export class JwtTokenFactory extends TokenFactory {
6162
const jwt = await signJwt.sign(jwk);
6263

6364
this.logger.debug(`Issued new JWT Token ${JSON.stringify(token)}`);
64-
// TODO: tokenstore should expire tokens eventually; can use expiring storage,
65-
// or just normal store with timer-based cleanup.
66-
await this.tokenStore.set(jwt, token);
65+
await this.tokenStore.set(jwt, token, ms(this.params.expirationTime as StringValue));
6766
return { token: jwt, tokenType: 'Bearer' };
6867
}
6968

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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AlgJwk, JwkGenerator, KeyValueStorage } from '@solid/community-server';
1+
import { AlgJwk, ExpiringStorage, JwkGenerator } from '@solid/community-server';
22
import { exportJWK, generateKeyPair, GenerateKeyPairResult, jwtVerify, SignJWT } from 'jose';
33
import { beforeAll, Mocked } from 'vitest';
44
import { AccessToken } from '../../../src/tokens/AccessToken';
@@ -33,7 +33,7 @@ describe('JwtTokenFactory', (): void => {
3333
};
3434

3535
let keyGen: Mocked<JwkGenerator>;
36-
let tokenStore: Mocked<KeyValueStorage<string, AccessToken>>;
36+
let tokenStore: Mocked<ExpiringStorage<string, AccessToken>>;
3737
let factory: JwtTokenFactory;
3838

3939
beforeAll(async(): Promise<void> => {
@@ -51,7 +51,7 @@ describe('JwtTokenFactory', (): void => {
5151

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

5656
factory = new JwtTokenFactory(keyGen, issuer, tokenStore);
5757
});
@@ -72,7 +72,7 @@ describe('JwtTokenFactory', (): void => {
7272
expect(parsed.protectedHeader.alg).toBe(alg);
7373
expect(parsed.protectedHeader.kid).toBe(privateKey.kid);
7474
expect(tokenStore.set).toHaveBeenCalledTimes(1);
75-
expect(tokenStore.set).toHaveBeenLastCalledWith(result.token, token);
75+
expect(tokenStore.set).toHaveBeenLastCalledWith(result.token, token, 30 * 60 * 1000);
7676
});
7777

7878
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)