Skip to content

Az.accounts 2.1.2 #13397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 3, 2020
Merged
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
31 changes: 29 additions & 2 deletions src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
using System.Threading;
using System.Threading.Tasks;

using Azure.Identity;

using Microsoft.Azure.Commands.Common.Authentication;
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core;
Expand All @@ -29,6 +31,7 @@
using Microsoft.Azure.Commands.Profile.Properties;
using Microsoft.Azure.Commands.ResourceManager.Common;
using Microsoft.Azure.PowerShell.Authenticators;
using Microsoft.Identity.Client;
using Microsoft.WindowsAzure.Commands.Common;
using Microsoft.WindowsAzure.Commands.Utilities.Common;

Expand Down Expand Up @@ -398,12 +401,36 @@ public override void ExecuteCmdlet()
}

HandleActions();
var result = (PSAzureProfile) (task.ConfigureAwait(false).GetAwaiter().GetResult());
WriteObject(result);

try
{
var result = (PSAzureProfile)(task.ConfigureAwait(false).GetAwaiter().GetResult());
WriteObject(result);
}
catch (AuthenticationFailedException ex)
{
if(IsUnableToOpenWebPageError(ex))
{
WriteWarning(Resources.InteractiveAuthNotSupported);
WriteDebug(ex.ToString());
}
else
{
WriteWarning(Resources.SuggestToUseDeviceCodeAuth);
WriteDebug(ex.ToString());
throw;
}
}
});
}
}

private bool IsUnableToOpenWebPageError(AuthenticationFailedException exception)
{
return exception.InnerException is MsalClientException && ((MsalClientException)exception.InnerException)?.ErrorCode == MsalError.LinuxXdgOpen
|| (exception.Message?.ToLower()?.Contains("unable to open a web page") ?? false);
}

private ConcurrentQueue<Task> _tasks = new ConcurrentQueue<Task>();

private void HandleActions()
Expand Down
7 changes: 3 additions & 4 deletions src/Accounts/Accounts/Az.Accounts.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Generated by: Microsoft Corporation
#
# Generated on: 10/23/2020
# Generated on: 11/2/2020
#

