Skip to content

Commit 8bcf009

Browse files
authored
fix(atlas-service): listen to state-updated before attempting token refresh (#4683)
1 parent 5e3deea commit 8bcf009

File tree

2 files changed

+69
-34
lines changed

2 files changed

+69
-34
lines changed

packages/atlas-service/src/main.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ describe('AtlasServiceMain', function () {
177177
AtlasService['oidcPluginLogger'].emit(
178178
'mongodb-oidc-plugin:refresh-succeeded'
179179
);
180+
AtlasService['oidcPluginLogger'].emit(
181+
'mongodb-oidc-plugin:state-updated'
182+
);
180183
})(),
181184
]);
182185
expect(authenticated).to.eq(true);
@@ -338,6 +341,9 @@ describe('AtlasServiceMain', function () {
338341
AtlasService['oidcPluginLogger'].emit(
339342
'mongodb-oidc-plugin:refresh-succeeded'
340343
);
344+
AtlasService['oidcPluginLogger'].emit(
345+
'mongodb-oidc-plugin:state-updated'
346+
);
341347
})(),
342348
]);
343349
expect(query).to.deep.eq({ test: 1 });
@@ -476,6 +482,7 @@ describe('AtlasServiceMain', function () {
476482
'oidcPluginSyncedFromLoggerState',
477483
'initial'
478484
);
485+
mockOidcPlugin.logger.emit('mongodb-oidc-plugin:state-updated');
479486
await once(
480487
AtlasService['oidcPluginLogger'],
481488
'atlas-service-token-refreshed'

packages/atlas-service/src/main.ts

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ export class AtlasService {
107107

108108
private static fetch: typeof fetch = fetch;
109109

110+
private static refreshing = false;
111+
110112
private static get clientId() {
111113
if (!process.env.COMPASS_CLIENT_ID) {
112114
throw new Error('COMPASS_CLIENT_ID is required');
@@ -164,44 +166,70 @@ export class AtlasService {
164166
// to the refresh-succeeded event and then kick off the "refresh"
165167
// programmatically to be able to get the actual tokens back and sync them
166168
// to the service state
167-
let refreshing = false;
168169
this.oidcPluginLogger.on('mongodb-oidc-plugin:refresh-succeeded', () => {
169-
// In case our call to REFRESH_TOKEN_CALLBACK somehow started another
170-
// token refresh instead of just returning token from the plugin state, we
171-
// short circuit if plugin logged a refresh-succeeded event and this
172-
// listener got triggered
173-
if (refreshing) {
174-
return;
175-
}
176-
refreshing = true;
177-
void this.plugin.mongoClientOptions.authMechanismProperties
178-
// WARN: in the oidc plugin refresh callback is actually the same method
179-
// as sign in, so calling it here means that potentially this can start
180-
// an actual sign in flow for the user instead of just trying to refresh
181-
// the token
182-
.REFRESH_TOKEN_CALLBACK(
183-
{ clientId: this.clientId, issuer: this.issuer },
184-
{
185-
// Required driver specific stuff
186-
version: 0,
187-
}
188-
)
189-
.then((token) => {
190-
this.token = token;
191-
this.oidcPluginSyncedFromLoggerState = 'authenticated';
192-
this.oidcPluginLogger.emit('atlas-service-token-refreshed');
193-
})
194-
.catch(() => {
195-
this.token = null;
196-
this.oidcPluginSyncedFromLoggerState = 'error';
197-
this.oidcPluginLogger.emit('atlas-service-token-refresh-failed');
198-
})
199-
.finally(() => {
200-
refreshing = false;
201-
});
170+
void this.refreshToken();
202171
});
203172
}
204173

174+
private static async refreshToken() {
175+
// In case our call to REFRESH_TOKEN_CALLBACK somehow started another
176+
// token refresh instead of just returning token from the plugin state, we
177+
// short circuit if plugin logged a refresh-succeeded event and this
178+
// listener got triggered
179+
if (this.refreshing) {
180+
return;
181+
}
182+
this.refreshing = true;
183+
try {
184+
await Promise.race([
185+
// When oidc-plugin logged that token was refreshed, the token is not
186+
// actually refreshed yet in the plugin state and so calling `REFRESH_TOKEN_CALLBACK`
187+
// causes weird behavior that actually opens the browser again, to work
188+
// around that we wait for the state update event in addition. We can't
189+
// guarantee that this event will be emitted for our particular state as
190+
// this is not something oidc-plugin exposes, but we can ignore this for
191+
// now as only one auth state is created in this instance of oidc-plugin
192+
once(this.oidcPluginLogger, 'mongodb-oidc-plugin:state-updated'),
193+
// At the same time refresh can still fail at this stage, so to avoid
194+
// refresh being stuck, we also wait for refresh-failed event and throw
195+
// if it happens to avoid calling `REFRESH_TOKEN_CALLBACK`
196+
once(this.oidcPluginLogger, 'mongodb-oidc-plugin:refresh-failed').then(
197+
() => {
198+
throw new Error('Refresh failed');
199+
}
200+
),
201+
]);
202+
try {
203+
const token =
204+
await this.plugin.mongoClientOptions.authMechanismProperties
205+
// WARN: in the oidc plugin refresh callback is actually the same
206+
// method as sign in, so calling it here means that potentially
207+
// this can start an actual sign in flow for the user instead of
208+
// just trying to refresh the token
209+
.REFRESH_TOKEN_CALLBACK(
210+
{ clientId: this.clientId, issuer: this.issuer },
211+
{
212+
// Required driver specific stuff
213+
version: 0,
214+
}
215+
);
216+
this.token = token;
217+
this.oidcPluginSyncedFromLoggerState = 'authenticated';
218+
this.oidcPluginLogger.emit('atlas-service-token-refreshed');
219+
} catch {
220+
// REFRESH_TOKEN_CALLBACK call failed for some reason
221+
this.token = null;
222+
this.oidcPluginSyncedFromLoggerState = 'error';
223+
this.oidcPluginLogger.emit('atlas-service-token-refresh-failed');
224+
}
225+
} catch {
226+
// encountered 'mongodb-oidc-plugin:refresh-failed' event, do nothing, we
227+
// already have a listener for this event
228+
} finally {
229+
this.refreshing = false;
230+
}
231+
}
232+
205233
private static async maybeWaitForToken({
206234
signal,
207235
}: { signal?: AbortSignal } = {}) {

0 commit comments

Comments
 (0)