Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 2 additions & 5 deletions package-lock.json

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

73 changes: 26 additions & 47 deletions packages/core/src/auth/consoleSessionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,14 +217,6 @@ export async function authenticateWithConsoleLogin(profileName?: string, region?

if (result.exitCode === 0) {
telemetry.aws_consoleLoginCLISuccess.emit({ result: 'Succeeded' })
// Show generic success message
void vscode.window.showInformationMessage(
localize(
'AWS.message.success.consoleLogin',
'Login with console credentials command completed. Profile "{0}" is now available.',
profileName
)
)
logger.info('Login with console credentials command completed. Exit code: %d', result.exitCode)
} else if (result.exitCode === 254) {
logger.error(
Expand Down Expand Up @@ -291,49 +283,36 @@ export async function authenticateWithConsoleLogin(profileName?: string, region?
code: 'ConsoleLoginConfigError',
})
}
// Show success message after seeing newly created session in the disk
void vscode.window.showInformationMessage(
`Profile "${profileName}" ready with credentials from ${profile.login_session} console session`
)

// Activate the newly created profile
try {
logger.info(`Activating profile: ${profileName}`)
// Connection ID format is "profile:profileName"
const credentialsId: CredentialsId = {
credentialSource: 'profile',
credentialTypeId: profileName,
}
const connectionId = asString(credentialsId)
// Invalidate cached credentials to force fresh fetch
getLogger().info(`Invalidated cached credentials for ${connectionId}`)
globals.loginManager.store.invalidateCredentials(credentialsId)
logger.info(`Looking for connection with ID: ${connectionId}`)

const connection = await Auth.instance.getConnection({ id: connectionId })
if (connection === undefined) {
// Log available connections for debugging
const availableConnections = await Auth.instance.listConnections()
logger.error(
'Connection not found. Available connections: %O',
availableConnections.map((c) => c.id)
)
throw new ToolkitError(`Failed to get connection from profile: ${connectionId}`, {
code: 'MissingConnection',
})
}
logger.info(`Activating profile: ${profileName}`)
const credentialsId: CredentialsId = {
credentialSource: 'profile',
credentialTypeId: profileName,
}
const connectionId = asString(credentialsId)
// Invalidate cached credentials to force fresh fetch
getLogger().info(`Invalidated cached credentials for ${connectionId}`)
globals.loginManager.store.invalidateCredentials(credentialsId)
logger.info(`Looking for connection with ID: ${connectionId}`)

// Don't call useConnection() - let credentials be fetched naturally when needed
await Auth.instance.updateConnectionState(connectionId, 'valid')
} catch (error: any) {
logger.error('Failed to activate profile: %O', error)
void vscode.window.showErrorMessage(
localize(
'AWS.message.error.consoleLogin.profileActivationFailed',
'Failed to activate profile: {0}',
error instanceof Error ? error.message : String(error)
)
// Make sure that connection exists before letting other part use connection
const connection = await Auth.instance.getConnection({ id: connectionId })
if (connection === undefined) {
// Log available connections for debugging
const availableConnections = await Auth.instance.listConnections()
logger.error(
'Connection not found. Available connections: %O',
availableConnections.map((c) => c.id)
)
throw new ToolkitError('Failed to activate profile', {
code: 'ProfileActivationFailed',
cause: error as Error,
throw new ToolkitError(`Failed to get connection from profile: ${connectionId}`, {
code: 'MissingConnection',
})
}
// Don't call useConnection() - let credentials be fetched naturally when needed
await Auth.instance.updateConnectionState(connectionId, 'valid')
})
}
151 changes: 98 additions & 53 deletions packages/core/src/auth/providers/sharedCredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ParsedIniData } from '@smithy/types'
import { chain } from '@aws-sdk/property-provider'
import { fromInstanceMetadata, fromContainerMetadata } from '@smithy/credential-provider-imds'
import { fromEnv } from '@aws-sdk/credential-provider-env'
import * as localizedText from '../../shared/localizedText'
import { getLogger } from '../../shared/logger/logger'
import { getStringHash } from '../../shared/utilities/textUtilities'
import { getMfaTokenFromUser, resolveProviderWithCancel } from '../credentials/utils'
Expand Down Expand Up @@ -59,6 +60,96 @@ function isSsoProfile(profile: Profile): boolean {
)
}