@{
Expand All @@ -12,7 +12,7 @@
# RootModule = ''

# Version number of this module.
ModuleVersion = '2.1.0'
ModuleVersion = '2.1.2'

# Supported PSEditions
CompatiblePSEditions = 'Core', 'Desktop'
Expand Down Expand Up @@ -143,8 +143,7 @@ PrivateData = @{
# IconUri = ''

# ReleaseNotes of this module
ReleaseNotes = '* [Breaking Change] Removed ''Get-AzProfile'' and ''Select-AzProfile''
* Replaced Azure Directory Authentication Library with Microsoft Authentication Library(MSAL)'
ReleaseNotes = '* Fixed one issue related to MSI'

# Prerelease string of this module
# Prerelease = ''
Expand Down
8 changes: 8 additions & 0 deletions src/Accounts/Accounts/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
-->
## Upcoming Release

## Version 2.1.2
* Fixed one issue related to MSI

## Version 2.1.1
* Fixed the issue that token is not renewed after expiring for LRO [#13367]
* Fixed the issue that AccountId is not respected in MSI [#13376]
* Fixed the issue that error message is unclear if browser is not avaialable for Interactive auth [#13340]

## Version 2.1.0
* [Breaking Change] Removed `Get-AzProfile` and `Select-AzProfile`
* Replaced Azure Directory Authentication Library with Microsoft Authentication Library(MSAL)
Expand Down
4 changes: 2 additions & 2 deletions src/Accounts/Accounts/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:

[assembly: AssemblyVersion("2.1.0")]
[assembly: AssemblyFileVersion("2.1.0")]
[assembly: AssemblyVersion("2.1.2")]
[assembly: AssemblyFileVersion("2.1.2")]
#if !SIGN
[assembly: InternalsVisibleTo("Microsoft.Azure.PowerShell.Cmdlets.Accounts.Test")]
#endif
Expand Down
18 changes: 18 additions & 0 deletions src/Accounts/Accounts/Properties/Resources.Designer.cs

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

6 changes: 6 additions & 0 deletions src/Accounts/Accounts/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -513,4 +513,10 @@
<data name="SendFeedbackOpenLinkManually" xml:space="preserve">
<value>Use a web browser to open the page {0}.</value>
</data>
<data name="InteractiveAuthNotSupported" xml:space="preserve">
<value>Interactive authentication is not supported in this session, please run Connect-AzAccount using switch -DeviceCode.</value>
</data>
<data name="SuggestToUseDeviceCodeAuth" xml:space="preserve">
<value>Please run 'Connect-AzAccount -DeviceCode' if browser is not supported in this session.</value>
</data>
</root>
3 changes: 1 addition & 2 deletions src/Accounts/Accounts/help/Connect-AzAccount.md
Original file line number Diff line number Diff line change
Expand Up @@ -610,8 +610,7 @@ Accept wildcard characters: False

### -UseDeviceAuthentication

Use device code authentication instead of a browser control. This is the default authentication type
for PowerShell version 6 and higher.
Use device code authentication instead of a browser control.

```yaml
Type: System.Management.Automation.SwitchParameter
Expand Down
4 changes: 2 additions & 2 deletions src/Accounts/Authentication/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("2.1.0")]
[assembly: AssemblyFileVersion("2.1.0")]
[assembly: AssemblyVersion("2.1.2")]
[assembly: AssemblyFileVersion("2.1.2")]
3 changes: 2 additions & 1 deletion src/Accounts/Authenticators/DeviceCodeAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public override Task<IAccessToken> Authenticate(AuthenticationParameters paramet
var authTask = codeCredential.AuthenticateAsync(requestContext, source.Token);
return MsalAccessToken.GetAccessTokenAsync(
authTask,
() => codeCredential.GetTokenAsync(requestContext, source.Token),
codeCredential,
requestContext,
source.Token);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Accounts/Authenticators/InteractiveUserAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public override Task<IAccessToken> Authenticate(AuthenticationParameters paramet

return MsalAccessToken.GetAccessTokenAsync(
authTask,
() => browserCredential.GetTokenAsync(requestContext, source.Token),
browserCredential,
requestContext,
source.Token);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// limitations under the License.
// ----------------------------------------------------------------------------------

using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

Expand All @@ -30,16 +31,18 @@ public class ManagedServiceIdentityAuthenticator : DelegatingAuthenticator
DefaultMSILoginUri = "http://169.254.169.254/metadata/identity/oauth2/token",
DefaultBackupMSILoginUri = "http://localhost:50342/oauth2/token";

private static Regex SystemMsiNameRegex = new Regex(@"MSI@\d+");

public override Task<IAccessToken> Authenticate(AuthenticationParameters parameters, CancellationToken cancellationToken)
{
var msiParameters = parameters as ManagedServiceIdentityParameters;

var scopes = new[] { GetResourceId(msiParameters.ResourceId, msiParameters.Environment) };
var requestContext = new TokenRequestContext(scopes);
ManagedIdentityCredential identityCredential = new ManagedIdentityCredential();
var tokenTask = identityCredential.GetTokenAsync(requestContext);
return MsalAccessToken.GetAccessTokenAsync(tokenTask, msiParameters.TenantId, msiParameters.Account.Id);
var userAccountId = SystemMsiNameRegex.IsMatch(msiParameters.Account.Id) ? null : msiParameters.Account.Id;
ManagedIdentityCredential identityCredential = new ManagedIdentityCredential(userAccountId);
return MsalAccessToken.GetAccessTokenAsync(identityCredential, requestContext, cancellationToken,
msiParameters.TenantId, msiParameters.Account.Id);
}

public override bool CanAuthenticate(AuthenticationParameters parameters)
Expand Down
67 changes: 48 additions & 19 deletions src/Accounts/Authenticators/MsalAccessToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace Microsoft.Azure.PowerShell.Authenticators
{
public class MsalAccessToken : IAccessToken
{
public string AccessToken { get; }
public string AccessToken { get; private set; }

public string UserId { get; }

Expand All @@ -39,50 +39,79 @@ public class MsalAccessToken : IAccessToken

public IDictionary<string, string> ExtendedProperties { get; } = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);

public MsalAccessToken(string token, string tenantId, string userId = null, string homeAccountId = null)
private DateTimeOffset ExpiredOn { get; set; }

private readonly static TimeSpan ExpirationThreshold = TimeSpan.FromMinutes(5);

private TokenCredential TokenCredential { get; set; }

private TokenRequestContext TokenRequestContext { get; set; }

public MsalAccessToken(TokenCredential tokenCredential, TokenRequestContext tokenRequestContext,
string token, DateTimeOffset expiresOn, string tenantId, string userId = null, string homeAccountId = null)
{
TokenCredential = tokenCredential;
TokenRequestContext = tokenRequestContext;
AccessToken = token;
ExpiredOn = expiresOn;
UserId = userId;
TenantId = tenantId;
HomeAccountId = homeAccountId;
}

public void AuthorizeRequest(Action<string, string> authTokenSetter)
{
Renew();
authTokenSetter("Bearer", AccessToken);
}

public static async Task<IAccessToken> GetAccessTokenAsync(
ValueTask<AccessToken> result,
TokenCredential tokenCredential,
TokenRequestContext requestContext,
CancellationToken cancellationToken,
string tenantId = null,
string userId = null,
string homeAccountId = "")
{
var token = await result;
return new MsalAccessToken(token.Token, tenantId, userId, homeAccountId);
var token = await tokenCredential.GetTokenAsync(requestContext, cancellationToken);
return new MsalAccessToken(tokenCredential, requestContext, token.Token, token.ExpiresOn, tenantId, userId, homeAccountId);
}

public static async Task<IAccessToken> GetAccessTokenAsync(
ValueTask<AccessToken> result,
Action action,
string tenantId = null,
string userId = null)
{
var token = await result;
action();
return new MsalAccessToken(token.Token, tenantId, userId);
}

public static async Task<IAccessToken> GetAccessTokenAsync(
Task<AuthenticationRecord> authTask,
Func<ValueTask<AccessToken>> getTokenAction,
CancellationToken cancellationToken = default(CancellationToken))
TokenCredential tokenCredential,
TokenRequestContext requestContext,
CancellationToken cancellationToken)
{
var record = await authTask;
cancellationToken.ThrowIfCancellationRequested();
var token = await getTokenAction();
var token = await tokenCredential.GetTokenAsync(requestContext, cancellationToken);

return new MsalAccessToken(token.Token, record.TenantId, record.Username, record.HomeAccountId);
return new MsalAccessToken(tokenCredential, requestContext, token.Token, token.ExpiresOn, record.TenantId, record.Username, record.HomeAccountId);
}


private void Renew()
{
if(IsNearExpiration())
{
var token = TokenCredential.GetToken(TokenRequestContext, default(CancellationToken));
AccessToken = token.Token;
ExpiredOn = token.ExpiresOn;
}
}

private bool IsNearExpiration()
{
#if DEBUG
if (Environment.GetEnvironmentVariable("FORCE_EXPIRED_ACCESS_TOKEN") != null)
{
return true;
}
#endif
var timeUntilExpiration = ExpiredOn - DateTimeOffset.UtcNow;
return timeUntilExpiration < ExpirationThreshold;
}
}
}
31 changes: 11 additions & 20 deletions src/Accounts/Authenticators/ServicePrincipalAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ namespace Microsoft.Azure.PowerShell.Authenticators
public class ServicePrincipalAuthenticator : DelegatingAuthenticator
{
private const string AuthenticationFailedMessage = "No certificate thumbprint or secret provided for the given service principal '{0}'.";
private ConcurrentDictionary<string, ClientCertificateCredential> ClientCertCredentialMap = new ConcurrentDictionary<string, ClientCertificateCredential>(StringComparer.OrdinalIgnoreCase);

//MSAL doesn't cache Service Principal into msal.cache
public override Task<IAccessToken> Authenticate(AuthenticationParameters parameters, CancellationToken cancellationToken)
Expand All @@ -54,31 +53,23 @@ public override Task<IAccessToken> Authenticate(AuthenticationParameters paramet
if (!string.IsNullOrEmpty(spParameters.Thumbprint))
{
//Service Principal with Certificate
ClientCertificateCredential certCredential;
if (!ClientCertCredentialMap.TryGetValue(spParameters.ApplicationId, out certCredential))
{
//first time login
var certificate = AzureSession.Instance.DataStore.GetCertificate(spParameters.Thumbprint);
certCredential = new ClientCertificateCredential(tenantId, spParameters.ApplicationId, certificate, options);
var tokenTask = certCredential.GetTokenAsync(requestContext, cancellationToken);
return MsalAccessToken.GetAccessTokenAsync(tokenTask,
() => { ClientCertCredentialMap[spParameters.ApplicationId] = certCredential; },
spParameters.TenantId,
spParameters.ApplicationId);
}
else
{
var tokenTask = certCredential.GetTokenAsync(requestContext, cancellationToken);
return MsalAccessToken.GetAccessTokenAsync(tokenTask, spParameters.TenantId, spParameters.ApplicationId);
}
var certificate = AzureSession.Instance.DataStore.GetCertificate(spParameters.Thumbprint);
ClientCertificateCredential certCredential = new ClientCertificateCredential(tenantId, spParameters.ApplicationId, certificate, options);
return MsalAccessToken.GetAccessTokenAsync(
certCredential,
requestContext,
cancellationToken,
spParameters.TenantId,
spParameters.ApplicationId);
}
else if (spParameters.Secret != null)
{
// service principal with secret
var secretCredential = new ClientSecretCredential(tenantId, spParameters.ApplicationId, spParameters.Secret.ConvertToString(), options);
var tokenTask = secretCredential.GetTokenAsync(requestContext, cancellationToken);
return MsalAccessToken.GetAccessTokenAsync(
tokenTask,
secretCredential,
requestContext,
cancellationToken,
spParameters.TenantId,
spParameters.ApplicationId);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Accounts/Authenticators/SilentAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public override Task<IAccessToken> Authenticate(AuthenticationParameters paramet
var cacheCredential = new SharedTokenCacheCredential(options);
var requestContext = new TokenRequestContext(scopes);
var tokenTask = cacheCredential.GetTokenAsync(requestContext);
return MsalAccessToken.GetAccessTokenAsync(tokenTask, silentParameters.TenantId, silentParameters.UserId, silentParameters.HomeAccountId);
return MsalAccessToken.GetAccessTokenAsync(cacheCredential, requestContext, cancellationToken, silentParameters.TenantId, silentParameters.UserId, silentParameters.HomeAccountId);
}

public override bool CanAuthenticate(AuthenticationParameters parameters)
Expand Down
3 changes: 2 additions & 1 deletion src/Accounts/Authenticators/UsernamePasswordAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ public override Task<IAccessToken> Authenticate(AuthenticationParameters paramet
var authTask = passwordCredential.AuthenticateAsync(requestContext, cancellationToken);
return MsalAccessToken.GetAccessTokenAsync(
authTask,
() => passwordCredential.GetTokenAsync(requestContext, cancellationToken),
passwordCredential,
requestContext,
cancellationToken);
}
else
Expand Down
Loading