Skip to content

Commit 0801dbe

Browse files
Pass additional metadata to ~/.enso/credentials
1 parent a3bf5a0 commit 0801dbe

File tree

8 files changed

+126
-42
lines changed

8 files changed

+126
-42
lines changed

app/ide-desktop/lib/client/src/authentication.ts

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -156,38 +156,49 @@ export function onOpenUrl(url: URL, window: () => electron.BrowserWindow) {
156156
* The credentials file is placed in the user's home directory in the `.enso` subdirectory
157157
* in the `credentials` file. */
158158
function initSaveAccessTokenListener() {
159-
electron.ipcMain.on(ipc.Channel.saveAccessToken, (event, accessToken: string | null) => {
160-
event.preventDefault()
159+
electron.ipcMain.on(
160+
ipc.Channel.saveAccessToken,
161+
(event, accessTokenPayload: SaveAccessTokenPayload | null) => {
162+
event.preventDefault()
161163

162-
/** Home directory for the credentials file. */
163-
const credentialsDirectoryName = `.${common.PRODUCT_NAME.toLowerCase()}`
164-
/** File name of the credentials file. */
165-
const credentialsFileName = 'credentials'
166-
/** System agnostic credentials directory home path. */
167-
const credentialsHomePath = path.join(os.homedir(), credentialsDirectoryName)
164+
/** Home directory for the credentials file. */
165+
const credentialsDirectoryName = `.${common.PRODUCT_NAME.toLowerCase()}`
166+
/** File name of the credentials file. */
167+
const credentialsFileName = 'credentials'
168+
/** System agnostic credentials directory home path. */
169+
const credentialsHomePath = path.join(os.homedir(), credentialsDirectoryName)
168170

169-
if (accessToken == null) {
170-
try {
171-
fs.unlinkSync(path.join(credentialsHomePath, credentialsFileName))
172-
} catch {
173-
// Ignored, most likely the path does not exist.
174-
}
175-
} else {
176-
fs.mkdir(credentialsHomePath, { recursive: true }, error => {
177-
if (error) {
178-
logger.error(`Couldn't create ${credentialsDirectoryName} directory.`)
179-
} else {
180-
fs.writeFile(
181-
path.join(credentialsHomePath, credentialsFileName),
182-
accessToken,
183-
innerError => {
184-
if (innerError) {
185-
logger.error(`Could not write to ${credentialsFileName} file.`)
186-
}
187-
}
188-
)
171+
if (accessTokenPayload == null) {
172+
try {
173+
fs.unlinkSync(path.join(credentialsHomePath, credentialsFileName))
174+
} catch {
175+
// Ignored, most likely the path does not exist.
189176
}
190-
})
177+
} else {
178+
fs.mkdir(credentialsHomePath, { recursive: true }, error => {
179+
if (error) {
180+
logger.error(`Couldn't create ${credentialsDirectoryName} directory.`)
181+
} else {
182+
fs.writeFile(
183+
path.join(credentialsHomePath, credentialsFileName),
184+
JSON.stringify({
185+
/* eslint-disable @typescript-eslint/naming-convention */
186+
client_id: accessTokenPayload.clientId,
187+
access_token: accessTokenPayload.accessToken,
188+
refresh_token: accessTokenPayload.refreshToken,
189+
refresh_uri: accessTokenPayload.refreshUri,
190+
expire_at: accessTokenPayload.expireAt,
191+
/* eslint-enable @typescript-eslint/naming-convention */
192+
}),
193+
innerError => {
194+
if (innerError) {
195+
logger.error(`Could not write to ${credentialsFileName} file.`)
196+
}
197+
}
198+
)
199+
}
200+
})
201+
}
191202
}
192-
})
203+
)
193204
}

app/ide-desktop/lib/client/src/preload.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ const AUTHENTICATION_API = {
146146
*
147147
* The backend doesn't have access to Electron's `localStorage` so we need to save access token
148148
* to a file. Then the token will be used to sign cloud API requests. */
149-
saveAccessToken: (accessToken: string | null) => {
150-
electron.ipcRenderer.send(ipc.Channel.saveAccessToken, accessToken)
149+
saveAccessToken: (accessTokenPayload: SaveAccessTokenPayload | null) => {
150+
electron.ipcRenderer.send(ipc.Channel.saveAccessToken, accessTokenPayload)
151151
},
152152
}
153153
electron.contextBridge.exposeInMainWorld(AUTHENTICATION_API_KEY, AUTHENTICATION_API)

app/ide-desktop/lib/dashboard/src/authentication/cognito.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,8 @@ export class Cognito {
184184
}
185185

186186
/** Save the access token to a file for further reuse. */
187-
saveAccessToken(accessToken: string | null) {
188-
this.amplifyConfig.saveAccessToken?.(accessToken)
187+
saveAccessToken(accessTokenPayload: SaveAccessTokenPayload | null) {
188+
this.amplifyConfig.saveAccessToken?.(accessTokenPayload)
189189
}
190190

191191
/** Return the current {@link UserSession}, or `None` if the user is not logged in.
@@ -194,7 +194,10 @@ export class Cognito {
194194
async userSession() {
195195
const currentSession = await results.Result.wrapAsync(() => amplify.Auth.currentSession())
196196
const amplifySession = currentSession.mapErr(intoCurrentSessionErrorType)
197-
return amplifySession.map(parseUserSession).unwrapOr(null)
197+
198+
return amplifySession
199+
.map(session => parseUserSession(session, this.amplifyConfig.userPoolWebClientId))
200+
.unwrapOr(null)
198201
}
199202

200203
/** Returns the associated organization ID of the current user, which is passed during signup,
@@ -262,6 +265,7 @@ export class Cognito {
262265
const result = await results.Result.wrapAsync(async () => {
263266
await amplify.Auth.signIn(username, password)
264267
})
268+
265269
return result.mapErr(intoAmplifyErrorOrThrow).mapErr(intoSignInWithPasswordErrorOrThrow)
266270
}
267271

@@ -367,19 +371,49 @@ export interface UserSession {
367371
readonly email: string
368372
/** User's access token, used to authenticate the user (e.g., when making API calls). */
369373
readonly accessToken: string
374+
/** User's refresh token, used to refresh the access token when it expires. */
375+
readonly refreshToken: string
376+
/** URL to refresh the access token. */
377+
readonly refreshUrl: string
378+
/** Time when the access token will expire, date in ISO format(UTC). */
379+
readonly expireAt: string
380+
/** The client ID of the user pool. */
381+
readonly clientId: string
370382
}
371383

372384
/** Parse a `CognitoUserSession` into a {@link UserSession}.
373385
* @throws If the `email` field of the payload is not a string. */
374-
function parseUserSession(session: cognito.CognitoUserSession): UserSession {
386+
function parseUserSession(
387+
session: cognito.CognitoUserSession,
388+
clientId: string
389+
): UserSession {
375390
const payload: Readonly<Record<string, unknown>> = session.getIdToken().payload
376391
const email = payload.email
392+
377393
/** The `email` field is mandatory, so we assert that it exists and is a string. */
378394
if (typeof email !== 'string') {
379395
throw new Error('Payload does not have an email field.')
380396
} else {
381397
const accessToken = session.getAccessToken().getJwtToken()
382-
return { email, accessToken }
398+
399+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
400+
const refreshUrl: string = session.getAccessToken().payload.iss
401+
const expirationTimestamp = session.getAccessToken().getExpiration()
402+
403+
const expireAt = (() => {
404+
const date = new Date(0)
405+
date.setUTCSeconds(expirationTimestamp)
406+
return date.toISOString()
407+
})()
408+
409+
return {
410+
email,
411+
accessToken,
412+
refreshUrl,
413+
clientId,
414+
refreshToken: session.getRefreshToken().getToken(),
415+
expireAt,
416+
}
383417
}
384418
}
385419

app/ide-desktop/lib/dashboard/src/authentication/config.ts

Whitespace-only changes.

app/ide-desktop/lib/dashboard/src/authentication/service.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface AmplifyConfig {
2727
readonly userPoolId: string
2828
readonly userPoolWebClientId: string
2929
readonly urlOpener: ((url: string, redirectUrl: string) => void) | null
30-
readonly saveAccessToken: ((accessToken: string | null) => void) | null
30+
readonly saveAccessToken: ((accessToken: SaveAccessTokenPayload | null) => void) | null
3131
readonly domain: string
3232
readonly scope: string[]
3333
readonly redirectSignIn: string
@@ -125,6 +125,7 @@ export function initAuthService(authConfig: AuthConfig): AuthService | null {
125125
amplifyConfig == null
126126
? null
127127
: new cognitoModule.Cognito(logger, supportsDeepLinks, amplifyConfig)
128+
128129
return cognito == null
129130
? null
130131
: { cognito, registerAuthEventListener: listen.registerAuthEventListener }
@@ -137,14 +138,14 @@ function loadAmplifyConfig(
137138
navigate: (url: string) => void
138139
): AmplifyConfig | null {
139140
let urlOpener: ((url: string) => void) | null = null
140-
let saveAccessToken: ((accessToken: string | null) => void) | null = null
141+
let saveAccessToken: ((accessToken: SaveAccessTokenPayload | null) => void) | null = null
141142
if ('authenticationApi' in window) {
142143
// When running on desktop we want to have option to save access token to a file,
143144
// so it can be reused later when issuing requests to the Cloud API.
144145
//
145146
// Note: Wrapping this function in an arrow function ensures that the current Authentication API
146147
// is always used.
147-
saveAccessToken = (accessToken: string | null) => {
148+
saveAccessToken = (accessToken: SaveAccessTokenPayload | null) => {
148149
window.authenticationApi.saveAccessToken(accessToken)
149150
}
150151
}

app/ide-desktop/lib/dashboard/src/providers/AuthProvider.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,13 @@ export default function AuthProvider(props: AuthProviderProps) {
348348
document.cookie = `logged_in=yes;max-age=34560000;domain=${parentDomain};samesite=strict;secure`
349349

350350
// Save access token so can it be reused by the backend.
351-
cognito?.saveAccessToken(session.accessToken)
351+
cognito?.saveAccessToken({
352+
accessToken: session.accessToken,
353+
clientId: session.clientId,
354+
expireAt: session.expireAt,
355+
refreshToken: session.refreshToken,
356+
refreshUri: session.refreshUrl,
357+
})
352358

353359
// Execute the callback that should inform the Electron app that the user has logged in.
354360
// This is done to transition the app from the authentication/dashboard view to the IDE.

app/ide-desktop/lib/types/globals.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ interface AuthenticationApi {
5252
* via a deep link. See `setDeepLinkHandler` for details. */
5353
readonly setDeepLinkHandler: (callback: (url: string) => void) => void
5454
/** Saves the access token to a file. */
55-
readonly saveAccessToken: (accessToken: string | null) => void
55+
readonly saveAccessToken: (accessToken: SaveAccessTokenPayload | null) => void
5656
}
5757

5858
// =====================================

app/ide-desktop/lib/types/ipc.d.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @file This file contains the types for the IPC events.
3+
* The IPC events are used to communicate between the main and renderer processes.
4+
* The events are defined in the main process and are listened to in the renderer process.
5+
*/
6+
7+
/**
8+
* Payload for the save-access-token event that saves an access token to a credentials file.
9+
*/
10+
interface SaveAccessTokenPayload {
11+
/**
12+
* The JWT token to save.
13+
*/
14+
readonly accessToken: string
15+
/**
16+
* The cognito app integration client id
17+
*/
18+
readonly clientId: string
19+
/**
20+
* The refresh token taken from initAuth flow.
21+
*/
22+
readonly refreshToken: string
23+
/**
24+
* The cognito url to refresh the token.
25+
*/
26+
readonly refreshUri: string
27+
/**
28+
* Time when the token will expire.
29+
* This is a string representation of a date in ISO 8601 format (e.g. "2021-01-01T00:00:00Z").
30+
*/
31+
readonly expireAt: string
32+
}

0 commit comments

Comments
 (0)