Skip to content

Commit 976666e

Browse files
Fix: Blocking background OIDC token refresh (#33)
1 parent 76a1431 commit 976666e

File tree

7 files changed

+82
-37
lines changed

7 files changed

+82
-37
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"/types/**/*.d.ts"
1414
],
1515
"scripts": {
16-
"test": "tsc -noEmit -p tsconfig-test.json && jest --useStderr --runInBand --detectOpenHandles --forceExit",
16+
"test": "tsc -noEmit -p tsconfig-test.json && jest --useStderr --runInBand --detectOpenHandles",
1717
"build": "npm run lint && tsc --emitDeclarationOnly && ./build.js",
1818
"prepack": "npm run build",
1919
"lint": "eslint --ext .ts,.js .",

src/classifications/scheduler.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -95,23 +95,27 @@ export default class ClassificationsScheduler extends CommandBase {
9595

9696
pollForCompletion = (id: any): Promise<Classification> => {
9797
return new Promise((resolve, reject) => {
98-
setTimeout(
99-
() =>
100-
reject(
101-
new Error(
102-
"classification didn't finish within configured timeout, " +
103-
'set larger timeout with .withWaitTimeout(timeout)'
104-
)
105-
),
106-
this.waitTimeout
107-
);
108-
109-
setInterval(() => {
98+
const timeout = setTimeout(() => {
99+
clearInterval(interval);
100+
clearTimeout(timeout);
101+
reject(
102+
new Error(
103+
"classification didn't finish within configured timeout, " +
104+
'set larger timeout with .withWaitTimeout(timeout)'
105+
)
106+
);
107+
}, this.waitTimeout);
108+
109+
const interval = setInterval(() => {
110110
new ClassificationsGetter(this.client)
111111
.withId(id)
112112
.do()
113113
.then((res: Classification) => {
114-
if (res.status === 'completed') resolve(res);
114+
if (res.status === 'completed') {
115+
clearInterval(interval);
116+
clearTimeout(timeout);
117+
resolve(res);
118+
}
115119
});
116120
}, 500);
117121
});

src/connection/auth.ts

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@ interface AuthenticatorResult {
66
refreshToken: string;
77
}
88

9+
interface OidcCredentials {
10+
silentRefresh: boolean;
11+
}
12+
913
export interface OidcAuthFlow {
1014
refresh: () => Promise<AuthenticatorResult>;
1115
}
1216

1317
export class OidcAuthenticator {
1418
private readonly http: HttpClient;
15-
private readonly creds: any;
19+
private readonly creds: OidcCredentials;
1620
private accessToken: string;
1721
private refreshToken?: string;
1822
private expiresAt: number;
1923
private refreshRunning: boolean;
24+
private refreshInterval!: NodeJS.Timeout;
2025

2126
constructor(http: HttpClient, creds: any) {
2227
this.http = http;
@@ -57,10 +62,7 @@ export class OidcAuthenticator {
5762
this.accessToken = resp.accessToken;
5863
this.expiresAt = resp.expiresAt;
5964
this.refreshToken = resp.refreshToken;
60-
if (!this.refreshRunning && this.refreshTokenProvided()) {
61-
this.runBackgroundTokenRefresh(authenticator);
62-
this.refreshRunning = true;
63-
}
65+
this.startTokenRefresh(authenticator);
6466
});
6567
};
6668

@@ -75,17 +77,25 @@ export class OidcAuthenticator {
7577
});
7678
};
7779

78-
runBackgroundTokenRefresh = (authenticator: { refresh: () => any }) => {
79-
setInterval(async () => {
80-
// check every 30s if the token will expire in <= 1m,
81-
// if so, refresh
82-
if (this.expiresAt - Date.now() <= 60_000) {
83-
const resp = await authenticator.refresh();
84-
this.accessToken = resp.accessToken;
85-
this.expiresAt = resp.expiresAt;
86-
this.refreshToken = resp.refreshToken;
87-
}
88-
}, 30_000);
80+
startTokenRefresh = (authenticator: { refresh: () => any }) => {
81+
if (this.creds.silentRefresh && !this.refreshRunning && this.refreshTokenProvided()) {
82+
this.refreshInterval = setInterval(async () => {
83+
// check every 30s if the token will expire in <= 1m,
84+
// if so, refresh
85+
if (this.expiresAt - Date.now() <= 60_000) {
86+
const resp = await authenticator.refresh();
87+
this.accessToken = resp.accessToken;
88+
this.expiresAt = resp.expiresAt;
89+
this.refreshToken = resp.refreshToken;
90+
}
91+
}, 30_000);
92+
this.refreshRunning = true;
93+
}
94+
};
95+
96+
stopTokenRefresh = () => {
97+
clearInterval(this.refreshInterval);
98+
this.refreshRunning = false;
8999
};
90100

91101
refreshTokenProvided = () => {
@@ -109,16 +119,19 @@ export interface UserPasswordCredentialsInput {
109119
username: string;
110120
password?: string;
111121
scopes?: any[];
122+
silentRefresh?: boolean;
112123
}
113124

114-
export class AuthUserPasswordCredentials {
125+
export class AuthUserPasswordCredentials implements OidcCredentials {
115126
private username: string;
116127
private password?: string;
117128
private scopes?: any[];
129+
public readonly silentRefresh: boolean;
118130
constructor(creds: UserPasswordCredentialsInput) {
119131
this.username = creds.username;
120132
this.password = creds.password;
121133
this.scopes = creds.scopes;
134+
this.silentRefresh = parseSilentRefresh(creds.silentRefresh);
122135
}
123136
}
124137

@@ -190,18 +203,21 @@ export interface AccessTokenCredentialsInput {
190203
accessToken: string;
191204
expiresIn: number;
192205
refreshToken?: string;
206+
silentRefresh?: boolean;
193207
}
194208

195-
export class AuthAccessTokenCredentials {
209+
export class AuthAccessTokenCredentials implements OidcCredentials {
196210
public readonly accessToken: string;
197211
public readonly expiresAt: number;
198212
public readonly refreshToken?: string;
213+
public readonly silentRefresh: boolean;
199214

200215
constructor(creds: AccessTokenCredentialsInput) {
201216
this.validate(creds);
202217
this.accessToken = creds.accessToken;
203218
this.expiresAt = calcExpirationEpoch(creds.expiresIn);
204219
this.refreshToken = creds.refreshToken;
220+
this.silentRefresh = parseSilentRefresh(creds.silentRefresh);
205221
}
206222

207223
validate = (creds: AccessTokenCredentialsInput) => {
@@ -270,15 +286,18 @@ class AccessTokenAuthenticator implements OidcAuthFlow {
270286
export interface ClientCredentialsInput {
271287
clientSecret: string;
272288
scopes?: any[];
289+
silentRefresh?: boolean;
273290
}
274291

275-
export class AuthClientCredentials {
292+
export class AuthClientCredentials implements OidcCredentials {
276293
private clientSecret: any;
277294
private scopes?: any[];
295+
public readonly silentRefresh: boolean;
278296

279297
constructor(creds: ClientCredentialsInput) {
280298
this.clientSecret = creds.clientSecret;
281299
this.scopes = creds.scopes;
300+
this.silentRefresh = parseSilentRefresh(creds.silentRefresh);
282301
}
283302
}
284303

@@ -345,3 +364,12 @@ export class ApiKey {
345364
function calcExpirationEpoch(expiresIn: number): number {
346365
return Date.now() + (expiresIn - 2) * 1000; // -2 for some lag
347366
}
367+
368+
function parseSilentRefresh(silentRefresh: boolean | undefined): boolean {
369+
// Silent token refresh by default
370+
if (silentRefresh === undefined) {
371+
return true;
372+
} else {
373+
return silentRefresh;
374+
}
375+
}

src/connection/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { Variables } from 'graphql-request';
88

99
export default class Connection {
1010
private apiKey?: string;
11-
private oidcAuth?: OidcAuthenticator;
1211
private authEnabled: boolean;
1312
private gql: GraphQLClient;
1413
public readonly http: HttpClient;
14+
public oidcAuth?: OidcAuthenticator;
1515

1616
constructor(params: ConnectionParams) {
1717
params = this.sanitizeParams(params);

src/connection/journey.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('connection', () => {
2121
authClientSecret: new AuthUserPasswordCredentials({
2222
username: 'ms_2d0e007e7136de11d5f29fce7a53dae219a51458@existiert.net',
2323
password: process.env.WCS_DUMMY_CI_PW,
24+
silentRefresh: false,
2425
}),
2526
});
2627

@@ -46,6 +47,7 @@ describe('connection', () => {
4647
host: 'localhost:8081',
4748
authClientSecret: new AuthClientCredentials({
4849
clientSecret: process.env.AZURE_CLIENT_SECRET,
50+
silentRefresh: false,
4951
}),
5052
});
5153

@@ -72,6 +74,7 @@ describe('connection', () => {
7274
authClientSecret: new AuthClientCredentials({
7375
clientSecret: process.env.OKTA_CLIENT_SECRET,
7476
scopes: ['some_scope'],
77+
silentRefresh: false,
7578
}),
7679
});
7780

@@ -98,6 +101,7 @@ describe('connection', () => {
98101
authClientSecret: new AuthUserPasswordCredentials({
99102
username: 'test@test.de',
100103
password: process.env.OKTA_DUMMY_CI_PW,
104+
silentRefresh: false,
101105
}),
102106
});
103107

@@ -124,6 +128,7 @@ describe('connection', () => {
124128
authClientSecret: new AuthUserPasswordCredentials({
125129
username: 'ms_2d0e007e7136de11d5f29fce7a53dae219a51458@existiert.net',
126130
password: process.env.WCS_DUMMY_CI_PW,
131+
silentRefresh: false,
127132
}),
128133
});
129134

@@ -168,6 +173,7 @@ describe('connection', () => {
168173
authClientSecret: new AuthUserPasswordCredentials({
169174
username: 'ms_2d0e007e7136de11d5f29fce7a53dae219a51458@existiert.net',
170175
password: process.env.WCS_DUMMY_CI_PW,
176+
silentRefresh: false,
171177
}),
172178
});
173179
// obtain access token with user/pass so we can
@@ -189,6 +195,7 @@ describe('connection', () => {
189195
.do()
190196
.then((res: any) => {
191197
expect(res.version).toBeDefined();
198+
client.oidcAuth?.stopTokenRefresh();
192199
})
193200
.catch((e: any) => {
194201
throw new Error('it should not have errord: ' + e);
@@ -207,6 +214,7 @@ describe('connection', () => {
207214
authClientSecret: new AuthUserPasswordCredentials({
208215
username: 'ms_2d0e007e7136de11d5f29fce7a53dae219a51458@existiert.net',
209216
password: process.env.WCS_DUMMY_CI_PW,
217+
silentRefresh: false,
210218
}),
211219
});
212220
// obtain access token with user/pass so we can
@@ -231,6 +239,7 @@ describe('connection', () => {
231239
.then((resp) => {
232240
expect(resp).toBeDefined();
233241
expect(resp != '').toBeTruthy();
242+
conn.oidcAuth?.stopTokenRefresh();
234243
})
235244
.catch((e: any) => {
236245
throw new Error('it should not have errord: ' + e);

src/connection/unit.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('mock server auth tests', () => {
1717
authClientSecret: new AuthClientCredentials({
1818
clientSecret: 'supersecret',
1919
scopes: ['some_scope'],
20+
silentRefresh: false,
2021
}),
2122
});
2223

@@ -58,6 +59,7 @@ describe('mock server auth tests', () => {
5859
expect(token).toEqual('access_token_000');
5960
expect((conn as any).oidcAuth?.refreshToken).toEqual('refresh_token_000');
6061
expect((conn as any).oidcAuth?.expiresAt).toBeGreaterThan(Date.now());
62+
conn.oidcAuth?.stopTokenRefresh();
6163
})
6264
.catch((e) => {
6365
throw new Error('it should not have failed: ' + e);
@@ -94,6 +96,7 @@ describe('mock server auth tests', () => {
9496
expect(token).toEqual('access_token_000');
9597
expect((conn as any).oidcAuth?.refreshToken).toEqual('refresh_token_000');
9698
expect((conn as any).oidcAuth?.expiresAt).toBeGreaterThan(Date.now());
99+
conn.oidcAuth?.stopTokenRefresh();
97100
})
98101
.catch((e) => {
99102
throw new Error('it should not have failed: ' + e);

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
AuthAccessTokenCredentials,
1515
AuthClientCredentials,
1616
AuthUserPasswordCredentials,
17+
OidcAuthenticator,
1718
} from './connection/auth';
1819
import MetaGetter from './misc/metaGetter';
1920
import { EmbeddedDB, EmbeddedOptions } from './embedded';
@@ -38,6 +39,7 @@ export interface WeaviateClient {
3839
backup: Backup;
3940
cluster: Cluster;
4041
embedded?: EmbeddedDB;
42+
oidcAuth?: OidcAuthenticator;
4143
}
4244

4345
const app = {
@@ -67,9 +69,8 @@ const app = {
6769
cluster: cluster(conn),
6870
};
6971

70-
if (params.embedded) {
71-
ifc.embedded = new EmbeddedDB(params.embedded);
72-
}
72+
if (params.embedded) ifc.embedded = new EmbeddedDB(params.embedded);
73+
if (conn.oidcAuth) ifc.oidcAuth = conn.oidcAuth;
7374

7475
return ifc;
7576
},

0 commit comments

Comments
 (0)