Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(oauth): remove refresh token mechanism #30

Merged
merged 4 commits into from
Feb 15, 2021
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
4 changes: 1 addition & 3 deletions packages/cli/src/commands/auth/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ describe('auth:login', () => {
getToken: () =>
Promise.resolve({
accessToken: 'this-is-the-token',
refreshToken: 'this-is-the-refresh-token',
}),
} as OAuth)
);
Expand Down Expand Up @@ -85,8 +84,7 @@ describe('auth:login', () => {
.command(['auth:login', '-o', 'foo'])
.it('save token from oauth service', () => {
expect(mockedStorage.mock.instances[0].save).toHaveBeenCalledWith(
'this-is-the-token',
'this-is-the-refresh-token'
'this-is-the-token'
);
});
});
Expand Down
72 changes: 51 additions & 21 deletions packages/cli/src/commands/auth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,43 +47,73 @@ export default class Login extends Command {
};

async run() {
const {flags} = this.parse(Login);
const {accessToken, refreshToken} = await new OAuth({
environment: flags.environment as PlatformEnvironment,
region: flags.region as PlatformRegion,
}).getToken();
await this.loginAndPersistToken();
await this.persistRegionAndEnvironment();
await this.persistOrganization();

await new Storage().save(accessToken, refreshToken!);
const cfg = new Config(this.config.configDir, this.error);
await cfg.set('environment', flags.environment as PlatformEnvironment);
await cfg.set('region', flags.region as PlatformRegion);
if (flags.organization) {
await cfg.set('organization', flags.organization);
} else {
const firstOrgAvailable = await this.pickFirstAvailableOrganization();
if (firstOrgAvailable) {
await cfg.set('organization', firstOrgAvailable as string);
this.log(
`No organization specified.\nYou are currently logged in ${firstOrgAvailable}.\nIf you wish to specify an organization, use the --organization parameter.`
);
}
}
this.config.runHook('analytics', buildAnalyticsSuccessHook(this, flags));
}

async catch(err?: Error) {
const {flags} = this.parse(Login);
const flags = this.flags;
await this.config.runHook(
'analytics',
buildAnalyticsFailureHook(this, flags, err)
);
throw err;
}

private async loginAndPersistToken() {
const flags = this.flags;
const {accessToken} = await new OAuth({
environment: flags.environment as PlatformEnvironment,
region: flags.region as PlatformRegion,
}).getToken();

await new Storage().save(accessToken);
}

private async persistRegionAndEnvironment() {
const flags = this.flags;
const cfg = this.configuration;
await cfg.set('environment', flags.environment as PlatformEnvironment);
await cfg.set('region', flags.region as PlatformRegion);
}

private async persistOrganization() {
const flags = this.flags;
const cfg = this.configuration;

if (flags.organization) {
await cfg.set('organization', flags.organization);
return;
}

const firstOrgAvailable = await this.pickFirstAvailableOrganization();
if (firstOrgAvailable) {
await cfg.set('organization', firstOrgAvailable as string);
this.log(
`No organization specified.\nYou are currently logged in ${firstOrgAvailable}.\nIf you wish to specify an organization, use the --organization parameter.`
);
return;
}

this.log('You have no access to any organization in Coveo.');
}

private async pickFirstAvailableOrganization() {
const orgs = await (
await new AuthenticatedClient().getClient()
).organization.list();
return ((orgs as unknown) as OrganizationModel[])[0]?.id;
}

private get flags() {
const {flags} = this.parse(Login);
return flags;
}

private get configuration() {
return new Config(this.config.configDir, this.error);
}
}
10 changes: 3 additions & 7 deletions packages/cli/src/lib/decorators/authenticationRequired.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,9 @@ export default function AuthenticationRequired() {

const isExpired = await authenticatedClient.isExpired();
if (isExpired) {
try {
await authenticatedClient.refresh();
} catch (e) {
target.error(
'Authentication token is expired. Run coveo auth:login first.'
);
}
target.error(
'Authentication token is expired. Run coveo auth:login first.'
);
}

originalRunCommand.apply(this);
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/src/lib/oauth/oauth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ jest.mock('@openid/appauth', () => ({
performTokenRequest: () =>
Promise.resolve({
accessToken: 'the-access-token',
refreshToken: 'the-refresh-token',
}),
})),
}));
Expand All @@ -36,7 +35,6 @@ describe('OAuth', () => {
performTokenRequest: () =>
Promise.resolve({
accessToken: 'this-is-the-new-access-token',
refreshToken: 'the-refresh-token',
}),
}));
const {accessToken} = await new OAuth().getToken();
Expand Down
23 changes: 1 addition & 22 deletions packages/cli/src/lib/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
AuthorizationServiceConfigurationJson,
BaseTokenRequestHandler,
GRANT_TYPE_AUTHORIZATION_CODE,
GRANT_TYPE_REFRESH_TOKEN,
TokenRequest,
TokenResponse,
} from '@openid/appauth';
import {
NodeBasedHandler,
Expand Down Expand Up @@ -52,29 +50,10 @@ export class OAuth {
return await this.getAccessToken(config, code);
}

public async refreshToken(refresh_token: string): Promise<TokenResponse> {
const config = new AuthorizationServiceConfiguration({
...this.clientConfig,
...this.authServiceConfig,
});

const tokenHandler = new BaseTokenRequestHandler(new NodeRequestor());
const request = new TokenRequest({
...this.clientConfig,
grant_type: GRANT_TYPE_REFRESH_TOKEN,
refresh_token,
extras: {
client_secret: this.clientConfig.client_id,
},
});

return await tokenHandler.performTokenRequest(config, request);
}

private async getAccessToken(
configuration: AuthorizationServiceConfiguration,
code: string
): Promise<{accessToken: string; refreshToken?: string}> {
): Promise<{accessToken: string}> {
const tokenHandler = new BaseTokenRequestHandler(new NodeRequestor());

const request = new TokenRequest({
Expand Down
7 changes: 1 addition & 6 deletions packages/cli/src/lib/oauth/storage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,12 @@ describe('oauth storage', () => {
});

it('should use userInfo when saving to storage', async () => {
await new Storage().save('foo', 'bar');
await new Storage().save('foo');
expect(mockedSetPassword).toHaveBeenCalledWith(
expect.stringContaining('com.coveo.cli'),
'bob',
'foo'
);
expect(mockedSetPassword).toHaveBeenCalledWith(
expect.stringContaining('com.coveo.cli'),
'bob',
'bar'
);
});

it('should return value retrieved from storage as the token', async () => {
Expand Down
20 changes: 2 additions & 18 deletions packages/cli/src/lib/oauth/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import {userInfo} from 'os';
export class Storage {
public async get(): Promise<{
accessToken: string | null;
refreshToken: string | null;
}> {
const accessToken = await this.getAccessToken();
const refreshToken = await this.getRefreshToken();
return {accessToken, refreshToken};
return {accessToken};
}

public async save(accessToken: string, refreshToken: string) {
public async save(accessToken: string) {
await this.setAccessToken(accessToken);
await this.setRefreshToken(refreshToken);
}

private get serviceName() {
Expand All @@ -23,27 +20,14 @@ export class Storage {
private get serviceNameAccessToken() {
return `${this.serviceName}.access.token`;
}
private get serviceNameRefreshToken() {
return `${this.serviceName}.refresh.token`;
}

private getAccessToken() {
const currentAccount = userInfo().username;
return getPassword(this.serviceNameAccessToken, currentAccount);
}

private getRefreshToken() {
const currentAccount = userInfo().username;
return getPassword(this.serviceNameRefreshToken, currentAccount);
}

private setAccessToken(tok: string) {
const currentAccount = userInfo().username;
return setPassword(this.serviceNameAccessToken, currentAccount, tok);
}

private setRefreshToken(tok: string) {
const currentAccount = userInfo().username;
return setPassword(this.serviceNameRefreshToken, currentAccount, tok);
}
}
13 changes: 2 additions & 11 deletions packages/cli/src/lib/platform/authenticatedClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export class AuthenticatedClient {
}

async isLoggedIn() {
const {accessToken, refreshToken} = await this.storage.get();
return accessToken !== null && refreshToken !== null;
const {accessToken} = await this.storage.get();
return accessToken !== null;
}

async isExpired() {
Expand Down Expand Up @@ -54,15 +54,6 @@ export class AuthenticatedClient {
return new OAuth({environment, region});
}

async refresh() {
const {refreshToken} = await this.storage.get();
const {accessToken, refreshToken: newRefreshToken} = await (
await this.getOauth()
).refreshToken(refreshToken!);

await this.storage.save(accessToken, newRefreshToken!);
}

async get() {
const loggedIn = await this.isLoggedIn();
if (!loggedIn) {
Expand Down
19 changes: 10 additions & 9 deletions terraform/deployment/oauth/oauth.tf
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
provider "coveo" {
env = var.env
env = var.env
region = var.region
}

resource "random_password" "client_secret" {
length = 32
length = 32
special = true
}

resource coveo_oauth2_client "oauth2_cli_client" {
client_id = "cli"
display_name = "Coveo CLI"
client_secret = random_password.client_secret.result
auto_approve_scopes = ["full"]
scopes = ["full"]
authorized_grant_types = ["refresh_token", "authorization_code"]
registered_redirect_uri = ["http://127.0.0.1:32111"]
client_id = "cli"
display_name = "Coveo CLI"
client_secret = "cli"
auto_approve_scopes = ["full"]
scopes = ["full"]
authorized_grant_types = ["authorization_code"]
registered_redirect_uri = ["http://127.0.0.1:32111"]
access_token_validity_seconds = 432000
}