Skip to content

Commit

Permalink
fix: continue the login process when a browser cannot be opened (WSL) (
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume Gautreau authored Mar 4, 2024
1 parent 848f674 commit 01e2ac4
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 62 deletions.
19 changes: 18 additions & 1 deletion src/services/oidc/authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,36 @@ export default class OidcAuthenticator {

private readonly open: typeof Open;

private readonly logger: Logger;

constructor({
assertPresent,
openIdClient,
env,
process,
open,
logger,
}: {
assertPresent: (args: unknown) => void;
openIdClient: Client;
env: Record<string, string>;
process: NodeJS.Process;
open: typeof Open;
logger: Logger;
}) {
assertPresent({
openIdClient,
env,
process,
open,
logger,
});

this.openIdClient = openIdClient;
this.env = env;
this.process = process;
this.open = open;
this.logger = logger;
}

private async register() {
Expand Down Expand Up @@ -75,7 +81,7 @@ export default class OidcAuthenticator {
);
this.process.stdout.write(`Your confirmation code: ${flow.user_code}\n`);

await this.open(flow.verification_uri_complete);
await this.tryOpen(flow.verification_uri_complete);

return await flow.poll();
} catch (e) {
Expand All @@ -91,6 +97,17 @@ export default class OidcAuthenticator {
}
}

private async tryOpen(url: string) {
try {
await this.open(url);
} catch (e) {
this.logger.log(
this.logger.WARN,
`Unable to open the browser: ${e.message}. Please open the link manually.`,
);
}
}

public async authenticate() {
const client = await this.register();

Expand Down
148 changes: 87 additions & 61 deletions test/services/oidc/authenticator.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,75 @@ import OidcAuthenticator from '../../../src/services/oidc/authenticator';
import OidcError from '../../../src/services/oidc/error';

describe('services > Oidc > Authenticator', () => {
describe('authenticate', () => {
function setupTest() {
const flow = {
poll: jest.fn(),
expired: jest.fn(),
verification_uri: 'https://verification.forest',
verification_uri_complete: 'https://verification.forest?user_code=ABCD',
user_code: 'ABC',
expires_in: 100,
};

const client = {
deviceAuthorization: jest.fn(),
};

const issuer = {
Client: {
register: jest.fn(),
},
};

const open = jest.fn();

const context = {
assertPresent: jest.fn(),
env: {
FOREST_SERVER_URL: 'https://forest.admin',
},
process: {
stdout: {
write: jest.fn(),
},
function setupTest() {
const flow = {
poll: jest.fn(),
expired: jest.fn(),
verification_uri: 'https://verification.forest',
verification_uri_complete: 'https://verification.forest?user_code=ABCD',
user_code: 'ABC',
expires_in: 100,
};

const client = {
deviceAuthorization: jest.fn(),
};

const issuer = {
Client: {
register: jest.fn(),
},
};

const open = jest.fn();

const context = {
assertPresent: jest.fn(),
env: {
FOREST_SERVER_URL: 'https://forest.admin',
},
process: {
stdout: {
write: jest.fn(),
},
openIdClient: {
Issuer: {
discover: jest.fn().mockReturnValue(issuer),
},
},
openIdClient: {
Issuer: {
discover: jest.fn().mockReturnValue(issuer),
},
open,
};

const authenticator = new OidcAuthenticator(
context as unknown as ConstructorParameters<typeof OidcAuthenticator>[0],
);
return {
...context,
authenticator,
client,
issuer,
flow,
};
}

it('should call assertPresent within constructor', async () => {
},
open,
logger: {
log: jest.fn(),
WARN: Symbol('warn'),
},
};

const authenticator = new OidcAuthenticator(
context as unknown as ConstructorParameters<typeof OidcAuthenticator>[0],
);

return {
...context,
authenticator,
client,
issuer,
flow,
context,
};
}

describe('dependencies', () => {
it('should assert that all dependencies are present', () => {
expect.assertions(1);
const { context } = setupTest();
const { assertPresent, ...dependencies } = context;

const context = setupTest();
const { openIdClient, env, process, open } = context;

expect(context.assertPresent).toHaveBeenCalledWith({
openIdClient,
env,
process,
open,
});
expect(assertPresent).toHaveBeenCalledWith(dependencies);
});
});

describe('authenticate', () => {
it('should successfully authenticate the user', async () => {
expect.assertions(7);
const { authenticator, issuer, client, flow, openIdClient, process, open } = setupTest();
Expand Down Expand Up @@ -214,5 +216,29 @@ describe('services > Oidc > Authenticator', () => {
);
await expect(promise).rejects.toHaveProperty('reason', undefined);
});

it('should log a warning when the browser cannot be opened', async () => {
expect.assertions(3);

const { open, authenticator, logger, openIdClient, issuer, client, flow } = setupTest();

openIdClient.Issuer.discover.mockResolvedValue(issuer);
issuer.Client.register.mockResolvedValue(client);
client.deviceAuthorization.mockResolvedValue(flow);

flow.poll.mockResolvedValue({
access_token: 'THE-TOKEN',
});
open.mockRejectedValue(new Error('The error'));

await expect(authenticator.authenticate()).resolves.toBe('THE-TOKEN');

expect(open).toHaveBeenCalledWith('https://verification.forest?user_code=ABCD');

expect(logger.log).toHaveBeenCalledWith(
logger.WARN,
'Unable to open the browser: The error. Please open the link manually.',
);
});
});
});

0 comments on commit 01e2ac4

Please sign in to comment.