Skip to content

Commit eeac0a6

Browse files
fix(oidc): fix OIDCAdapter broken flows (#7837)
* fix(oidc): fix OIDCAdapter broken flows * fix(oidc): fix storage type to include string for userCode index * test(oidc): add regression tests for OIDCAdapter broken flows
1 parent 4b30653 commit eeac0a6

2 files changed

Lines changed: 98 additions & 40 deletions

File tree

src/node/security/OIDCAdapter.ts

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,110 @@
11
import {LRUCache} from 'lru-cache';
22
import type {Adapter, AdapterPayload} from "oidc-provider";
33

4-
54
const options = {
65
max: 500,
7-
sizeCalculation: (item:any, key:any) => {
8-
return 1
9-
},
10-
// for use with tracking overall storage size
6+
sizeCalculation: (item: any, key: any) => 1,
117
maxSize: 5000,
12-
13-
// how long to live in ms
148
ttl: 1000 * 60 * 5,
15-
16-
// return stale items before removing from cache?
179
allowStale: false,
18-
1910
updateAgeOnGet: false,
2011
updateAgeOnHas: false,
2112
}
2213

2314
const epochTime = (date = Date.now()) => Math.floor(date / 1000);
24-
25-
const storage = new LRUCache<string,AdapterPayload|string[]|string>(options);
15+
const storage = new LRUCache<string, AdapterPayload | string[] | string>(options);
2616

2717
function grantKeyFor(id: string) {
2818
return `grant:${id}`;
2919
}
30-
31-
function userCodeKeyFor(userCode:string) {
20+
function userCodeKeyFor(userCode: string) {
3221
return `userCode:${userCode}`;
3322
}
3423

35-
class MemoryAdapter implements Adapter{
24+
class MemoryAdapter implements Adapter {
3625
private readonly name: string;
37-
constructor(name:string) {
26+
27+
constructor(name: string) {
3828
this.name = name;
3929
}
4030

41-
key(id:string) {
31+
key(id: string) {
4232
return `${this.name}:${id}`;
4333
}
4434

45-
destroy(id:string) {
35+
destroy(id: string) {
4636
const key = this.key(id);
37+
const found = storage.get(key) as AdapterPayload | undefined;
38+
const grantId = found?.grantId;
4739

48-
const found = storage.get(key) as AdapterPayload;
49-
const grantId = found && found.grantId;
40+
if (found?.userCode) {
41+
storage.delete(userCodeKeyFor(found.userCode));
42+
}
5043

5144
storage.delete(key);
5245

5346
if (grantId) {
5447
const grantKey = grantKeyFor(grantId);
55-
(storage.get(grantKey) as string[])!.forEach(token => storage.delete(token));
48+
const tokens = storage.get(grantKey) as string[] | undefined;
49+
tokens?.forEach(token => storage.delete(token));
5650
storage.delete(grantKey);
5751
}
5852

5953
return Promise.resolve();
6054
}
6155

6256
consume(id: string) {
63-
(storage.get(this.key(id)) as AdapterPayload)!.consumed = epochTime();
57+
const key = this.key(id);
58+
const payload = storage.get(key) as AdapterPayload | undefined;
59+
if (payload) {
60+
payload.consumed = epochTime();
61+
storage.set(key, payload);
62+
}
6463
return Promise.resolve();
6564
}
6665

6766
find(id: string): Promise<AdapterPayload | void | undefined> {
68-
if (storage.has(this.key(id))){
69-
return Promise.resolve<AdapterPayload>(storage.get(this.key(id)) as AdapterPayload);
67+
if (storage.has(this.key(id))) {
68+
return Promise.resolve(storage.get(this.key(id)) as AdapterPayload);
7069
}
71-
return Promise.resolve<undefined>(undefined)
70+
return Promise.resolve(undefined);
7271
}
7372

7473
findByUserCode(userCode: string) {
7574
const id = storage.get(userCodeKeyFor(userCode)) as string;
7675
return this.find(id);
7776
}
7877

79-
upsert(id: string, payload: {
80-
iat: number;
81-
exp: number;
82-
uid: string;
83-
kind: string;
84-
jti: string;
85-
accountId: string;
86-
loginTs: number;
87-
}, expiresIn: number) {
78+
upsert(id: string, payload: AdapterPayload, expiresIn: number) {
8879
const key = this.key(id);
8980

90-
storage.set(key, payload, {ttl: expiresIn * 1000});
81+
if (payload.grantId) {
82+
const grantKey = grantKeyFor(payload.grantId);
83+
const grant = (storage.get(grantKey) as string[]) || [];
84+
if (!grant.includes(key)) grant.push(key);
85+
storage.set(grantKey, grant);
86+
}
87+
88+
if (payload.userCode) {
89+
storage.set(userCodeKeyFor(payload.userCode), id);
90+
}
9191

92+
storage.set(key, payload, {ttl: expiresIn * 1000});
9293
return Promise.resolve();
9394
}
9495

9596
findByUid(uid: string): Promise<AdapterPayload | void | undefined> {
96-
for(const [_, value] of storage.entries()){
97-
if(typeof value ==="object" && "uid" in value && value.uid === uid){
98-
return Promise.resolve(value);
97+
for (const [_, value] of storage.entries()) {
98+
if (typeof value === "object" && "uid" in value && value.uid === uid) {
99+
return Promise.resolve(value as AdapterPayload);
99100
}
100101
}
101102
return Promise.resolve(undefined);
102103
}
103104

104105
revokeByGrantId(grantId: string): Promise<void | undefined> {
105106
const grantKey = grantKeyFor(grantId);
106-
const grant = storage.get(grantKey) as string[];
107+
const grant = storage.get(grantKey) as string[] | undefined;
107108
if (grant) {
108109
grant.forEach((token) => storage.delete(token));
109110
storage.delete(grantKey);
@@ -112,4 +113,4 @@ class MemoryAdapter implements Adapter{
112113
}
113114
}
114115

115-
export default MemoryAdapter
116+
export default MemoryAdapter;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Regression tests for OIDCAdapter (MemoryAdapter).
3+
*
4+
* Covers the four flows that were silently broken before the fix:
5+
* 1. upsert → revokeByGrantId actually clears tokens
6+
* 2. upsert → findByUserCode returns the payload
7+
* 3. destroy clears the userCode mapping
8+
* 4. consume on a missing id does not throw
9+
*/
10+
11+
import {describe, it} from 'mocha';
12+
import {strict as assert} from 'assert';
13+
import MemoryAdapter from '../../../node/security/OIDCAdapter';
14+
15+
describe('OIDCAdapter', () => {
16+
let adapter: InstanceType<typeof MemoryAdapter>;
17+
18+
beforeEach(() => {
19+
adapter = new MemoryAdapter('Session');
20+
});
21+
22+
it('revokeByGrantId clears all tokens sharing the grant', async () => {
23+
const grantId = 'grant-1';
24+
25+
await adapter.upsert('tok-a', {grantId, jti: 'tok-a'} as any, 60);
26+
await adapter.upsert('tok-b', {grantId, jti: 'tok-b'} as any, 60);
27+
28+
await adapter.revokeByGrantId(grantId);
29+
30+
assert.equal(await adapter.find('tok-a'), undefined);
31+
assert.equal(await adapter.find('tok-b'), undefined);
32+
});
33+
34+
it('findByUserCode returns the payload after upsert', async () => {
35+
const userCode = 'USER-CODE-123';
36+
37+
await adapter.upsert('dc-tok', {userCode, jti: 'dc-tok'} as any, 60);
38+
39+
const result = await adapter.findByUserCode(userCode);
40+
assert.ok(result, 'expected payload to be defined');
41+
assert.equal((result as any).jti, 'dc-tok');
42+
});
43+
44+
it('destroy removes the userCode mapping', async () => {
45+
const userCode = 'USER-CODE-456';
46+
47+
await adapter.upsert('dc-tok-2', {userCode, jti: 'dc-tok-2'} as any, 60);
48+
await adapter.destroy('dc-tok-2');
49+
50+
const result = await adapter.findByUserCode(userCode);
51+
assert.equal(result, undefined);
52+
});
53+
54+
it('consume on a missing id does not throw', async () => {
55+
await assert.doesNotReject(() => adapter.consume('non-existent-id'));
56+
});
57+
});

0 commit comments

Comments
 (0)