export async function handleInvalidConsoleCredentials(
error: Error,
profileName: string,
region: string
): Promise<never> {
getLogger().error('Console login authentication failed for profile %s in region %s: %O', profileName, region, error)

// Indicates that a VS Code window reload is required to reinitialize credential providers
// and avoid using stale console session credentials when login cache and in-memory state diverge.
let requiresVscodeReloadForCredentials = false
if (
error.message.includes('Your session has expired') ||
error.message.includes('Failed to load a token for session') ||
error.message.includes('Failed to load token from')
) {
requiresVscodeReloadForCredentials = true
// Ask for user confirmation before refreshing
const response = await vscode.window.showInformationMessage(
`Unable to use your console credentials for profile "${profileName}". Would you like to retry?`,
localizedText.retry,
localizedText.cancel
)

if (response !== localizedText.retry) {
throw ToolkitError.chain(error, 'User cancelled console credentials token refresh.', {
code: 'LoginSessionRefreshCancelled',
cancelled: true,
})
}

getLogger().info('Re-authenticating using console credentials for profile %s', profileName)
// Execute the console login command with the existing profile and region
try {
await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region)
} catch (_) {
void vscode.window.showErrorMessage(
`Unable to refresh your AWS credentials. Please run 'aws login --profile ${profileName}' in your terminal, then reload VS Code to continue.`
)
}
}

if (error.message.includes('does not contain login_session')) {
// The credential provider was created before the CLI wrote the new login session to disk.
// This happens when you run console login and immediately try to use the connection.
// A window reload is needed to pick up the newly created session.
requiresVscodeReloadForCredentials = true
}

if (requiresVscodeReloadForCredentials) {
getLogger().info(
`Reloading window to sync with updated credentials cache using connection for profile: ${profileName}`
)
const reloadResponse = await vscode.window.showInformationMessage(
`Credentials for "${profileName}" were updated. A window reload is required to apply them. Save your work before continuing. Reload now?`,
localizedText.yes,
localizedText.no
)
if (reloadResponse === localizedText.yes) {
// At this point, the console credential cache on disk has been updated (via AWS CLI login),
// but the in-memory credential providers used by the Toolkit / AWS SDK were already
// constructed earlier and continue to reference stale credentials.
//
// Notes on behavior:
// - Console credentials are read once when the provider is created and are not reloaded
// dynamically at runtime.
// - Removing or recreating connections/profiles does not rebuild the underlying provider.
// - Filesystem watchers may detect cache changes, but provider instances still hold
// the originally loaded credentials.
// - Attempting to swap providers at runtime can introduce incompatibilities between
// legacy credential shims and AWS SDK v3 providers.
//
// Authentication flow (simplified):
// aws login (CLI) -> writes ~/.aws/login/cache
// Toolkit -> constructs credential provider (snapshots credentials in memory)
// SDK calls -> continue using in-memory credentials until provider is reinitialized
//
// A VS Code window reload is the only safe and deterministic way to fully reinitialize
// credential providers and ensure the updated console session credentials are used.
await vscode.commands.executeCommand('workbench.action.reloadWindow')
}
throw ToolkitError.chain(error, 'Console credentials require window reload', {
code: 'FromLoginCredentialProviderError',
})
}

throw ToolkitError.chain(error, 'Console credentials error', {
code: 'FromLoginCredentialProviderError',
})
}

