Skip to content
Draft
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
5 changes: 4 additions & 1 deletion .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ jobs:
for node in $node_versions; do
OS_NODE+=("ubuntu-latest:$node")
done
if [[ "$HEAD_REF" == release-please--* ]]; then
# XXX(serhalp): also run the macOS + Windows matrix on this branch so we can verify the new keychain code
# paths before merge.
# Revert before merging.
if [[ "$HEAD_REF" == release-please--* || "$HEAD_REF" == "feat/native-token-keychain-storage" ]]; then
for os in $extra_oses; do
OS_NODE+=("$os:$primary")
done
Expand Down
220 changes: 220 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
},
"dependencies": {
"@fastify/static": "^9.1.1",
"@napi-rs/keyring": "^1.3.0",
"@netlify/ai": "^0.4.1",
"@netlify/api": "^14.0.19",
"@netlify/blobs": "^10.7.7",
Expand Down
19 changes: 15 additions & 4 deletions src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import merge from 'lodash/merge.js'
import pick from 'lodash/pick.js'

import { getAgent } from '../lib/http-agent.js'
import { writeAuthTokenForStorage } from '../lib/secure-storage.js'
import {
NETLIFY_CYAN,
USER_AGENT,
Expand Down Expand Up @@ -187,16 +188,18 @@ export type BaseOptionValues = {
verbose?: boolean
}

export function storeToken(
export async function storeToken(
globalConfig: Awaited<ReturnType<typeof getGlobalConfigStore>>,
{ userId, name, email, accessToken }: { userId: string; name?: string; email?: string; accessToken: string },
) {
): Promise<{ mode: 'keychain' | 'legacy'; keychainFailed: boolean }> {
const result = await writeAuthTokenForStorage(userId, accessToken)

const userData = merge(globalConfig.get(`users.${userId}`), {
id: userId,
name,
email,
auth: {
token: accessToken,
token: result.mode === 'keychain' ? undefined : accessToken,
github: {
user: undefined,
token: undefined,
Expand All @@ -205,6 +208,7 @@ export function storeToken(
})
globalConfig.set('userId', userId)
globalConfig.set(`users.${userId}`, userData)
return result
}

/** Base command class that provides tracking and config initialization */
Expand Down Expand Up @@ -551,7 +555,14 @@ export default class BaseCommand extends Command {
return logAndThrowError('Could not retrieve user ID from Netlify API')
}

storeToken(this.netlify.globalConfig, { userId, name, email, accessToken })
const { keychainFailed } = await storeToken(this.netlify.globalConfig, { userId, name, email, accessToken })
if (keychainFailed) {
warn(
`Could not store the auth token in your OS keychain. Falling back to the plaintext config file. Set ${chalk.cyanBright(
'NETLIFY_USE_LEGACY_AUTH_STORAGE=1',
)} to silence this and always use the plaintext config file.`,
)
}

await identify({
name,
Expand Down
11 changes: 9 additions & 2 deletions src/commands/login/login-check.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NetlifyAPI } from '@netlify/api'
import { OptionValues } from 'commander'

import { log, logAndThrowError, logJson } from '../../utils/command-helpers.js'
import { chalk, log, logAndThrowError, logJson, warn } from '../../utils/command-helpers.js'
import { storeToken } from '../base-command.js'
import type { NetlifyOptions } from '../types.js'

Expand Down Expand Up @@ -45,12 +45,19 @@ export const loginCheck = async (
return logAndThrowError('Could not retrieve user ID from Netlify API')
}

storeToken(globalConfig, {
const { keychainFailed } = await storeToken(globalConfig, {
userId: user.id,
name: user.full_name,
email: user.email,
accessToken,
})
if (keychainFailed) {
warn(
`Could not store the auth token in your OS keychain. Falling back to the plaintext config file. Set ${chalk.cyanBright(
'NETLIFY_USE_LEGACY_AUTH_STORAGE=1',
)} to silence this.`,
)
}

logJson({
status: 'authorized',
Expand Down
2 changes: 2 additions & 0 deletions src/commands/login/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const msg = function (location: TokenLocation) {
return 'via CLI --auth flag'
case 'config':
return 'via netlify config on your machine'
case 'keychain':
return 'via your OS keychain (secure storage)'
default:
return ''
}
Expand Down
7 changes: 6 additions & 1 deletion src/commands/logout/logout.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { OptionValues } from 'commander'

import { deleteTokenFromKeychain } from '../../lib/secure-storage.js'
import { exit, getToken, log } from '../../utils/command-helpers.js'
import { track } from '../../utils/telemetry/index.js'
import BaseCommand from '../base-command.js'
Expand All @@ -16,7 +17,11 @@ export const logout = async (_options: OptionValues, command: BaseCommand) => {

await track('user_logout')

// unset userID without deleting key
const userId = command.netlify.globalConfig.get('userId') as string | undefined
if (userId) {
await deleteTokenFromKeychain(userId)
command.netlify.globalConfig.set(`users.${userId}.auth.token`, undefined)
}
command.netlify.globalConfig.set('userId', null)

if (location === 'env') {
Expand Down
Loading
Loading