From cbd158dc6d68f9ac43273a880188d681eb2b8cc3 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 7 Aug 2026 16:10:04 +0200 Subject: [PATCH 1/3] refactor: cleanup introspect logic --- packages/atlas-service/src/main.spec.ts | 91 ++++++++++++++++--------- packages/atlas-service/src/main.ts | 78 +++++++++++---------- packages/atlas-service/src/util.ts | 2 - 3 files changed, 98 insertions(+), 73 deletions(-) diff --git a/packages/atlas-service/src/main.spec.ts b/packages/atlas-service/src/main.spec.ts index 4a7a02905ac..a787adb6771 100644 --- a/packages/atlas-service/src/main.spec.ts +++ b/packages/atlas-service/src/main.spec.ts @@ -30,9 +30,6 @@ describe('CompassAuthServiceMain', function () { const mockFetch = sandbox.stub().callsFake((url: string) => { return { - 'http://example.com/tokens/introspect': { - ok: true, - }, 'http://example.com/tokens/revoke': { ok: true, }, @@ -187,32 +184,14 @@ describe('CompassAuthServiceMain', function () { }); describe('isAuthenticated', function () { - it('should return true if token is active', async function () { - CompassAuthService['fetch'] = sandbox.stub().resolves({ - ok: true, - json() { - return Promise.resolve({ active: true }); - }, - }) as any; + it('should return true if there is a current user', async function () { + CompassAuthService['currentUser'] = { sub: atlasUid }; expect(await CompassAuthService.isAuthenticated()).to.eq(true); }); - it('should return false if token is inactive', async function () { - CompassAuthService['fetch'] = sandbox.stub().resolves({ - ok: true, - json() { - return Promise.resolve({ active: false }); - }, - }) as any; - - expect(await CompassAuthService.isAuthenticated()).to.eq(false); - }); - - it('should return false if checking token fails', async function () { - CompassAuthService['fetch'] = sandbox - .stub() - .resolves({ ok: false, status: 500 }) as any; + it('should return false if there is no current user', async function () { + CompassAuthService['currentUser'] = null; expect(await CompassAuthService.isAuthenticated()).to.eq(false); }); @@ -229,6 +208,61 @@ describe('CompassAuthServiceMain', function () { }); }); + describe('restoreCurrentUser', function () { + function mockPluginWithCallback(callback: Sinon.SinonStub) { + CompassAuthService['plugin'] = { + mongoClientOptions: { + authMechanismProperties: { OIDC_HUMAN_CALLBACK: callback }, + }, + } as any; + } + + it('should restore the current user from the access token', async function () { + mockPluginWithCallback( + sandbox.stub().resolves({ accessToken, refreshToken }) + ); + + await CompassAuthService['restoreCurrentUser'](); + + expect(CompassAuthService['currentUser']).to.have.property( + 'sub', + atlasUid + ); + expect(await CompassAuthService.isAuthenticated()).to.eq(true); + }); + + it('should leave the current user unset if no token can be acquired', async function () { + mockPluginWithCallback( + sandbox.stub().rejects(new Error('Auth flows are not allowed')) + ); + + await CompassAuthService['restoreCurrentUser'](); + + expect(CompassAuthService['currentUser']).to.eq(null); + expect(await CompassAuthService.isAuthenticated()).to.eq(false); + }); + + it('should leave the current user unset if the access token cannot be parsed', async function () { + mockPluginWithCallback( + sandbox.stub().resolves({ accessToken: 'not-a-jwt', refreshToken }) + ); + + await CompassAuthService['restoreCurrentUser'](); + + expect(CompassAuthService['currentUser']).to.eq(null); + expect(await CompassAuthService.isAuthenticated()).to.eq(false); + }); + + it('should clear a previously signed in user if the token is gone', async function () { + CompassAuthService['currentUser'] = { sub: atlasUid }; + mockPluginWithCallback(sandbox.stub().resolves({ refreshToken })); + + await CompassAuthService['restoreCurrentUser'](); + + expect(CompassAuthService['currentUser']).to.eq(null); + }); + }); + describe('throwIfNotOk', function () { it('should not throw if res is ok', async function () { await throwIfNotOk({ @@ -299,12 +333,7 @@ describe('CompassAuthServiceMain', function () { await preferences.savePreferences({ networkTraffic: false }); }); - for (const methodName of [ - 'requestOAuthToken', - 'signIn', - 'introspect', - 'revoke', - ]) { + for (const methodName of ['requestOAuthToken', 'signIn', 'revoke']) { it(`${methodName} should throw`, async function () { try { await (CompassAuthService as any)[methodName]({}); diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index 0ec8b7a1cc7..c786bfc8d58 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -18,7 +18,7 @@ import { } from '@mongodb-js/oidc-plugin'; import { oidcServerRequestHandler } from '@mongodb-js/devtools-connect'; import type { Agent } from 'https'; -import type { IntrospectInfo, AtlasUserInfo, AtlasServiceConfig } from './util'; +import type { AtlasUserInfo, AtlasServiceConfig } from './util'; import { throwIfAborted } from '@mongodb-js/compass-utils'; import type { HadronIpcMain } from 'hadron-ipc'; import { ipcMain } from 'hadron-ipc'; @@ -132,9 +132,8 @@ export class CompassAuthService { private static getAllowedAuthFlows(): AuthFlowType[] { if (!this.signInPromise) { - throw new Error( - 'Auth flows are not allowed when sign in is not triggered by user' - ); + // This is not a sign in flow, most likely a token refresh - so we don't want to allow user interaction + return []; } return ['auth-code']; } @@ -175,7 +174,6 @@ export class CompassAuthService { if (this.ipcMain) { this.ipcMain.createHandle('AtlasService', this, [ 'getUserInfo', - 'introspect', 'isAuthenticated', 'signIn', 'signOut', @@ -190,6 +188,7 @@ export class CompassAuthService { ); const serializedState = await this.secretStore.getState(); this.setupPlugin(serializedState); + if (serializedState) await this.restoreCurrentUser(); })()); } @@ -237,14 +236,46 @@ export class CompassAuthService { }); } + /** + * Compass cannot use the introspect endpoint as it's a protected resource and Compass is an unauthenticated client. + * This method returns the last known state, which might be corrected if the next token refresh fails. + */ static async isAuthenticated({ signal, }: { signal?: AbortSignal } = {}): Promise { throwIfAborted(signal); + return !!this.currentUser; + } + + static async restoreCurrentUser(): Promise { try { - return (await this.introspect({ signal })).active; + const accessToken = await this.maybeGetToken({ + tokenType: 'accessToken', + }); + if (!accessToken) { + this.currentUser = null; + log.info( + mongoLogId(1_001_000_437), + 'AtlasService', + 'Did not restore sign in state', + { reason: 'No usable access token' } + ); + return; + } + this.currentUser = this.getUserInfoFromAccessToken(accessToken); + log.info( + mongoLogId(1_001_000_438), + 'AtlasService', + 'Restored sign in state from stored token' + ); } catch { - return false; + this.currentUser = null; + log.info( + mongoLogId(1_001_000_437), + 'AtlasService', + 'Did not restore sign in state', + { reason: 'Failed to parse access token' } + ); } } @@ -377,39 +408,6 @@ export class CompassAuthService { } } - static async introspect({ - signal, - tokenType, - }: { - signal?: AbortSignal; - tokenType?: 'accessToken' | 'refreshToken'; - } = {}) { - // TODO(COMPASS-7094): use the discovery endpoint instead of hardcoding this - this.throwIfNetworkTrafficDisabled(); - const url = new URL(`${this.config.atlasLogin.issuer}/tokens/introspect`); - url.searchParams.set('client_id', this.config.atlasLogin.clientId); - - tokenType ??= 'accessToken'; - - const token = await this.maybeGetToken({ signal, tokenType }); - - const res = await this.fetch(url.toString(), { - method: 'POST', - body: new URLSearchParams([ - ['token', token ?? ''], - ['token_type_hint', TOKEN_TYPE_TO_HINT[tokenType]], - ['client_id', this.config.atlasLogin.clientId], - ]), - headers: { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded', - }, - signal: signal, - }); - - return res.json() as Promise; - } - static async revoke({ signal, tokenType, diff --git a/packages/atlas-service/src/util.ts b/packages/atlas-service/src/util.ts index 78761645624..28db8de07ed 100644 --- a/packages/atlas-service/src/util.ts +++ b/packages/atlas-service/src/util.ts @@ -7,8 +7,6 @@ export type AtlasUserInfo = { sub: string; }; -export type IntrospectInfo = { active: boolean }; - export type Token = plugin.IdPServerResponse; // See: https://www.mongodb.com/docs/atlas/api/atlas-admin-api-ref/#errors From f9171a5d258d46b7e1a6bc085427acea82391425 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Fri, 7 Aug 2026 16:36:20 +0200 Subject: [PATCH 2/3] fixes --- packages/atlas-service/src/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index c786bfc8d58..d617b97d409 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -244,6 +244,7 @@ export class CompassAuthService { signal, }: { signal?: AbortSignal } = {}): Promise { throwIfAborted(signal); + await this.initPromise; return !!this.currentUser; } @@ -271,7 +272,7 @@ export class CompassAuthService { } catch { this.currentUser = null; log.info( - mongoLogId(1_001_000_437), + mongoLogId(1_001_000_439), 'AtlasService', 'Did not restore sign in state', { reason: 'Failed to parse access token' } From 8180345b22b812b21ee695df98c1f8e2abd7f882 Mon Sep 17 00:00:00 2001 From: Paula Stachova Date: Wed, 12 Aug 2026 20:13:14 +0200 Subject: [PATCH 3/3] revert to throwing --- packages/atlas-service/src/main.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index d617b97d409..b0d4e8c82c8 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -132,8 +132,9 @@ export class CompassAuthService { private static getAllowedAuthFlows(): AuthFlowType[] { if (!this.signInPromise) { - // This is not a sign in flow, most likely a token refresh - so we don't want to allow user interaction - return []; + throw new Error( + 'Auth flows are not allowed when sign in is not triggered by user' + ); } return ['auth-code']; }