modules/AWSPowerShell/Common/SSOCmdlets.cs (579 lines of code) (raw):

/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. * * AWS Tools for Windows (TM) PowerShell (TM) * */ using Amazon.Runtime.CredentialManagement; using Amazon.Runtime.Internal.Util; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; using Amazon.Runtime.Credentials.Internal; using System.Threading; using System.Text; using Amazon.PowerShell.Common.Internal; using Amazon.PowerShell.Utils; using Amazon.Runtime; using System.Collections.ObjectModel; using System.Globalization; using System.Linq.Expressions; using Amazon.Runtime.Internal.Endpoints.StandardLibrary; namespace Amazon.PowerShell.Common { /// <summary> /// <para> /// The Invoke-AWSSSOLogin cmdlet retrieves and caches an AWS IAM Identity Center SSO access token to exchange for AWS credentials. /// To login, the requested profile must have first been set up , typically by using the Initialize-AWSSSOConfiguration Cmdlet. /// Login will be initiated only when the token or device registration has expired. /// Please note that only one login session can be active for a given SSO Session, and /// creating multiple profiles does not allow for multiple users to be authenticated against the same SSO Session. /// </para> /// </summary> [Cmdlet("Invoke", "AWSSSOLogin", DefaultParameterSetName = ProfileNameParameterSet)] [AWSCmdlet("Retrieves and caches an AWS SSO access token to exchange for AWS credentials.")] [OutputType("None")] [AWSCmdletOutput("None", "This cmdlet does not generate any output.")] public class InvokeAWSSSOLoginCmdlet : BaseCmdlet { private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly ILogger _logger = Logger.GetLogger(typeof(InvokeAWSSSOLoginCmdlet)); public const string ProfileNameParameterSet = "Profile"; public const string SessionNameParameterSet = "Session"; /// <summary> /// The name of an SSO-based profile that contains SSO configuration information. The profile is defined in the shared configuration file '~/.aws/config'. /// The profile can be set up by using the Initialize-AWSSSOConfiguration cmdlet. /// </summary> [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, ValueFromPipeline = true, ParameterSetName = ProfileNameParameterSet)] public string ProfileName { get; set; } /// <summary> /// <para>Name of an sso-session section of the configuration file that is used to group configuration variables for acquiring SSO access tokens, which can then be used to acquire AWS credentials.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = SessionNameParameterSet)] public string SessionName { get; set; } /// <summary> /// <para>Forces the cmdlet to invalidate the cached token and retrieve a new token.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] public SwitchParameter Force { get; set; } protected override void StopProcessing() { base.StopProcessing(); _cancellationTokenSource.Cancel(); } protected override void ProcessRecord() { base.ProcessRecord(); CredentialProfileOptions profileOptions = null; if (this.ParameterSetName.Equals(SessionNameParameterSet, StringComparison.OrdinalIgnoreCase)) { profileOptions = SSOProfileMethods.GetSsoSessionSection(SessionName); if (profileOptions == null) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"session {SessionName} not found in the shared config (~/.aws/config) file."), "ArgumentException", ErrorCategory.InvalidArgument, this.SessionName)); } } else if (this.ParameterSetName.Equals(ProfileNameParameterSet, StringComparison.OrdinalIgnoreCase) && MyInvocation.BoundParameters.ContainsKey("ProfileName")) { if (!SettingsStore.TryGetProfile(ProfileName, null, out var profile)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"profile {ProfileName} not found in the shared config (~/.aws/config) file."), "ArgumentException", ErrorCategory.InvalidArgument, this.ProfileName)); } // validate if SSO options are set. if not error out ValidateSSOOptions(profile.Options); profileOptions = profile.Options; } else { // check if default is an SSO profile if (!SettingsStore.TryGetProfile("default", null, out var profile)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"profile {ProfileName} not found in the shared config (~/.aws/config) file."), "ArgumentException", ErrorCategory.InvalidArgument, this.ProfileName)); } if (profile.Options.SsoSession != null && profile.Options.SsoStartUrl != null && profile.Options.SsoRegion != null) { profileOptions = profile.Options; } else { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"Either ProfileName or SessionName or a default profile with SSO configuration is required."), "ArgumentException", ErrorCategory.InvalidArgument, this.ProfileName)); } } if (Force.IsPresent) { // Logoff SSOUtils.LogoutAsync(profileOptions, cancellationToken: _cancellationTokenSource.Token).GetAwaiter().GetResult(); } var cachedSsoToken = SSOUtils.GetCachedTokenAsync(profileOptions, _cancellationTokenSource.Token).GetAwaiter().GetResult(); if (cachedSsoToken != null) { if (!cachedSsoToken.IsExpired()) { Console.WriteLine($"SSO re-authentication is not necessary since a cached token exists for sso-session {profileOptions.SsoSession}. Use -Force to re-authenticate and retrieve a new token."); return; } else if (!cachedSsoToken.RegisteredClientExpired() && cachedSsoToken.CanRefresh()) { Console.WriteLine("Attempting to refresh SSO Token. If the refresh fails, the SSO authorization will be initiated."); } } // below Action will be called back by the .NET SDK Login Flow. Action<SsoVerificationArguments> ssoVerificationCallback = args => { Console.WriteLine(GetSSOLoginMessage(args.UserCode, args.VerificationUri)); try { Process.Start(new ProcessStartInfo { FileName = args.VerificationUriComplete, UseShellExecute = true }); } catch (Exception ex) { // it's safe to ignore the exception since an attempt was made to open the link in the browser. // the customer has instructions to complete auth flow even when the browser doesn't open automatically. _logger.Error(ex, "Unable to open browser."); } }; var ssoToken = SSOUtils.LoginAsync(profileOptions, ssoVerificationCallback, _cancellationTokenSource.Token).GetAwaiter().GetResult(); if (ssoToken != null) { string associatedProfileMessage = ProfileName != null ? $" associated with the profile {ProfileName}" : ""; Console.WriteLine($"SSO authentication successful for the sso-session {profileOptions.SsoSession}{associatedProfileMessage}."); Console.WriteLine(); } } private void ValidateSSOOptions(CredentialProfileOptions options) { var missingProperties = SSOUtils.GetSSOMissingProperties(options); if (missingProperties.Any()) { string beginErrorMessage = $"There are missing SSO properties for the profile {ProfileName}. Use Initialize-AWSSSOConfiguration to configure SSO profile."; string missingPropertiesMessage = "missing SSO properties: " + string.Join(", ", missingProperties) + "."; this.ThrowTerminatingError(new ErrorRecord( new ArgumentException(beginErrorMessage + Environment.NewLine + missingPropertiesMessage), "ArgumentException", ErrorCategory.InvalidArgument, this)); } } private string GetSSOLoginMessage(string userCode, string verificationUri) { var sb = new StringBuilder(); sb.Append(Environment.NewLine); sb.Append("Attempting to automatically open the SSO authorization page in your default browser."); sb.Append(Environment.NewLine); sb.Append("If the browser does not open or you wish to use a different device to authorize this request, open the following URL:"); sb.Append(Environment.NewLine); sb.Append(Environment.NewLine); sb.Append($"{verificationUri.TrimEnd('/')}/?user_code={userCode}"); sb.Append(Environment.NewLine); sb.Append(Environment.NewLine); sb.Append("Verification code:"); sb.Append(Environment.NewLine); sb.Append(Environment.NewLine); sb.Append(userCode); sb.Append(Environment.NewLine); return sb.ToString(); } } /// <summary> /// <para> /// The Initialize-AWSSSOConfiguration cmdlet creates or updates a profile with the configuration values required to use AWS IAM Identity Center for single sign-on (SSO). /// The configuration is saved in the shared configuration file '~/.aws/config'. /// When any of the following parameters are omitted, the cmdlet prompts for their values interactively: ProfileName, SessionName, AccountId, RoleName, StartUrl, and SSORegion. /// When profile configuration is complete, login flow is automatically initiated by calling the Invoke-AWSSSOLogin cmdlet. /// </para> /// </summary> [Cmdlet("Initialize", "AWSSSOConfiguration", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] [AWSCmdlet("Creates or updates a profile with the configuration values required to use AWS SSO.")] [OutputType("None")] [AWSCmdletOutput("None", "This cmdlet does not generate any output.")] public class InitializeAWSSSOConfigurationCmdlet : BaseCmdlet { private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly ILogger _logger = Logger.GetLogger(typeof(InitializeAWSSSOConfigurationCmdlet)); /// <summary> /// <para>Name of the profile that will be saved in the shared configuration file '~/.aws/config'.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string ProfileName { get; set; } /// <summary> /// <para>Name of an sso-session section of the configuration file that is used to group configuration variables for acquiring SSO access tokens, which can then be used to acquire AWS credentials.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string SessionName { get; set; } /// <summary> /// <para>Identifier for the AWS account that is assigned to the user.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string AccountId { get; set; } /// <summary> /// <para>Name of the IAM Identity Center permission set that is assigned to the user.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string RoleName { get; set; } /// <summary> /// <para>URL that points to the organization's AWS access portal.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string StartUrl { get; set; } /// <summary> /// <para>AWS Region that contains the AWS access portal host. This is separate from, and can be a different Region than, the profile region parameter.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string SSORegion { get; set; } /// <summary> /// <para>List of scopes to be authorized for the SSO session. Scopes authorize access to IAM Identity Center bearer token authorized endpoints. Default value is sso:account:access.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string[] RegistrationScopes { get; set; } //"sso:account:access" /// <summary> /// <para>System name of an AWS Region that will be set for a specified profile.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] [ValidateNotNullOrEmpty] public string Region { get; set; } protected override void StopProcessing() { base.StopProcessing(); _cancellationTokenSource.Cancel(); } protected override void ProcessRecord() { base.ProcessRecord(); // When any of the required parameters are not passed, interactively prompt for the values. #region ExistingProfile // if Profile is passed CredentialProfile existingProfile = null; if (ProfileName != null) { SettingsStore.TryGetProfile(ProfileName, null, out existingProfile); } #endregion if (StartUrl == null) { StartUrl = PromptForStartUrl(existingProfile?.Options?.SsoStartUrl); } if (SSORegion == null) { SSORegion = PromptForSSORegion(existingProfile?.Options?.SsoRegion); } if (IsInteractiveMode() && RegistrationScopes == null) { RegistrationScopes = PromptForRegistrationScopes(); } // validate arguments #region Validate StartUrl if (!Uri.IsWellFormedUriString(StartUrl, UriKind.Absolute)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"{nameof(StartUrl)} {StartUrl} is not valid."), "ArgumentException", ErrorCategory.InvalidArgument, this.StartUrl)); } // Remove trailing slash "/" for consistency if (StartUrl.EndsWith("/")) StartUrl = StartUrl.TrimEnd('/'); #endregion #region RegistrationScopes if (!(RegistrationScopes?.Length > 0)) { RegistrationScopes = new string[] { "sso:account:access" }; } var registrationScopesString = string.Join(",", RegistrationScopes); #endregion #region SSORegion // validate SSORegion if (!IsRegionValid(SSORegion)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"{nameof(SSORegion)} {SSORegion} is not valid."), "ArgumentException", ErrorCategory.InvalidArgument, this.SSORegion)); } #endregion #region Region // validate Region if (Region != null && !IsRegionValid(Region)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"{nameof(Region)} {Region} is not valid."), "ArgumentException", ErrorCategory.InvalidArgument, this.Region)); } #endregion if (SessionName == null) { SessionName = PromptForSessionName(existingProfile?.Options?.SsoSession); } var profileOptions = new CredentialProfileOptions { SsoRegistrationScopes = registrationScopesString, SsoSession = SessionName, SsoStartUrl = StartUrl, SsoRegion = SSORegion }; // Register sso-session section to the config and then initiate login flow. profileOptions.RegisterSsoSession(); WriteVerbose("Calling Invoke-AWSSSOLogin Cmdlet"); ScriptBlock.Create("param($Cmdlet, $SessionName) & $Cmdlet -SessionName $SessionName -Force").Invoke("Invoke-AWSSSOLogin", SessionName); var cachedSsoToken = SSOUtils.GetCachedTokenAsync(profileOptions, _cancellationTokenSource.Token).GetAwaiter().GetResult(); var accountIds = SSOUtils.GetAccountIdsAsync(cachedSsoToken.AccessToken, SSORegion).GetAwaiter().GetResult(); var promptForAccountId = true; if (AccountId != null) { if (accountIds.Contains(AccountId)) { promptForAccountId = false; profileOptions.SsoAccountId = AccountId; } else { Console.WriteLine($"AccountId {AccountId} is invalid."); } } if (promptForAccountId) { profileOptions.SsoAccountId = PromptForAccountId(accountIds, existingProfile?.Options?.SsoAccountId); } var roles = SSOUtils.GetAccountRolesAsync(profileOptions.SsoAccountId, cachedSsoToken.AccessToken, SSORegion).GetAwaiter().GetResult(); var promptForRoleName = true; if (RoleName != null) { if (roles.Contains(RoleName)) { promptForRoleName = false; profileOptions.SsoRoleName = RoleName; } else { Console.WriteLine($"RoleName {RoleName} is invalid."); } } if (promptForRoleName) { profileOptions.SsoRoleName = PromptForRoleName(roles, existingProfile?.Options?.SsoRoleName); } if (IsInteractiveMode() && Region == null) { Region = PromptForRegion(existingProfile?.Region?.SystemName); } if (ProfileName == null) { ProfileName = PromptForProfileName(profileOptions.SsoAccountId, profileOptions.SsoRoleName); } var profile = new CredentialProfile(ProfileName, profileOptions); if (Region != null) { profile.Region = RegionEndpoint.GetBySystemName(Region); } profile.RegisterSsoProfileAndSession(); Console.WriteLine(); Console.WriteLine("To use this profile, specify the profile name using -ProfileName, as shown:"); Console.WriteLine(); Console.WriteLine(); Console.WriteLine($"Get-S3Bucket -ProfileName '{ProfileName}'"); Console.WriteLine(); } private bool IsInteractiveMode() { // The cmdlet will be in an interactive mode when any of the required parameter values are not set. return !(MyInvocation.BoundParameters.ContainsKey(nameof(ProfileName)) && MyInvocation.BoundParameters.ContainsKey(nameof(AccountId)) && MyInvocation.BoundParameters.ContainsKey(nameof(RoleName)) && MyInvocation.BoundParameters.ContainsKey(nameof(SessionName)) && MyInvocation.BoundParameters.ContainsKey(nameof(StartUrl)) && MyInvocation.BoundParameters.ContainsKey(nameof(SSORegion))); } private string PromptForProfileName(string accountId, string roleName) { var defaultProfileName = $"{accountId}-{roleName}"; bool IsProfileNameValid(string profileName) => !string.IsNullOrEmpty(profileName); return PromptForInput("ProfileName", null, defaultProfileName, IsProfileNameValid, false); } private string PromptForStartUrl(string currentStartUrl) { bool IsUrlValid(string url) => !string.IsNullOrEmpty(url) && Uri.IsWellFormedUriString(url, UriKind.Absolute); return PromptForInput("SSO start URL", currentStartUrl, null, IsUrlValid, false); } private string PromptForSSORegion(string currentSSORegion) { return PromptForInput("SSO Region", currentSSORegion, null, IsRegionValid, false); } private string PromptForRegion(string currentRegion) { return PromptForInput("Profile region", currentRegion, null, IsRegionValid, true); } private string PromptForSessionName(string currentSessionName) { string defaultSessionName = "sso-session-" + new Uri(StartUrl).Host.Split('.')[0]; bool IsSessionNameValid(string sessionName) => !string.IsNullOrEmpty(sessionName); return PromptForInput("SessionName", currentSessionName, defaultSessionName, IsSessionNameValid, false); } private string[] PromptForRegistrationScopes() { var defaultRegistrationScope = $"sso:account:access"; var registrationScopes = new List<string>(); var descriptions = new Collection<FieldDescription> { new FieldDescription($"SSO registration scopes [{defaultRegistrationScope}]") }; var promptValue = Host.UI.Prompt("", "", descriptions); var enteredRegistrationScopes = promptValue.Values.First().ToString(); if (string.IsNullOrEmpty(enteredRegistrationScopes)) { registrationScopes.Add(defaultRegistrationScope); } else { registrationScopes = enteredRegistrationScopes.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Replace("'", "").Replace("\"", "").Trim()).ToList(); } return registrationScopes.ToArray(); } private bool IsRegionValid(string region) { var isRegionValid = false; try { if (!string.IsNullOrEmpty(region) && RegionEndpoint.GetBySystemName(region).DisplayName != "Unknown") { isRegionValid = true; } } catch { } return isRegionValid; } private string PromptForChoice(List<string> choiceValues, string currentValue, string label, string labelPlural, object targetObject) { string choiceValue = null; switch (choiceValues.Count) { case 0: this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"No {labelPlural} are set up for SSO. Please refer to https://docs.aws.amazon.com/powershell/latest/userguide/creds-idc.html for instructions."), "ArgumentException", ErrorCategory.InvalidArgument, targetObject)); break; case 1: choiceValue = choiceValues[0]; Console.WriteLine($"The only AWS {label} available to you is: {choiceValue}"); Console.WriteLine($"Using the {label} : {choiceValue}"); break; default: { bool IsChoiceValid(string choice) => !string.IsNullOrEmpty(choice) && choiceValues.Contains(choice); var sb = new StringBuilder(); sb.Append($"The following {choiceValues.Count} AWS {labelPlural} are available to you."); sb.Append(Environment.NewLine); foreach (var choice in choiceValues.OrderBy(x => x)) { sb.Append($" {choice}"); sb.Append(Environment.NewLine); } var caption = sb.ToString(); choiceValue = PromptForInput($"Enter {label}", currentValue, null, IsChoiceValid, false, caption); Console.WriteLine($"Using the {label} : {choiceValue}"); break; } } return choiceValue; } // Prompts for input until the entered value is valid. private string PromptForInput(string fieldDescription, string currentValue, string defaultValue, Func<string, bool> isValueValid, bool optionalInput, string caption = "") { var displayDefault = currentValue ?? defaultValue; if (isValueValid(displayDefault)) { fieldDescription += $" [{displayDefault}]"; } if (displayDefault == null && optionalInput) { // When there is no current value or default for an optional input, append [None] in the display fieldDescription += $" [None]"; } string enteredValue; var descriptions = new Collection<FieldDescription> { new FieldDescription(fieldDescription) }; while (true) { enteredValue = Host.UI.Prompt(caption, "", descriptions).Values.First().ToString().Trim(); if (string.IsNullOrEmpty(enteredValue)) { if (displayDefault != null) { enteredValue = displayDefault; } else if (optionalInput) { enteredValue = null; break; } } if (isValueValid(enteredValue)) { break; } else { Console.WriteLine($"Invalid input: {enteredValue}"); } }; return enteredValue; } private string PromptForAccountId(List<string> accountIds, string currentAccountId) { return PromptForChoice(accountIds, currentAccountId, "account Id", "accounts", this.AccountId); } private string PromptForRoleName(List<string> roles, string currentRoleName) { return PromptForChoice(roles, currentRoleName, "role name", "roles", this.RoleName); } } /// <summary> /// <para> /// The Set-AWSSSOSessionConfiguration cmdlet creates or updates an sso-session section in a configuration file. /// The SSO session can then be associated with a profile to retrieve SSO access tokens and AWS credentials. /// The configuration is saved in the shared configuration file '~/.aws/config'. /// </para> /// </summary> [Cmdlet("Set", "AWSSSOSessionConfiguration", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] [AWSCmdlet("Creates or updates an SSO profile with the configuration values required to use AWS SSO." )] [OutputType("None")] [AWSCmdletOutput("None", "This cmdlet does not generate any output.")] public class SetAWSSSOSessionConfigurationCmdlet : BaseCmdlet { private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly ILogger _logger = Logger.GetLogger(typeof(SetAWSSSOSessionConfigurationCmdlet)); /// <summary> /// <para>Name of an sso-session section of the configuration file that is used to group configuration variables for acquiring SSO access tokens, which can then be used to acquire AWS credentials.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true, Mandatory = true)] public string SessionName { get; set; } /// <summary> /// <para>URL that points to the organization's AWS access portal.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true, Mandatory = true)] public string StartUrl { get; set; } /// <summary> /// <para>AWS Region that contains the AWS access portal host. This is separate from, and can be a different Region than, the profile region parameter.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true, Mandatory = true)] public string SSORegion { get; set; } /// <summary> /// <para>List of scopes to be authorized for the SSO session. Scopes authorize access to IAM Identity Center bearer token authorized endpoints. Default value is sso:account:access.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true)] public string[] RegistrationScopes { get; set; } protected override void StopProcessing() { base.StopProcessing(); _cancellationTokenSource.Cancel(); } protected override void ProcessRecord() { base.ProcessRecord(); if (!(RegistrationScopes?.Length > 0)) { RegistrationScopes = new string[] { "sso:account:access" }; } var registrationScopesString = string.Join(",", RegistrationScopes); // validate StartUrl if (!Uri.IsWellFormedUriString(StartUrl, UriKind.Absolute)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException($"StartUrl {StartUrl} is not valid."), "ArgumentException", ErrorCategory.InvalidArgument, this.StartUrl)); } // ensure StartUrl ends with "/" for consistency if (!StartUrl.EndsWith("/")) StartUrl = StartUrl.TrimEnd('/'); var profileOptions = new CredentialProfileOptions { SsoRegistrationScopes = registrationScopesString, SsoSession = SessionName, SsoStartUrl = StartUrl, SsoRegion = SSORegion }; var resourceIdentifiersText = FormatParameterValuesForConfirmationMsg(nameof(this.SessionName), MyInvocation.BoundParameters); if (!ConfirmShouldProceed(false, resourceIdentifiersText, "Set-AWSSSOSessionConfiguration")) { return; } profileOptions.RegisterSsoSession(); } } /// <summary> /// <para> /// The Invoke-AWSSSOLogout cmdlet removes cached IAM Identity Center SSO tokens. By default, it removes tokens across all profiles. /// Optionally, the cached token associated with a particular profile can be removed by using the -ProfileName parameter. /// To use these profiles again, run the Invoke-AWSSSOLogin cmdlet. /// </para> /// </summary> [Cmdlet("Invoke", "AWSSSOLogout", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Medium)] [AWSCmdlet("By default, removes all cached AWS SSO tokens across all profiles." )] [OutputType("None")] [AWSCmdletOutput("None", "This cmdlet does not generate any output.")] public class InvokeAWSSSOLogoutCmdlet : BaseCmdlet { private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly ILogger _logger = Logger.GetLogger(typeof(InvokeAWSSSOLogoutCmdlet)); /// <summary> /// <para>Name of the profile in the shared configuration file '~/.aws/config'.</para> /// </summary> [Parameter(ValueFromPipelineByPropertyName = true, ValueFromPipeline = true)] public string ProfileName { get; set; } protected override void StopProcessing() { base.StopProcessing(); _cancellationTokenSource.Cancel(); } protected override void ProcessRecord() { base.ProcessRecord(); var resourceIdentifiersText = ""; if (!string.IsNullOrEmpty(ProfileName)) { if (!SettingsStore.TryGetProfile(ProfileName, null, out var profile)) { this.ThrowTerminatingError(new ErrorRecord( new ArgumentException( $"profile {ProfileName} not found in the shared config (~/.aws/config) file."), "ArgumentException", ErrorCategory.InvalidArgument, this.ProfileName)); } ValidateSSOProfile(profile.Options); resourceIdentifiersText = FormatParameterValuesForConfirmationMsg(nameof(this.ProfileName), MyInvocation.BoundParameters); if (!ConfirmShouldProceed(false, resourceIdentifiersText, "Invoke-AWSSSOLogout")) { return; } WriteVerbose($"Logging out cached SSO token for profile {this.ProfileName}"); SSOUtils.LogoutAsync(profile: profile, cancellationToken: _cancellationTokenSource.Token).GetAwaiter().GetResult(); return; } resourceIdentifiersText = "All SSO tokens"; if (!ConfirmShouldProceed(false, resourceIdentifiersText, "Invoke-AWSSSOLogout")) { return; } WriteVerbose($"Logging out all cached SSO tokens."); SSOUtils.LogoutAsync(cancellationToken: _cancellationTokenSource.Token).GetAwaiter().GetResult(); } private void ValidateSSOProfile(CredentialProfileOptions options) { var missingProperties = SSOUtils.GetSSOMissingProperties(options); if (missingProperties.Any()) { var beginErrorMessage = $"There are missing SSO properties for the profile {ProfileName}. Update profile to add the following properties or use Invoke-AWSSSOLogoff to logoff all SSO tokens."; var missingPropertiesMessage = "missing SSO properties: " + string.Join(", ", missingProperties) + "."; this.ThrowTerminatingError(new ErrorRecord( new ArgumentException(beginErrorMessage + Environment.NewLine + missingPropertiesMessage), "ArgumentException", ErrorCategory.InvalidArgument, this)); } } } }