/**
* Represents one profile from the AWS Shared Credentials files.
*/
Expand Down Expand Up @@ -400,67 +491,21 @@ export class SharedCredentialsProvider implements CredentialsProvider {
const baseProvider = fromLoginCredentials({
profile: this.profileName,
clientConfig: {
// Console session profiles created by 'aws login' may not have a region property
// The AWS CLI's philosophy is to treat global options like --region as per-invocation overrides
// rather than persistent configuration, minimizing what gets permanently stored in profiles
// and deferring configuration decisions until the actual command execution.
region: defaultRegion,
},
})
return async () => {
try {
return await baseProvider()
} catch (error) {
getLogger().error(
'Console login authentication failed for profile %s in region %s: %O',
this.profileName,
defaultRegion,
error
)

if (
error instanceof Error &&
(error.message.includes('Your session has expired') ||
error.message.includes('Failed to load a token for session') ||
error.message.includes('Failed to load token from'))
) {
// Ask for user confirmation before refreshing
const response = await vscode.window.showInformationMessage(
`Unable to use your console credentials for profile "${this.profileName}". Would you like to refresh it?`,
'Refresh',
'Cancel'
)

if (response !== 'Refresh') {
throw ToolkitError.chain(error, 'User cancelled console credentials token refresh.', {
code: 'LoginSessionRefreshCancelled',
cancelled: true,
})
}

getLogger().info('Re-authenticating using console credentials for profile %s', this.profileName)
// Execute the console login command with the existing profile and region
try {
await vscode.commands.executeCommand(
'aws.toolkit.auth.consoleLogin',
this.profileName,
defaultRegion
)
} catch (reAuthError) {
throw ToolkitError.chain(
reAuthError,
`Failed to refresh credentials for profile ${this.profileName}. Run 'aws login --profile ${this.profileName}' to authenticate.`,
{ code: 'LoginSessionReAuthError' }
)
}

getLogger().info(
'Authentication completed for profile %s, refreshing credentials...',
this.profileName
)

// Use the same provider instance but get fresh credentials
return await baseProvider()
if (error instanceof Error) {
await handleInvalidConsoleCredentials(error, this.profileName, defaultRegion)
}
throw ToolkitError.chain(error, `Failed to get console credentials`, {
code: 'FromLoginCredentialProviderError',
})
throw error
}
}
}
Expand Down
21 changes: 9 additions & 12 deletions packages/core/src/awsService/sagemaker/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function deeplinkConnect(
appType?: string,
workspaceName?: string,
namespace?: string,
clusterArn?: string,
eksClusterArn?: string,
isSMUS: boolean = false
) {
getLogger().debug(
Expand All @@ -118,11 +118,7 @@ export async function deeplinkConnect(
appType: ${appType},
workspaceName: ${workspaceName},
namespace: ${namespace},
clusterArn: ${clusterArn}`
)

getLogger().info(
`sm:deeplinkConnect: domain: ${domain}, appType: ${appType}, workspaceName: ${workspaceName}, namespace: ${namespace}, clusterArn: ${clusterArn}`
eksClusterArn: ${eksClusterArn}`
)

if (isRemoteWorkspace()) {
Expand All @@ -132,8 +128,8 @@ export async function deeplinkConnect(

try {
let connectionType = 'sm_dl'
if (!domain && clusterArn && workspaceName && namespace) {
const { accountId, region, clusterName } = parseArn(clusterArn)
if (!domain && eksClusterArn && workspaceName && namespace) {
const { accountId, region, clusterName } = parseArn(eksClusterArn)
connectionType = 'sm_hp'
const proposedSession = `${workspaceName}_${namespace}_${clusterName}_${region}_${accountId}`
session = isValidSshHostname(proposedSession)
Expand Down Expand Up @@ -224,21 +220,22 @@ function createValidSshSession(
const components = [
sanitize(workspaceName, 63), // K8s limit
sanitize(namespace, 63), // K8s limit
sanitize(clusterName, 63), // HP cluster limit
sanitize(clusterName, 100), // EKS limit
sanitize(region, 16), // Longest AWS region limit
sanitize(accountId, 12), // Fixed
].filter((c) => c.length > 0)
// Total: 63 + 63 + 63 + 16 + 12 + 4 separators + 3 chars for hostname header = 224 < 253 (max limit)
// Total: 63 + 63 + 100 + 16 + 12 + 4 separators + 3 chars for hostname header = 261 > 253 (max limit)
// If all attributes max out char limit, then accountId will be truncated to the first 4 char.

const session = components.join('_').substring(0, 224)
const session = components.join('_').substring(0, 253)
return session
}

/**
* Validates if a string meets SSH hostname naming convention
*/
function isValidSshHostname(label: string): boolean {
return /^[a-z0-9]([a-z0-9.-_]{0,222}[a-z0-9])?$/.test(label)
return /^[a-z0-9]([a-z0-9.-_]{0,251}[a-z0-9])?$/.test(label)
}

export async function stopSpace(
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/awsService/sagemaker/uriHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function register(ctx: ExtContext) {
undefined,
params.workspaceName,
params.namespace,
params.clusterArn
params.eksClusterArn
)
})
}
Expand All @@ -51,7 +51,7 @@ export function register(ctx: ExtContext) {

export function parseHyperpodConnectParams(query: SearchParams) {
const requiredParams = query.getFromKeysOrThrow('sessionId', 'streamUrl', 'sessionToken', 'cell-number')
const optionalParams = query.getFromKeys('workspaceName', 'namespace', 'clusterArn')
const optionalParams = query.getFromKeys('workspaceName', 'namespace', 'eksClusterArn')
return { ...requiredParams, ...optionalParams }
}
export function parseConnectParams(query: SearchParams) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { CodeCatalystAuthenticationProvider } from '../../../../codecatalyst/aut
import { AuthError, AuthFlowState } from '../types'
import { setContext } from '../../../../shared/vscode/setContext'
import { builderIdStartUrl } from '../../../../auth/sso/constants'
import { CredentialsId, asString } from '../../../../auth/providers/credentials'
import { RegionProfile } from '../../../../codewhisperer/models/model'
import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager'
import globals from '../../../../shared/extensionGlobals'
Expand Down Expand Up @@ -93,6 +94,14 @@ export class ToolkitLoginWebview extends CommonAuthWebview {
// Execute AWS CLI login command
await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region)

// Use profile as active connection
const credentialsId: CredentialsId = {
credentialSource: 'profile',
credentialTypeId: profileName,
}
const connectionId = asString(credentialsId)
await Auth.instance.useConnection({ id: connectionId })

// Hide auth view and show resource explorer
await setContext('aws.explorer.showAuthView', false)
await this.showResourceExplorer()
Expand Down
Loading
Loading