diff --git a/app/ide-desktop/lib/client/src/authentication.ts b/app/ide-desktop/lib/client/src/authentication.ts index ceb9c5a5334..5b866dcc524 100644 --- a/app/ide-desktop/lib/client/src/authentication.ts +++ b/app/ide-desktop/lib/client/src/authentication.ts @@ -74,6 +74,9 @@ import * as electron from 'electron' import opener from 'opener' +import * as fs from 'node:fs' +import * as os from 'node:os' +import * as path from 'node:path' import * as common from 'enso-common' import * as contentConfig from 'enso-content-config' @@ -104,6 +107,7 @@ const OPEN_URL_EVENT = 'open-url' export function initModule(window: () => electron.BrowserWindow) { initIpc() initOpenUrlListener(window) + initSaveAccessTokenListener() } /** Registers an Inter-Process Communication (IPC) channel between the Electron application and the @@ -139,3 +143,37 @@ function initOpenUrlListener(window: () => electron.BrowserWindow) { } }) } + +/** Registers a listener that fires a callback for `save-access-token` events. + * + * This listener is used to save given access token to credentials file to be later used by enso backend. + * + * Credentials file is placed in users home directory in `.enso` subdirectory in `credentials` file. */ +function initSaveAccessTokenListener() { + electron.ipcMain.on(ipc.Channel.saveAccessToken, (event, accessToken: string) => { + /** Enso home directory for credentials file. */ + const ensoCredentialsDirectoryName = '.enso' + /** Enso credentials file. */ + const ensoCredentialsFileName = 'credentials' + /** System agnostic credentials directory home path. */ + const ensoCredentialsHomePath = path.join(os.homedir(), ensoCredentialsDirectoryName) + + fs.mkdir(ensoCredentialsHomePath, { recursive: true }, err => { + if (err) { + logger.error(`Couldn't create ${ensoCredentialsDirectoryName} directory.`) + } else { + fs.writeFile( + path.join(ensoCredentialsHomePath, ensoCredentialsFileName), + accessToken, + err => { + if (err) { + logger.error(`Could not write to ${ensoCredentialsFileName} file.`) + } + } + ) + } + }) + + event.preventDefault() + }) +} diff --git a/app/ide-desktop/lib/client/src/ipc.ts b/app/ide-desktop/lib/client/src/ipc.ts index 8299aaf0861..301ab8615cf 100644 --- a/app/ide-desktop/lib/client/src/ipc.ts +++ b/app/ide-desktop/lib/client/src/ipc.ts @@ -19,4 +19,6 @@ export enum Channel { setDeepLinkHandler = 'set-deep-link-handler', /** Channel for signaling that a deep link to this application was opened. */ openDeepLink = 'open-deep-link', + /** Channel for signaling that access token be saved to a credentials file. */ + saveAccessToken = 'save-access-token', } diff --git a/app/ide-desktop/lib/client/src/preload.ts b/app/ide-desktop/lib/client/src/preload.ts index e8241803995..311f5a89c22 100644 --- a/app/ide-desktop/lib/client/src/preload.ts +++ b/app/ide-desktop/lib/client/src/preload.ts @@ -105,5 +105,12 @@ const AUTHENTICATION_API = { electron.ipcRenderer.on(ipc.Channel.openDeepLink, (_event, url: string) => { callback(url) }), + /** Saves the access token to a credentials file. + * + * Enso backend doesn't have access to Electron localStorage so we need to save access token to a file. + * Then the token will be used to sign cloud API requests. */ + saveAccessToken: (accessToken: string) => { + electron.ipcRenderer.send(ipc.Channel.saveAccessToken, accessToken) + }, } electron.contextBridge.exposeInMainWorld(AUTHENTICATION_API_KEY, AUTHENTICATION_API) diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/cognito.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/cognito.ts index ee84e96678f..aa02672026e 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/cognito.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/cognito.ts @@ -133,7 +133,7 @@ export class Cognito { constructor( private readonly logger: loggerProvider.Logger, private readonly platform: platformModule.Platform, - amplifyConfig: config.AmplifyConfig + private readonly amplifyConfig: config.AmplifyConfig ) { /** Amplify expects `Auth.configure` to be called before any other `Auth` methods are * called. By wrapping all the `Auth` methods we care about and returning an `Cognito` API @@ -143,6 +143,14 @@ export class Cognito { amplify.Auth.configure(nestedAmplifyConfig) } + /** Saves the access token to a file for further reuse. */ + + saveAccessToken(accessToken: string) { + if (this.amplifyConfig.accessTokenSaver) { + this.amplifyConfig.accessTokenSaver(accessToken) + } + } + /** Returns the current {@link UserSession}, or `None` if the user is not logged in. * * Will refresh the {@link UserSession} if it has expired. */ @@ -150,7 +158,7 @@ export class Cognito { return userSession() } - /** Sign up with with username and password. + /** Sign up with username and password. * * Does not rely on federated identity providers (e.g., Google or GitHub). */ signUp(username: string, password: string) { diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/config.ts b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/config.ts index 4307e3ed679..ac265207100 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/config.ts +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/config.ts @@ -70,6 +70,9 @@ export type OAuthRedirect = newtype.Newtype * we want to open OAuth URLs in the system browser. This is because the user can't be expected to * trust their credentials to an Electron app. */ export type OAuthUrlOpener = (url: string, redirectUrl: string) => void +/** A function used to save access token to a Enso credentials file. The token is used by the engine to issue + * http request to cloud API. */ +export type AccessTokenSaver = (accessToken: string) => void /** Function used to register a callback. The callback will get called when a deep link is received * by the app. This is only used in the desktop app (i.e., not in the cloud). This is used when the * user is redirected back to the app from the system browser, after completing an OAuth flow. */ @@ -90,6 +93,7 @@ export interface AmplifyConfig { userPoolId: UserPoolId userPoolWebClientId: UserPoolWebClientId urlOpener: OAuthUrlOpener | null + accessTokenSaver: AccessTokenSaver | null domain: OAuthDomain scope: OAuthScope[] redirectSignIn: OAuthRedirect diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx index bbf3d70dac2..0968ff433eb 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/providers/auth.tsx @@ -172,6 +172,9 @@ export function AuthProvider(props: AuthProviderProps) { organization, } + /** Save access token so can be reused by Enso backend. */ + cognito.saveAccessToken(accessToken) + /** Execute the callback that should inform the Electron app that the user has logged in. * This is done to transition the app from the authentication/dashboard view to the IDE. */ onAuthenticated() diff --git a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx index 5c2ac40a298..7424b35172c 100644 --- a/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx +++ b/app/ide-desktop/lib/dashboard/src/authentication/src/authentication/service.tsx @@ -133,6 +133,7 @@ function loadAmplifyConfig( /** Load the environment-specific Amplify configuration. */ const baseConfig = AMPLIFY_CONFIGS[config.ENVIRONMENT] let urlOpener = null + let accessTokenSaver = null if (platform === platformModule.Platform.desktop) { /** If we're running on the desktop, we want to override the default URL opener for OAuth * flows. This is because the default URL opener opens the URL in the desktop app itself, @@ -144,6 +145,10 @@ function loadAmplifyConfig( * we avoid unnecessary reloads/refreshes caused by redirects. */ urlOpener = openUrlWithExternalBrowser + /** When running on destop we want to have option to save access token to a file, + * so it can be later reuse when issuing requests to Cloud API. */ + accessTokenSaver = saveAccessToken + /** To handle redirects back to the application from the system browser, we also need to * register a custom URL handler. */ setDeepLinkHandler(logger, navigate) @@ -154,6 +159,7 @@ function loadAmplifyConfig( ...baseConfig, ...platformConfig, urlOpener, + accessTokenSaver, } } @@ -161,6 +167,10 @@ function openUrlWithExternalBrowser(url: string) { window.authenticationApi.openUrlInSystemBrowser(url) } +function saveAccessToken(accessToken: string) { + window.authenticationApi.saveAccessToken(accessToken) +} + /** Set the callback that will be invoked when a deep link to the application is opened. * * Typically this callback is invoked when the user is redirected back to the app after: diff --git a/app/ide-desktop/lib/types/globals.d.ts b/app/ide-desktop/lib/types/globals.d.ts index a3a643f96a8..27ca6b15a94 100644 --- a/app/ide-desktop/lib/types/globals.d.ts +++ b/app/ide-desktop/lib/types/globals.d.ts @@ -38,6 +38,8 @@ interface AuthenticationApi { /** Set the callback to be called when the system browser redirects back to a URL in the app, * via a deep link. See {@link setDeepLinkHandler} for details. */ setDeepLinkHandler: (callback: (url: string) => void) => void + /** Saves the access token to a file. */ + saveAccessToken: (access_token: string) => void } declare global {