Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 60 additions & 31 deletions packages/atlas-service/src/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down Expand Up @@ -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);
});
Expand All @@ -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({
Expand Down Expand Up @@ -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]({});
Expand Down
74 changes: 37 additions & 37 deletions packages/atlas-service/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -175,7 +175,6 @@ export class CompassAuthService {
if (this.ipcMain) {
this.ipcMain.createHandle('AtlasService', this, [
'getUserInfo',
'introspect',
'isAuthenticated',
'signIn',
'signOut',
Expand All @@ -190,6 +189,7 @@ export class CompassAuthService {
);
const serializedState = await this.secretStore.getState();
this.setupPlugin(serializedState);
if (serializedState) await this.restoreCurrentUser();
})());
}

Expand Down Expand Up @@ -237,14 +237,47 @@ 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<boolean> {
throwIfAborted(signal);
await this.initPromise;
return !!this.currentUser;
}
Comment thread
paula-stacho marked this conversation as resolved.

static async restoreCurrentUser(): Promise<void> {
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_439),
'AtlasService',
'Did not restore sign in state',
{ reason: 'Failed to parse access token' }
);
}
}

Expand Down Expand Up @@ -377,39 +410,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<IntrospectInfo>;
}

static async revoke({
signal,
tokenType,
Expand Down
2 changes: 0 additions & 2 deletions packages/atlas-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading