From 482607a6a610d11385a53f0dd3cb87c345a052c4 Mon Sep 17 00:00:00 2001 From: Stan Dzhumaev Date: Thu, 13 Feb 2025 09:31:42 -0800 Subject: [PATCH] [AXON-46] Add remote auth flow using atlascode-backend --- src/atlclients/authInfo.ts | 1 + src/atlclients/loginManager.ts | 17 + src/atlclients/oauthDancer.ts | 32 ++ src/atlclients/strategy.test.ts | 4 +- src/atlclients/strategy.ts | 316 +++++------------- src/atlclients/strategyData.ts | 188 +++++++++++ src/lib/ipc/fromUI/config.ts | 4 + .../config/configWebviewController.ts | 13 + .../config/auth/SiteAuthenticator.tsx | 26 +- src/react/atlascode/config/auth/SiteList.tsx | 2 +- .../atlascode/config/configController.ts | 11 + src/uriHandler/actions/simpleCallback.ts | 4 +- src/uriHandler/atlascodeUriHandler.ts | 4 + 13 files changed, 389 insertions(+), 233 deletions(-) create mode 100644 src/atlclients/strategyData.ts diff --git a/src/atlclients/authInfo.ts b/src/atlclients/authInfo.ts index dacfb71d..19af157a 100644 --- a/src/atlclients/authInfo.ts +++ b/src/atlclients/authInfo.ts @@ -41,6 +41,7 @@ export enum OAuthProvider { BitbucketCloudStaging = 'bbcloudstaging', JiraCloud = 'jiracloud', JiraCloudStaging = 'jiracloudstaging', + JiraCloudRemote = 'jiracloudremote', } export interface AuthInfoV1 { access: string; diff --git a/src/atlclients/loginManager.ts b/src/atlclients/loginManager.ts index 60c76f03..50b5e154 100644 --- a/src/atlclients/loginManager.ts +++ b/src/atlclients/loginManager.ts @@ -53,6 +53,23 @@ export class LoginManager { this.saveDetails(provider, site, resp, isOnboarding); } + public async initRemoteAuth(state: Object) { + this._dancer.doInitRemoteDance(state); + } + + public async finishRemoteAuth(code: string): Promise { + const provider = OAuthProvider.JiraCloudRemote; + const site = { + host: 'https://jira.atlassian.com', + product: ProductJira, + }; + + const resp = await this._dancer.doFinishRemoteDance(provider, site, code); + + // TODO: change false here when this is reachable from the onboarding flow + this.saveDetails(provider, site, resp, false); + } + private async saveDetails(provider: OAuthProvider, site: SiteInfo, resp: OAuthResponse, isOnboarding?: boolean) { try { const oauthInfo: OAuthInfo = { diff --git a/src/atlclients/oauthDancer.ts b/src/atlclients/oauthDancer.ts index 57280e84..e14b555e 100644 --- a/src/atlclients/oauthDancer.ts +++ b/src/atlclients/oauthDancer.ts @@ -96,6 +96,38 @@ export class OAuthDancer implements Disposable { return app; } + public async doInitRemoteDance(state: any) { + const provider = OAuthProvider.JiraCloudRemote; + const strategy = strategyForProvider(provider); + + const stateBase64 = Buffer.from(JSON.stringify(state)).toString('base64'); + const uri = vscode.Uri.parse(strategy.authorizeUrl(stateBase64)); + vscode.window.showInformationMessage(`Opening browser to ${uri.toString(true)}`); + vscode.env.openExternal(uri); + } + + public async doFinishRemoteDance(provider: OAuthProvider, site: SiteInfo, code: string): Promise { + const strategy = strategyForProvider(provider); + const agent = getAgent(site); + const responseHandler = responseHandlerForStrategy(strategy!, agent, this._axios); + const tokens = await responseHandler.tokens(code as string); + const accessibleResources = await responseHandler.accessibleResources(tokens.accessToken); + if (accessibleResources.length === 0) { + throw new Error(`No accessible resources found for ${provider}`); + } + const user = await responseHandler.user(tokens.accessToken, accessibleResources[0]); + + return { + access: tokens.accessToken, + refresh: tokens.refreshToken!, + expirationDate: tokens.expiration, + iat: tokens.iat, + receivedAt: tokens.receivedAt, + user: user, + accessibleResources: accessibleResources, + }; + } + public async doDance(provider: OAuthProvider, site: SiteInfo, callback: string): Promise { const currentlyInflight = this._authsInFlight.get(provider); if (currentlyInflight) { diff --git a/src/atlclients/strategy.test.ts b/src/atlclients/strategy.test.ts index 348f435a..fe81faf3 100644 --- a/src/atlclients/strategy.test.ts +++ b/src/atlclients/strategy.test.ts @@ -60,7 +60,7 @@ const expectedData = { }, tokenRefreshData: '{"grant_type":"refresh_token","client_id":"bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2","refresh_token":"refreshToken"}', - profileUrl: '', + profileUrl: 'https://api.atlassian.com/me', emailsUrl: '', }, jiracloudstaging: { @@ -77,7 +77,7 @@ const expectedData = { }, tokenRefreshData: '{"grant_type":"refresh_token","client_id":"pmzXmUav3Rr5XEL0Sie7Biec0WGU8BKg","refresh_token":"refreshToken"}', - profileUrl: '', + profileUrl: 'https://api.stg.atlassian.com/me', emailsUrl: '', }, }; diff --git a/src/atlclients/strategy.ts b/src/atlclients/strategy.ts index cd9c3da5..e2c4a4b2 100644 --- a/src/atlclients/strategy.ts +++ b/src/atlclients/strategy.ts @@ -1,208 +1,101 @@ -import { OAuthProvider } from './authInfo'; import { createVerifier, base64URLEncode, sha256, basicAuth } from './strategyCrypto'; - -const JiraProdStrategyData = { - clientID: 'bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2', - clientSecret: '', - authorizationURL: 'https://auth.atlassian.com/authorize', - tokenURL: 'https://auth.atlassian.com/oauth/token', - profileURL: 'https://api.atlassian.com/me', - accessibleResourcesURL: 'https://api.atlassian.com/oauth/token/accessible-resources', - callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.JiraCloud, - scope: 'read:jira-user read:jira-work write:jira-work offline_access manage:jira-project', - authParams: { - audience: 'api.atlassian.com', - prompt: 'consent', - }, -}; - -const JiraStagingStrategyData = { - clientID: 'pmzXmUav3Rr5XEL0Sie7Biec0WGU8BKg', - clientSecret: '', - authorizationURL: 'https://auth.stg.atlassian.com/authorize', - tokenURL: 'https://auth.stg.atlassian.com/oauth/token', - profileURL: 'https://api.stg.atlassian.com/me', - accessibleResourcesURL: 'https://api.stg.atlassian.com/oauth/token/accessible-resources', - callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.JiraCloudStaging, - scope: 'read:jira-user read:jira-work write:jira-work offline_access manage:jira-project', - authParams: { - audience: 'api.stg.atlassian.com', - prompt: 'consent', - }, -}; - -const BitbucketProdStrategyData = { - clientID: '3hasX42a7Ugka2FJja', - clientSecret: 'st7a4WtBYVh7L2mZMU8V5ehDtvQcWs9S', - authorizationURL: 'https://bitbucket.org/site/oauth2/authorize', - tokenURL: 'https://bitbucket.org/site/oauth2/access_token', - profileURL: 'https://api.bitbucket.org/2.0/user', - emailsURL: 'https://api.bitbucket.org/2.0/user/emails', - callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.BitbucketCloud, -}; - -const BitbucketStagingStrategyData = { - clientID: '7jspxC7fgemuUbnWQL', - clientSecret: 'sjHugFh6SVVshhVE7PUW3bgXbbQDVjJD', - authorizationURL: 'https://staging.bb-inf.net/site/oauth2/authorize', - tokenURL: 'https://staging.bb-inf.net/site/oauth2/access_token', - profileURL: 'https://api-staging.bb-inf.net/2.0/user', - emailsURL: 'https://api-staging.bb-inf.net/2.0/user/emails', - callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.BitbucketCloudStaging, -}; +import { OAuthProvider } from './authInfo'; +import { StrategyProps, OAuthStrategyData } from './strategyData'; export function strategyForProvider(provider: OAuthProvider): Strategy { switch (provider) { case OAuthProvider.JiraCloud: { - return new PKCEJiraProdStrategy(); + return new JiraStrategy(OAuthStrategyData.JiraProd); } case OAuthProvider.JiraCloudStaging: { - return new PKCEJiraStagingStrategy(); + return new JiraStrategy(OAuthStrategyData.JiraStaging); } case OAuthProvider.BitbucketCloud: { - return new BitbucketProdStrategy(); + return new BitbucketStrategy(OAuthStrategyData.BitbucketProd); } case OAuthProvider.BitbucketCloudStaging: { - return new BitbucketStagingStrategy(); + return new BitbucketStrategy(OAuthStrategyData.BitbucketStaging); + } + case OAuthProvider.JiraCloudRemote: { + return new JiraDevStrategy(OAuthStrategyData.JiraRemote); + } + default: { + throw new Error(`Unknown provider: ${provider}`); } } } export abstract class Strategy { - public abstract provider(): OAuthProvider; - public abstract authorizeUrl(state: string): string; - public abstract accessibleResourcesUrl(): string; - public abstract tokenAuthorizationData(code: string): string; - public abstract tokenUrl(): string; - public abstract apiUrl(): string; - public abstract refreshHeaders(): any; - public abstract tokenRefreshData(refreshToken: string): string; - public profileUrl(): string { - return ''; - } - public emailsUrl(): string { - return ''; - } -} - -class PKCEJiraProdStrategy extends Strategy { - private verifier: string; + verifier: string; + data: StrategyProps; - public constructor() { - super(); + public constructor(data: StrategyProps) { + this.data = data; this.verifier = createVerifier(); } public provider(): OAuthProvider { - return OAuthProvider.JiraCloud; - } - - public authorizeUrl(state: string): string { - const codeChallenge = base64URLEncode(sha256(this.verifier)); - const params = new URLSearchParams(); - params.append('client_id', JiraProdStrategyData.clientID); - params.append('redirect_uri', JiraProdStrategyData.callbackURL); - params.append('response_type', 'code'); - params.append('scope', JiraProdStrategyData.scope); - params.append('audience', JiraProdStrategyData.authParams.audience); - params.append('prompt', JiraProdStrategyData.authParams.prompt); - params.append('state', state); - params.append('code_challenge', codeChallenge); - params.append('code_challenge_method', 'S256'); - return JiraProdStrategyData.authorizationURL + '?' + params.toString(); + return this.data.provider; } public accessibleResourcesUrl(): string { - return JiraProdStrategyData.accessibleResourcesURL; + return this.data.accessibleResourcesURL || ''; } public tokenUrl(): string { - return JiraProdStrategyData.tokenURL; + return this.data.tokenURL; } public apiUrl(): string { - return 'api.atlassian.com'; + return this.data.apiURL; } - public refreshHeaders() { - return { - 'Content-Type': 'application/json', - }; + public profileUrl(): string { + return this.data.profileURL || ''; } - public tokenAuthorizationData(code: string): string { - const data = JSON.stringify({ - grant_type: 'authorization_code', - code: code, - redirect_uri: JiraProdStrategyData.callbackURL, - client_id: JiraProdStrategyData.clientID, - code_verifier: this.verifier, - }); - return data; + public emailsUrl(): string { + return this.data.emailsURL || ''; } - public tokenRefreshData(refreshToken: string): string { - const dataString = JSON.stringify({ - grant_type: 'refresh_token', - client_id: JiraProdStrategyData.clientID, - refresh_token: refreshToken, - }); - return dataString; - } + abstract authorizeUrl(state: string): string; + abstract tokenAuthorizationData(code: string): string; + abstract refreshHeaders(): any; + abstract tokenRefreshData(refreshToken: string): string; } -class PKCEJiraStagingStrategy extends Strategy { - private verifier: string; - - public constructor() { - super(); - this.verifier = createVerifier(); +export class JiraStrategy extends Strategy { + public constructor(data: StrategyProps) { + super(data); } - public provider(): OAuthProvider { - return OAuthProvider.JiraCloudStaging; - } + public authorizeUrl(state: string): string { + if (!this.data.scope || !this.data.authParams) { + throw new Error('No scope or authParams for this strategy'); + } - public authorizeUrl(state: string) { const codeChallenge = base64URLEncode(sha256(this.verifier)); - const params = new URLSearchParams(); - params.append('client_id', JiraStagingStrategyData.clientID); - params.append('redirect_uri', JiraStagingStrategyData.callbackURL); - params.append('response_type', 'code'); - params.append('scope', JiraStagingStrategyData.scope); - params.append('audience', JiraStagingStrategyData.authParams.audience); - params.append('prompt', JiraStagingStrategyData.authParams.prompt); - params.append('state', state); - params.append('code_challenge', codeChallenge); - params.append('code_challenge_method', 'S256'); - - return JiraStagingStrategyData.authorizationURL + '?' + params.toString(); - } - - public tokenUrl(): string { - return JiraStagingStrategyData.tokenURL; - } - - public apiUrl(): string { - return 'api.stg.atlassian.com'; - } + const params = new URLSearchParams({ + client_id: this.data.clientID, + redirect_uri: this.data.callbackURL, + response_type: 'code', + scope: this.data.scope, + audience: this.data.authParams.audience, + prompt: this.data.authParams.prompt, + state: state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }); - public accessibleResourcesUrl(): string { - return JiraStagingStrategyData.accessibleResourcesURL; - } - - public refreshHeaders() { - return { - 'Content-Type': 'application/json', - }; + return this.data.authorizationURL + '?' + params.toString(); } public tokenAuthorizationData(code: string): string { const data = JSON.stringify({ grant_type: 'authorization_code', code: code, - redirect_uri: JiraStagingStrategyData.callbackURL, - client_id: JiraStagingStrategyData.clientID, + redirect_uri: this.data.callbackURL, + client_id: this.data.clientID, code_verifier: this.verifier, }); return data; @@ -211,110 +104,81 @@ class PKCEJiraStagingStrategy extends Strategy { public tokenRefreshData(refreshToken: string): string { const dataString = JSON.stringify({ grant_type: 'refresh_token', - client_id: JiraStagingStrategyData.clientID, + client_id: this.data.clientID, refresh_token: refreshToken, }); return dataString; } -} -class BitbucketProdStrategy extends Strategy { - public provider(): OAuthProvider { - return OAuthProvider.BitbucketCloud; - } - - public authorizeUrl(state: string): string { - const url = new URL(BitbucketProdStrategyData.authorizationURL); - url.searchParams.append('client_id', BitbucketProdStrategyData.clientID); - url.searchParams.append('response_type', 'code'); - url.searchParams.append('state', state); - - return url.toString(); - } - - public accessibleResourcesUrl(): string { - return ''; - } - - public tokenAuthorizationData(code: string): string { - return `grant_type=authorization_code&code=${code}`; - } - - public tokenUrl(): string { - return BitbucketProdStrategyData.tokenURL; - } - - public apiUrl(): string { - return 'https://bitbucket.org'; - } - - // We kinda abuse refreshHeaders for bitbucket. Maybe have a authorizationHeaders as well? Just rename? public refreshHeaders() { return { - 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: basicAuth(BitbucketProdStrategyData.clientID, BitbucketProdStrategyData.clientSecret), + 'Content-Type': 'application/json', }; } +} - public tokenRefreshData(refreshToken: string): string { - return `grant_type=refresh_token&refresh_token=${refreshToken}`; - } +export class JiraDevStrategy extends JiraStrategy { + public tokenAuthorizationData(code: string): string { + const data = JSON.stringify({ + grant_type: 'authorization_code', + code: code, + redirect_uri: this.data.callbackURL, + client_id: this.data.clientID, + client_secret: this.data.clientSecret, + }); - public profileUrl(): string { - return BitbucketProdStrategyData.profileURL; + return data; } - public emailsUrl(): string { - return BitbucketProdStrategyData.emailsURL; + public authorizeUrl(state: string): string { + if (!this.data.scope || !this.data.authParams) { + throw new Error('No scope or authParams for this strategy'); + } + + const params = new URLSearchParams({ + client_id: this.data.clientID, + redirect_uri: this.data.callbackURL, + response_type: 'code', + scope: this.data.scope, + audience: this.data.authParams.audience, + prompt: this.data.authParams.prompt, + state: state, + }); + + return this.data.authorizationURL + '?' + params.toString(); } } -class BitbucketStagingStrategy extends Strategy { - public provider(): OAuthProvider { - return OAuthProvider.BitbucketCloudStaging; +export class BitbucketStrategy extends Strategy { + public constructor(data: StrategyProps) { + super(data); } public authorizeUrl(state: string): string { - const url = new URL(BitbucketStagingStrategyData.authorizationURL); - url.searchParams.append('client_id', BitbucketStagingStrategyData.clientID); + const url = new URL(this.data.authorizationURL); + url.searchParams.append('client_id', this.data.clientID); url.searchParams.append('response_type', 'code'); url.searchParams.append('state', state); return url.toString(); } - public accessibleResourcesUrl(): string { - return ''; - } - public tokenAuthorizationData(code: string): string { return `grant_type=authorization_code&code=${code}`; } - public tokenUrl(): string { - return BitbucketStagingStrategyData.tokenURL; - } - - public apiUrl(): string { - return 'https://staging.bb-inf.net'; + public tokenRefreshData(refreshToken: string): string { + return `grant_type=refresh_token&refresh_token=${refreshToken}`; } public refreshHeaders() { + if (!this.data.clientSecret) { + throw new Error('No client secret for this strategy'); + } + return { 'Content-Type': 'application/x-www-form-urlencoded', - Authorization: basicAuth(BitbucketStagingStrategyData.clientID, BitbucketStagingStrategyData.clientSecret), + Authorization: basicAuth(this.data.clientID, this.data.clientSecret), }; } - - public tokenRefreshData(refreshToken: string): string { - return `grant_type=refresh_token&refresh_token=${refreshToken}`; - } - - public profileUrl(): string { - return BitbucketStagingStrategyData.profileURL; - } - - public emailsUrl(): string { - return BitbucketStagingStrategyData.emailsURL; - } } diff --git a/src/atlclients/strategyData.ts b/src/atlclients/strategyData.ts new file mode 100644 index 00000000..6058e231 --- /dev/null +++ b/src/atlclients/strategyData.ts @@ -0,0 +1,188 @@ +import { OAuthProvider, Product, ProductBitbucket, ProductJira } from './authInfo'; + +export type StrategyProps = { + /** What does this strategy refer to? Essentially, strategy ID */ + provider: OAuthProvider; + /** Is this for JIRA or Bitbucket? */ + product: Product; + /** + * Client ID of the OAuth app. Docs: + * - Jira: https://developer.atlassian.com/cloud/confluence/oauth-2-3lo-apps/ + * - Bitbucket: https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/ + */ + clientID: string; + /** + * Base URL for the initial authorization request + */ + authorizationURL: string; + /** + * Base URL for getting access tokens + */ + tokenURL: string; + /** + * Base URL for getting user profile information + */ + profileURL: string; + /** + * The callback URL this strategy will supply to the OAuth provider. + * Must be exactly as configured in the OAuth app. + */ + callbackURL: string; + /** + * Base URL for the API calls. Used to + */ + apiURL: string; + + /** + * Present only in non-PKCE environments + */ + clientSecret?: string; + + /** + * Jira-only + * Base URL for getting accessible resources + */ + accessibleResourcesURL?: string; + /** + * Jira-only + * Scope for the OAuth request, as seen in a classic Jira platform REST API authorization URL + * (e.g. "manage:jira-project offline_access") + * See developer.atlassian.com 3LO docs + */ + scope?: string; + /** + * Jira-only + * Additional parameters for the OAuth request + * See developer.atlassian.com 3LO docs + */ + authParams?: { + /** + * The audience parameter for the OAuth request + */ + audience: string; + /** + * The prompt parameter for the OAuth request + */ + prompt: string; + }; + + /** + * Bitbucket-only + * Base URL for getting user emails + */ + emailsURL?: string; +}; + +/** + * Temporary bit of logic to get remote auth config from environment. + * It's fine if this is not set in prod - the new remote auth isn't invoked anywhere yet. + */ +type RemoteAuthConfig = { + clientID: string; + clientSecret: string; + callbackURL: string; +}; + +const getRemoteAuthConfig = () => { + const DEFAULT_REMOTE_AUTH_CONFIG = { + clientID: '', + clientSecret: '', + callbackURL: '', + }; + + try { + const config = JSON.parse(process.env.ATLASCODE_REMOTE_AUTH_CONFIG || '') as RemoteAuthConfig; + if (config.clientID && config.clientSecret && config.callbackURL) { + return config; + } else { + return DEFAULT_REMOTE_AUTH_CONFIG; + } + } catch (e) { + console.log('Failed to parse remote auth config', e); + return DEFAULT_REMOTE_AUTH_CONFIG; + } +}; + +const remoteAuthConfig = getRemoteAuthConfig(); + +export class OAuthStrategyData { + static readonly JiraRemote: StrategyProps = { + provider: OAuthProvider.JiraCloud, + product: ProductJira, + clientID: remoteAuthConfig.clientID, + clientSecret: remoteAuthConfig.clientSecret, + authorizationURL: 'https://auth.atlassian.com/authorize', + tokenURL: 'https://auth.atlassian.com/oauth/token', + profileURL: 'https://api.atlassian.com/me', + accessibleResourcesURL: 'https://api.atlassian.com/oauth/token/accessible-resources', + callbackURL: remoteAuthConfig.callbackURL, + apiURL: 'api.atlassian.com', + scope: 'read:jira-user read:jira-work write:jira-work offline_access manage:jira-project', + authParams: { + audience: 'api.atlassian.com', + prompt: 'consent', + }, + }; + + static readonly JiraProd: StrategyProps = { + provider: OAuthProvider.JiraCloud, + product: ProductJira, + clientID: 'bJChVgBQd0aNUPuFZ8YzYBVZz3X4QTe2', + clientSecret: '', + authorizationURL: 'https://auth.atlassian.com/authorize', + tokenURL: 'https://auth.atlassian.com/oauth/token', + profileURL: 'https://api.atlassian.com/me', + accessibleResourcesURL: 'https://api.atlassian.com/oauth/token/accessible-resources', + callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.JiraCloud, + apiURL: 'api.atlassian.com', + scope: 'read:jira-user read:jira-work write:jira-work offline_access manage:jira-project', + authParams: { + audience: 'api.atlassian.com', + prompt: 'consent', + }, + }; + + static readonly JiraStaging: StrategyProps = { + provider: OAuthProvider.JiraCloudStaging, + product: ProductJira, + clientID: 'pmzXmUav3Rr5XEL0Sie7Biec0WGU8BKg', + clientSecret: '', + authorizationURL: 'https://auth.stg.atlassian.com/authorize', + tokenURL: 'https://auth.stg.atlassian.com/oauth/token', + profileURL: 'https://api.stg.atlassian.com/me', + accessibleResourcesURL: 'https://api.stg.atlassian.com/oauth/token/accessible-resources', + callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.JiraCloudStaging, + apiURL: 'api.stg.atlassian.com', + scope: 'read:jira-user read:jira-work write:jira-work offline_access manage:jira-project', + authParams: { + audience: 'api.stg.atlassian.com', + prompt: 'consent', + }, + }; + + static readonly BitbucketProd: StrategyProps = { + provider: OAuthProvider.BitbucketCloud, + product: ProductBitbucket, + clientID: '3hasX42a7Ugka2FJja', + clientSecret: 'st7a4WtBYVh7L2mZMU8V5ehDtvQcWs9S', + authorizationURL: 'https://bitbucket.org/site/oauth2/authorize', + tokenURL: 'https://bitbucket.org/site/oauth2/access_token', + profileURL: 'https://api.bitbucket.org/2.0/user', + emailsURL: 'https://api.bitbucket.org/2.0/user/emails', + callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.BitbucketCloud, + apiURL: 'https://bitbucket.org', + }; + + static readonly BitbucketStaging = { + provider: OAuthProvider.BitbucketCloudStaging, + product: ProductBitbucket, + clientID: '7jspxC7fgemuUbnWQL', + clientSecret: 'sjHugFh6SVVshhVE7PUW3bgXbbQDVjJD', + authorizationURL: 'https://staging.bb-inf.net/site/oauth2/authorize', + tokenURL: 'https://staging.bb-inf.net/site/oauth2/access_token', + profileURL: 'https://api-staging.bb-inf.net/2.0/user', + emailsURL: 'https://api-staging.bb-inf.net/2.0/user/emails', + callbackURL: 'http://127.0.0.1:31415/' + OAuthProvider.BitbucketCloudStaging, + apiURL: 'https://staging.bb-inf.net', + }; +} diff --git a/src/lib/ipc/fromUI/config.ts b/src/lib/ipc/fromUI/config.ts index 6b7cdc13..749d5b36 100644 --- a/src/lib/ipc/fromUI/config.ts +++ b/src/lib/ipc/fromUI/config.ts @@ -6,6 +6,7 @@ import { ReducerAction } from '@atlassianlabs/guipi-core-controller'; export enum ConfigActionType { Login = 'login', + RemoteLogin = 'remoteLogin', Logout = 'logout', SaveSettings = 'saveSettings', OpenJSON = 'openJson', @@ -22,6 +23,7 @@ export enum ConfigActionType { export type ConfigAction = | ReducerAction + | ReducerAction | ReducerAction | ReducerAction | ReducerAction @@ -44,6 +46,8 @@ export interface LoginAuthAction extends AuthAction { authInfo: AuthInfo; } +export interface RemoteLoginAuthAction {} + export interface LogoutAuthAction extends AuthAction { siteInfo: DetailedSiteInfo; } diff --git a/src/lib/webview/controller/config/configWebviewController.ts b/src/lib/webview/controller/config/configWebviewController.ts index f4c62284..5ddb7b71 100644 --- a/src/lib/webview/controller/config/configWebviewController.ts +++ b/src/lib/webview/controller/config/configWebviewController.ts @@ -13,6 +13,11 @@ import { Logger } from '../../../logger'; import { WebViewID } from '../../../ipc/models/common'; import { defaultActionGuard } from '@atlassianlabs/guipi-core-controller'; import { formatError } from '../../formatError'; +import uuid from 'uuid'; + +// TODO - figure out why linter is mad here: +import vscode from 'vscode'; // eslint-disable-line +import { Container } from '../../../../container'; //eslint-disable-line export const id: string = 'atlascodeSettingsV2'; @@ -138,6 +143,14 @@ export class ConfigWebviewController implements WebviewController { + const state = { deeplink: uri.toString(true), attemptId: uuid.v4() }; + Container.loginManager.initRemoteAuth(state); + }); + break; + } case ConfigActionType.Logout: { this._api.clearAuth(msg.siteInfo); this._analytics.fireLogoutButtonEvent(id); diff --git a/src/react/atlascode/config/auth/SiteAuthenticator.tsx b/src/react/atlascode/config/auth/SiteAuthenticator.tsx index e0839c8d..2226b916 100644 --- a/src/react/atlascode/config/auth/SiteAuthenticator.tsx +++ b/src/react/atlascode/config/auth/SiteAuthenticator.tsx @@ -7,6 +7,7 @@ import DomainIcon from '@material-ui/icons/Domain'; import { Product, ProductJira } from '../../../../atlclients/authInfo'; import { SiteList } from './SiteList'; import { SiteWithAuthInfo } from '../../../../lib/ipc/toUI/config'; +import { ConfigControllerContext } from '../configController'; type SiteAuthenticatorProps = { product: Product; @@ -17,10 +18,15 @@ type SiteAuthenticatorProps = { export const SiteAuthenticator: React.FunctionComponent = memo( ({ product, isRemote, sites }) => { const authDialogController = useContext(AuthDialogControllerContext); + const configController = useContext(ConfigControllerContext); const openProductAuth = useCallback(() => { authDialogController.openDialog(product, undefined); }, [authDialogController, product]); + const remoteAuth = useCallback(() => { + configController.remoteLogin(); + }, [configController]); + const handleEdit = useCallback( (swa: SiteWithAuthInfo) => { authDialogController.openDialog(product, swa); @@ -38,6 +44,7 @@ export const SiteAuthenticator: React.FunctionComponent openProductAuth={openProductAuth} sites={sites} handleEdit={handleEdit} + remoteAuth={remoteAuth} /> ) : ( openProductAuth={openProductAuth} sites={sites} handleEdit={handleEdit} + remoteAuth={remoteAuth} /> )} @@ -60,9 +68,17 @@ interface AuthContainerProps { openProductAuth: () => void; sites: SiteWithAuthInfo[]; handleEdit: (swa: SiteWithAuthInfo) => void; + remoteAuth: () => void; } -const LegacyAuthContainer = ({ isRemote, product, openProductAuth, sites, handleEdit }: AuthContainerProps) => ( +const LegacyAuthContainer = ({ + isRemote, + product, + openProductAuth, + sites, + handleEdit, + remoteAuth, +}: AuthContainerProps) => ( ); -const AuthContainer = ({ isRemote, product, openProductAuth, sites, handleEdit }: AuthContainerProps) => ( +const AuthContainer = ({ isRemote, product, openProductAuth, sites, handleEdit, remoteAuth }: AuthContainerProps) => ( @@ -113,6 +132,9 @@ const AuthContainer = ({ isRemote, product, openProductAuth, sites, handleEdit } {!isRemote && ( + + + diff --git a/src/react/atlascode/config/auth/SiteList.tsx b/src/react/atlascode/config/auth/SiteList.tsx index 33baa918..d4a6452a 100644 --- a/src/react/atlascode/config/auth/SiteList.tsx +++ b/src/react/atlascode/config/auth/SiteList.tsx @@ -87,7 +87,7 @@ function generateListItems( {swa.auth.state === AuthInfoState.Invalid && ( - edit(swa)}> + console.log(swa)}> diff --git a/src/react/atlascode/config/configController.ts b/src/react/atlascode/config/configController.ts index 5c729834..b126249d 100644 --- a/src/react/atlascode/config/configController.ts +++ b/src/react/atlascode/config/configController.ts @@ -33,6 +33,7 @@ export interface ConfigControllerApi { refresh: () => void; openLink: (linkId: KnownLinkID) => void; login: (site: SiteInfo, auth: AuthInfo) => void; + remoteLogin: () => void; logout: (site: DetailedSiteInfo) => void; fetchJqlOptions: (site: DetailedSiteInfo) => Promise; fetchJqlSuggestions: ( @@ -75,6 +76,9 @@ export const emptyApi: ConfigControllerApi = { login: (site: SiteInfo, auth: AuthInfo) => { return; }, + remoteLogin: () => { + return; + }, logout: (site: DetailedSiteInfo) => { return; }, @@ -314,6 +318,11 @@ export function useConfigController(): [ConfigState, ConfigControllerApi] { [postMessage], ); + const remoteLogin = useCallback(() => { + dispatch({ type: ConfigUIActionType.Loading }); + postMessage({ type: ConfigActionType.RemoteLogin }); + }, [postMessage]); + const logout = useCallback( (site: DetailedSiteInfo) => { dispatch({ type: ConfigUIActionType.Loading }); @@ -503,6 +512,7 @@ export function useConfigController(): [ConfigState, ConfigControllerApi] { refresh: sendRefresh, openLink: openLink, login: login, + remoteLogin: remoteLogin, logout: logout, fetchJqlSuggestions: fetchJqlSuggestions, fetchJqlOptions: fetchJqlOptions, @@ -516,6 +526,7 @@ export function useConfigController(): [ConfigState, ConfigControllerApi] { }, [ handleConfigChange, login, + remoteLogin, logout, openLink, postMessage, diff --git a/src/uriHandler/actions/simpleCallback.ts b/src/uriHandler/actions/simpleCallback.ts index 618d9ce2..6920d789 100644 --- a/src/uriHandler/actions/simpleCallback.ts +++ b/src/uriHandler/actions/simpleCallback.ts @@ -13,7 +13,7 @@ import { UriHandlerAction } from '../uriHandlerAction'; export class SimpleCallbackAction implements UriHandlerAction { constructor( private uriSuffix: string, - private callback: () => Promise, + private callback: (uri: Uri) => Promise, ) {} isAccepted(uri: Uri): boolean { @@ -21,6 +21,6 @@ export class SimpleCallbackAction implements UriHandlerAction { } async handle(uri: Uri): Promise { - return await this.callback(); + return await this.callback(uri); } } diff --git a/src/uriHandler/atlascodeUriHandler.ts b/src/uriHandler/atlascodeUriHandler.ts index 962be630..74432e85 100644 --- a/src/uriHandler/atlascodeUriHandler.ts +++ b/src/uriHandler/atlascodeUriHandler.ts @@ -50,6 +50,10 @@ export class AtlascodeUriHandler implements Disposable, UriHandler { jiraIssueFetcher: JiraIssueFetcher = new JiraIssueFetcher(), ) { return new AtlascodeUriHandler([ + new SimpleCallbackAction('auth', async (uri) => { + const params = new URLSearchParams(uri.query); + Container.loginManager.finishRemoteAuth(params.get('code')!); + }), new SimpleCallbackAction('openSettings', () => Container.settingsWebviewFactory.createOrShow()), new SimpleCallbackAction('openOnboarding', () => Container.onboardingWebviewFactory.createOrShow()), new CheckoutBranchUriHandlerAction(bitbucketHelper, analyticsApi),