Skip to content

Commit 5c45b3b

Browse files
isra-felYeming Liuvidai-msft
authored
Sign in with Claims Challenge (#28193)
Co-authored-by: Yeming Liu <[email protected]> Co-authored-by: Vincent Dai <[email protected]>
1 parent 8e67334 commit 5c45b3b

19 files changed

+355
-142
lines changed

src/Accounts/Accounts.Test/SilentReAuthByTenantCmdletTest.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
using System.Threading.Tasks;
3737
using Xunit;
3838
using Xunit.Abstractions;
39+
using Microsoft.Azure.Commands.Common.Exceptions;
3940

4041
namespace Microsoft.Azure.Commands.ResourceManager.Common.Test
4142
{
@@ -55,9 +56,11 @@ public class SilentReAuthByTenantCmdletTest
5556
private const string fakeToken = "fakertoken";
5657

5758
private const string body200 = @"{{""value"":[{{""id"":""/tenants/{0}"",""tenantId"":""{0}"",""countryCode"":""US"",""displayName"":""AzureSDKTeam"",""domains"":[""AzureSDKTeam.onmicrosoft.com"",""azdevextest.com""],""tenantCategory"":""Home""}}]}}";
58-
private const string body401 = @"{""error"":{""code"":""AuthenticationFailed"",""message"":""Authentication failed.""}}";
59-
private const string WwwAuthenticateIP = @"Bearer authorization_uri=""https://login.windows.net/"", error=""invalid_token"", error_description=""Tenant IP Policy validate failed."", claims=""eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNjEzOTgyNjA2In0sInhtc19ycF9pcGFkZHIiOnsidmFsdWUiOiIxNjcuMjIwLjI1NS40MSJ9fX0=""";
60-
59+
private const string bodyErrorMessage401 = "Authentication failed.";
60+
private const string body401 = @"{""error"":{""code"":""AuthenticationFailed"",""message"":"""+bodyErrorMessage401+@"""}}";
61+
private const string claimsChallengeBase64 = "eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNjEzOTgyNjA2In0sInhtc19ycF9pcGFkZHIiOnsidmFsdWUiOiIxNjcuMjIwLjI1NS40MSJ9fX0=";
62+
private const string WwwAuthenticateIP = @"Bearer authorization_uri=""https://login.windows.net/"", error=""invalid_token"", error_description=""Tenant IP Policy validate failed."", claims="""+ claimsChallengeBase64+@"""";
63+
private const string identityExceptionMessage = "Exception from Azure Identity.";
6164
XunitTracingInterceptor xunitLogger;
6265

6366
public class GetAzureRMTenantCommandMock : GetAzureRMTenantCommand
@@ -171,7 +174,7 @@ public void SilentReauthenticateFailure()
171174
{
172175
return new ValueTask<AccessToken>(new AccessToken(fakeToken, DateTimeOffset.Now.AddHours(1)));
173176
}
174-
throw new CredentialUnavailableException("Exception from Azure Identity.");
177+
throw new CredentialUnavailableException(identityExceptionMessage);
175178
}
176179
));
177180
AzureSession.Instance.RegisterComponent(nameof(AzureCredentialFactory), () => mockAzureCredentialFactory.Object, true);
@@ -190,9 +193,11 @@ public void SilentReauthenticateFailure()
190193

191194
// Act
192195
cmdlet.InvokeBeginProcessing();
193-
AuthenticationFailedException e = Assert.Throws<AuthenticationFailedException>(() => cmdlet.ExecuteCmdlet());
194-
string errorMessage = $"Exception from Azure Identity.{Environment.NewLine}authorization_uri: https://login.windows.net/{Environment.NewLine}error: invalid_token{Environment.NewLine}error_description: Tenant IP Policy validate failed.{Environment.NewLine}claims: eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNjEzOTgyNjA2In0sInhtc19ycF9pcGFkZHIiOnsidmFsdWUiOiIxNjcuMjIwLjI1NS40MSJ9fX0={Environment.NewLine}";
195-
Assert.Equal(errorMessage, e.Message);
196+
AzPSAuthenticationFailedException e = Assert.Throws<AzPSAuthenticationFailedException>(() => cmdlet.ExecuteCmdlet());
197+
Assert.DoesNotContain(identityExceptionMessage, e.Message); // cause it's misleading
198+
Assert.Contains(bodyErrorMessage401, e.Message);
199+
Assert.Contains("Connect-AzAccount", e.Message);
200+
Assert.Contains(claimsChallengeBase64, e.Message);
196201
cmdlet.InvokeEndProcessing();
197202
}
198203
finally

src/Accounts/Accounts/Account/ConnectAzureRmAccount.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@
1313
// ----------------------------------------------------------------------------------
1414

1515
using Azure.Identity;
16-
1716
using Microsoft.Azure.Commands.Common.Authentication;
1817
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
1918
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core;
20-
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces;
2119
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Models;
2220
using Microsoft.Azure.Commands.Common.Authentication.Config.Models;
2321
using Microsoft.Azure.Commands.Common.Authentication.Factories;
@@ -41,7 +39,6 @@
4139
using Microsoft.WindowsAzure.Commands.Common.Sanitizer;
4240
using Microsoft.WindowsAzure.Commands.Common.Utilities;
4341
using Microsoft.WindowsAzure.Commands.Utilities.Common;
44-
4542
using System;
4643
using System.Collections.Concurrent;
4744
using System.Linq;
@@ -237,6 +234,10 @@ public class ConnectAzureRmAccountCommand : AzureContextModificationCmdlet, IMod
237234
[ValidateNotNullOrEmpty]
238235
public string FederatedToken { get; set; }
239236

237+
[Parameter(ParameterSetName = UserParameterSet, Mandatory = false, HelpMessage = "Specifies the claims challenge with base64 encoding.")]
238+
[ValidateNotNullOrEmpty]
239+
public string ClaimsChallenge { get; set; }
240+
240241
protected override IAzureContext DefaultContext
241242
{
242243
get
@@ -353,7 +354,6 @@ public override void ExecuteCmdlet()
353354
{
354355
subscriptionName = Subscription;
355356
}
356-
357357
}
358358
else if (AzureSession.Instance.TryGetComponent<IConfigManager>(nameof(IConfigManager), out var configManager))
359359
{
@@ -373,6 +373,15 @@ public override void ExecuteCmdlet()
373373
}
374374
}
375375

376+
string claimsChallenge = null;
377+
if (this.IsParameterBound(c => c.ClaimsChallenge))
378+
{
379+
if (!ClaimsChallengeUtilities.TryParseClaimsChallenge(ClaimsChallenge, out claimsChallenge))
380+
{
381+
throw new PSArgumentException(Resources.InvalidClaimsChallenge, nameof(ClaimsChallenge));
382+
}
383+
}
384+
376385
var azureAccount = new AzureAccount();
377386

378387
switch (ParameterSetName)
@@ -548,6 +557,7 @@ public override void ExecuteCmdlet()
548557
SkipValidation,
549558
new OpenIDConfiguration(Tenant, baseUri: _environment.ActiveDirectoryAuthority, httpClientFactory: httpClientFactory),
550559
WriteWarningEvent, //Could not use WriteWarning directly because it may be in worker thread
560+
claimsChallenge,
551561
name,
552562
shouldPopulateContextList,
553563
MaxContextPopulation,

src/Accounts/Accounts/ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
-->
2020

2121
## Upcoming Release
22+
* Added new parameter `-ClaimsChallenge` to `Connect-AzAccount` to support claims challenge authentication for MFA.
23+
* Refined the error message when a cmdlet fails because of policy violations about Multi-Factor Authentication (MFA) to provide more actionable guidance.
2224

2325
## Version 5.1.1
2426
* Updated the date in the message about multi-factor authentication (MFA). For more details, see https://go.microsoft.com/fwlink/?linkid=2276971

src/Accounts/Accounts/CommonModule/ContextAdapter.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,23 @@
1212
// limitations under the License.
1313
// ----------------------------------------------------------------------------------
1414

15-
using System;
16-
using System.Threading;
17-
using System.Threading.Tasks;
18-
using System.Net.Http;
19-
using System.Collections.Generic;
15+
using Azure.Identity;
16+
using Microsoft.Azure.Commands.Common.Authentication;
2017
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
2118
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Core;
19+
using Microsoft.Azure.Commands.Common.Exceptions;
2220
using Microsoft.Azure.Commands.Common.Utilities;
2321
using Microsoft.Azure.Commands.Profile.Models;
24-
using System.Globalization;
25-
using Microsoft.Azure.Commands.Common.Authentication;
22+
using Microsoft.Azure.Commands.Profile.Properties;
2623
using Microsoft.Azure.Commands.ResourceManager.Common.ArgumentCompleters;
24+
using System;
25+
using System.Collections.Generic;
26+
using System.Globalization;
2727
using System.Linq;
2828
using System.Management.Automation;
29-
using Microsoft.Azure.Commands.Profile.Properties;
30-
using Azure.Identity;
29+
using System.Net.Http;
30+
using System.Threading;
31+
using System.Threading.Tasks;
3132

3233
namespace Microsoft.Azure.Commands.Common
3334
{
@@ -200,14 +201,13 @@ internal async Task<HttpResponseMessage> AuthenticationHelper(IAzureContext cont
200201
{
201202
var response = await next(request, cancelToken, cancelAction, signal);
202203

203-
if (response.MatchClaimsChallengePattern())
204+
if (response.MatchClaimsChallengePattern(out var claimsChallenge))
204205
{
205206
//get token again with claims challenge
206207
if (accessToken is IClaimsChallengeProcessor processor)
207208
{
208209
try
209210
{
210-
var claimsChallenge = ClaimsChallengeUtilities.GetClaimsChallenge(response);
211211
if (!string.IsNullOrEmpty(claimsChallenge))
212212
{
213213
await processor.OnClaimsChallenageAsync(newRequest, claimsChallenge, cancelToken).ConfigureAwait(false);
@@ -219,7 +219,7 @@ internal async Task<HttpResponseMessage> AuthenticationHelper(IAzureContext cont
219219
}
220220
catch (AuthenticationFailedException e)
221221
{
222-
throw e.WithAdditionalMessage(response?.GetWwwAuthenticateMessage());
222+
throw new AzPSAuthenticationFailedException(ClaimsChallengeUtilities.FormatClaimsChallengeErrorMessage(claimsChallenge, await response?.Content?.ReadAsStringAsync()), null, e);
223223
}
224224
}
225225
}

src/Accounts/Accounts/Models/RMProfileClient.cs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313
// ----------------------------------------------------------------------------------
14+
1415
using Microsoft.Azure.Commands.Common.Authentication;
1516
using Microsoft.Azure.Commands.Common.Authentication.Abstractions;
1617
using Microsoft.Azure.Commands.Common.Authentication.Abstractions.Interfaces;
@@ -22,13 +23,11 @@
2223
using Microsoft.Azure.Commands.Profile.Utilities;
2324
using Microsoft.Rest.Azure;
2425
using Microsoft.WindowsAzure.Commands.Common;
25-
2626
using System;
2727
using System.Collections.Generic;
2828
using System.Linq;
2929
using System.Management.Automation;
3030
using System.Security;
31-
3231
using AuthenticationMessages = Microsoft.Azure.Commands.Common.Authentication.Properties.Resources;
3332
using ProfileMessages = Microsoft.Azure.Commands.Profile.Properties.Resources;
3433
using ResourceMessages = Microsoft.Azure.Commands.ResourceManager.Common.Properties.Resources;
@@ -52,7 +51,7 @@ private IAzureContext DefaultContext
5251
{
5352
get
5453
{
55-
if(_profile == null || _profile.DefaultContext == null || _profile.DefaultContext.Account == null)
54+
if (_profile == null || _profile.DefaultContext == null || _profile.DefaultContext.Account == null)
5655
{
5756
throw new PSInvalidOperationException(ResourceMessages.RunConnectAccount);
5857
}
@@ -130,21 +129,22 @@ public AzureRmProfile Login(
130129
bool skipValidation,
131130
IOpenIDConfiguration openIDConfigDoc,
132131
Action<string> promptAction,
132+
string claimsChallenge = null,
133133
string name = null,
134134
bool shouldPopulateContextList = true,
135135
int maxContextPopulation = Profile.ConnectAzureRmAccountCommand.DefaultMaxContextPopulation,
136136
string authScope = null,
137137
bool IsInteractiveContextSelectionEnabled = true)
138138
{
139-
139+
140140
WriteInteractiveInformationMessage($"{PSStyle.ForegroundColor.BrightYellow}{Resources.PleaseSelectAccount}{PSStyle.Reset}{System.Environment.NewLine}");
141141

142142
IAzureSubscription defaultSubscription = null;
143143
IAzureTenant defaultTenant = null;
144144
List<AzureSubscription> subscriptions = new List<AzureSubscription>();
145145
List<AzureSubscription> tempSubscriptions = null;
146146
string tenantName = null;
147-
147+
148148
bool selectSubscriptionFromList = AzureAccount.AccountType.User.Equals(account.Type) &&
149149
IsInteractiveContextSelectionEnabled &&
150150
string.IsNullOrEmpty(subscriptionId) &&
@@ -161,9 +161,9 @@ public AzureRmProfile Login(
161161
SubscritpionClientCandidates.Reset();
162162

163163
bool needDataPlanAuthFirst = !string.IsNullOrEmpty(authScope);
164-
if(needDataPlanAuthFirst)
164+
if (needDataPlanAuthFirst)
165165
{
166-
var token = AcquireAccessToken(account, environment, tenantIdOrName, password, promptBehavior, promptAction, authScope);
166+
var token = AcquireAccessToken(account, environment, tenantIdOrName, password, promptBehavior, promptAction, claimsChallenge, authScope);
167167
promptBehavior = ShowDialog.Never;
168168
}
169169

@@ -202,7 +202,8 @@ public AzureRmProfile Login(
202202
tenantIdOrName,
203203
password,
204204
promptBehavior,
205-
promptAction);
205+
promptAction,
206+
claimsChallenge);
206207

207208
if (!Guid.TryParse(tenantIdOrName, out Guid _))
208209
{
@@ -229,7 +230,7 @@ public AzureRmProfile Login(
229230
}
230231
}
231232
}
232-
catch(Exception e)
233+
catch (Exception e)
233234
{
234235
string baseMessage = string.Format(ProfileMessages.TenantDomainNotFound, tenantIdOrName);
235236
var typeMessageMap = new Dictionary<string, string>
@@ -293,7 +294,7 @@ public AzureRmProfile Login(
293294

294295
try
295296
{
296-
token = AcquireAccessToken(account, environment, tenant.Id, password, ShowDialog.Auto, null);
297+
token = AcquireAccessToken(account, environment, tenant.Id, password, ShowDialog.Auto, null, claimsChallenge);
297298
if (accountId == null)
298299
{
299300
accountId = account.Id;
@@ -314,7 +315,7 @@ public AzureRmProfile Login(
314315
token = null;
315316
}
316317
}
317-
catch(Exception e)
318+
catch (Exception e)
318319
{
319320
WriteWarningMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenant.Id, e.Message));
320321
WriteDebugMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenant.Id, e.ToString()));
@@ -334,7 +335,7 @@ public AzureRmProfile Login(
334335
defaultTenant = tempTenant;
335336
}
336337
}
337-
if(tempSubscription != null)
338+
if (tempSubscription != null)
338339
{
339340
subscriptions.AddRange(tempSubscriptions);
340341
}
@@ -397,7 +398,7 @@ public AzureRmProfile Login(
397398
{
398399
var defaultContext = _profile.DefaultContext;
399400
var populatedSubscriptions = (maxContextPopulation < 0 || selectSubscriptionFromList) ? ListSubscriptions(tenantIdOrName) : ListSubscriptions(tenantIdOrName).Take(maxContextPopulation);
400-
401+
401402
foreach (var subscription in populatedSubscriptions)
402403
{
403404
IAzureTenant tempTenant = InteractiveSubscriptionSelectionHelper.GetDetailedTenantFromQueryHistory(_queriedTenants, subscription.GetProperty(AzureSubscription.Property.Tenants)) ?? new AzureTenant()
@@ -449,7 +450,7 @@ public IAzureContext SetCurrentContext(string subscriptionNameOrId, string tenan
449450
}
450451

451452
var tenantFromSubscription = subscription.GetTenant();
452-
tenant = string.IsNullOrWhiteSpace(tenantId) ? (string.IsNullOrEmpty(tenantFromSubscription) ? context.Tenant : CreateTenant(tenantFromSubscription)): CreateTenant(tenantId);
453+
tenant = string.IsNullOrWhiteSpace(tenantId) ? (string.IsNullOrEmpty(tenantFromSubscription) ? context.Tenant : CreateTenant(tenantFromSubscription)) : CreateTenant(tenantId);
453454
}
454455
else if (!string.IsNullOrWhiteSpace(tenantId))
455456
{
@@ -536,14 +537,14 @@ public bool TryGetSubscriptionListByName(string tenantId, string subscriptionNam
536537
HashSet<Guid> existedSubscriptionIds = new HashSet<Guid>();
537538

538539
// Consider subscription in Home tenant first, exclude duplicate subscriptions by id.
539-
foreach(IAzureSubscription subscription in subscriptions)
540+
foreach (IAzureSubscription subscription in subscriptions)
540541
{
541-
if (subscription is PSAzureSubscription && subscription.GetTenant() != null
542+
if (subscription is PSAzureSubscription && subscription.GetTenant() != null
542543
&& subscription.GetHomeTenant().Equals(subscription.GetTenant()) && existedSubscriptionIds.Add(subscription.GetId()))
543544
{
544545
subscriptionList.Add(subscription);
545546
}
546-
547+
547548
}
548549
// Consider other subscriptions.
549550
foreach (IAzureSubscription subscription in subscriptions)
@@ -679,6 +680,7 @@ private IAccessToken AcquireAccessToken(
679680
SecureString password,
680681
string promptBehavior,
681682
Action<string> promptAction,
683+
string claimsChallenge = null,
682684
string resourceId = AzureEnvironment.Endpoint.ActiveDirectoryServiceEndpointResourceId)
683685
{
684686
if (account.Type == AzureAccount.AccountType.AccessToken)
@@ -689,11 +691,13 @@ private IAccessToken AcquireAccessToken(
689691

690692
var optionalParameters = new Dictionary<string, object>()
691693
{
692-
{AuthenticationFactory.TokenCacheParameterName, _cache},
693-
{AuthenticationFactory.ResourceIdParameterName, resourceId },
694-
{AuthenticationFactory.CmdletContextParameterName, CmdletContext }
694+
{ AuthenticationFactory.ResourceIdParameterName, resourceId },
695+
{ AuthenticationFactory.ClaimsChallengeParameterName, claimsChallenge },
696+
{ AuthenticationFactory.TokenCacheParameterName, _cache },
697+
{ AuthenticationFactory.CmdletContextParameterName, CmdletContext }
695698
};
696699

700+
697701
return AzureSession.Instance.AuthenticationFactory.Authenticate(
698702
account,
699703
environment,
@@ -814,7 +818,7 @@ private List<AzureTenant> ListAccountTenants(
814818

815819
result = SubscriptionAndTenantClient?.ListAccountTenants(commonTenantToken, environment);
816820
}
817-
catch(Exception e)
821+
catch (Exception e)
818822
{
819823
WriteWarningMessage(string.Format(ProfileMessages.UnableToAqcuireToken, commonTenant, e.Message));
820824
WriteDebugMessage(string.Format(ProfileMessages.UnableToAqcuireToken, commonTenant, e.ToString()));
@@ -861,7 +865,7 @@ private IEnumerable<AzureSubscription> ListAllSubscriptionsForTenant(
861865
{
862866
accessToken = AcquireAccessToken(account, environment, tenantId, password, promptBehavior, null);
863867
}
864-
catch(Exception e)
868+
catch (Exception e)
865869
{
866870
WriteWarningMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenantId, e.Message));
867871
WriteDebugMessage(string.Format(ProfileMessages.UnableToAqcuireToken, tenantId, e.ToString()));
@@ -881,7 +885,7 @@ private void WriteWarningMessage(string message)
881885

882886
private void WriteDebugMessage(string message)
883887
{
884-
if(DebugLog != null)
888+
if (DebugLog != null)
885889
{
886890
DebugLog(message);
887891
}

0 commit comments

Comments
 (0)