Save cognito access token to ~/.enso/credentials (#6251)

To allow libs for using Cloud API they need to have access to the JWT token set by cognito. After talking to @jdunkerley and finding out it can not be obtained from localStorage we agreed to dump it to the file. This PR introduces simple saving jwt access token to ~/.enso/credentials (system agnostic)
This commit is contained in:
Paweł Buchowski 2023-04-13 18:02:13 +02:00 committed by GitHub
parent 72ae10c8e2
commit 467d3df4c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 76 additions and 2 deletions

View File

@ -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()
})
}

View File

@ -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',
}

View File

@ -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)

View File

@ -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) {

View File

@ -70,6 +70,9 @@ export type OAuthRedirect = newtype.Newtype<string, 'OAuthRedirect'>
* 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

View File

@ -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()

View File

@ -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:

View File

@ -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 {