Skip to content

Commit 33872e4

Browse files
authored
Test authentication when Key Vault switches tenants (Azure#35087)
* Support switching tenant IDs Fixes Azure#25086 * Resolve PR feedback * Assert the call count Alternatively, I could increment the count only when the tenant ID is specified, but @christothes and I agree the comments here are clearer.
1 parent aa4642b commit 33872e4

File tree

3 files changed

+118
-3
lines changed

3 files changed

+118
-3
lines changed

sdk/core/Azure.Core/src/Pipeline/BearerTokenAuthenticationPolicy.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,8 @@ public TokenRequestState(TokenRequestContext currentContext, TaskCompletionSourc
412412

413413
public bool RequestRequiresNewToken(TokenRequestContext context) =>
414414
(context.Scopes != null && !context.Scopes.AsSpan().SequenceEqual(CurrentContext.Scopes.AsSpan())) ||
415-
(context.Claims != null && !string.Equals(context.Claims, CurrentContext.Claims));
415+
(context.Claims != null && !string.Equals(context.Claims, CurrentContext.Claims)) ||
416+
(context.TenantId != null && !string.Equals(context.TenantId, CurrentContext.TenantId));
416417

417418
public bool BackgroundTokenAcquiredSuccessfully(DateTimeOffset now) =>
418419
BackgroundUpdateTcs != null &&

sdk/core/Azure.Core/tests/BearerTokenAuthenticationPolicyTests.cs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,122 @@ public async Task BearerTokenAuthenticationPolicy_BackgroundRefreshFailsAndLogs(
789789
Assert.IsTrue(logged);
790790
}
791791

792+
[Test]
793+
public async Task BearerTokenAuthenticationPolicy_SwitchedTenants()
794+
{
795+
var responses = new[]
796+
{
797+
new MockResponse(401)
798+
.WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/de763a21-49f7-4b08-a8e1-52c8fbc103b4"", resource=""https://vault.azure.net"""),
799+
800+
new MockResponse(200),
801+
new MockResponse(200),
802+
803+
// Moved tenants.
804+
new MockResponse(401)
805+
.WithHeader("WWW-Authenticate", @"Bearer authorization=""https://login.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47"", resource=""https://vault.azure.net""")
806+
.WithJson("""
807+
{
808+
"error": {
809+
"code": "Unauthorized",
810+
"message": "AKV10032: Invalid issuer. Expected one of https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/, https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/, https://sts.windows.net/e2d54eb5-3869-4f70-8578-dee5fc7331f4/, https://sts.windows.net/33e01921-4d64-4f8c-a055-5bdaffd5e33d/, https://sts.windows.net/975f013f-7f24-47e8-a7d3-abc4752bf346/, found https://sts.windows.net/96be4b7a-defb-4dc2-a31f-49ee6145d5ab/."
811+
}
812+
}
813+
"""),
814+
815+
new MockResponse(200),
816+
};
817+
818+
var transport = CreateMockTransport(responses);
819+
820+
string tenantId = null;
821+
int callCount = 0;
822+
var credential = new TokenCredentialStub((r, c) =>
823+
{
824+
tenantId = r.TenantId;
825+
Interlocked.Increment(ref callCount);
826+
827+
return new(Guid.NewGuid().ToString(), DateTimeOffset.Now.AddHours(2));
828+
}, IsAsync);
829+
var policy = new ChallengeBasedAuthenticationTestPolicy(credential, "scope");
830+
831+
await SendGetRequest(transport, policy, uri: new("https://example.com/1/Original"));
832+
Assert.AreEqual("de763a21-49f7-4b08-a8e1-52c8fbc103b4", tenantId);
833+
// This is initially 2 because the pipeline tries to pre-authenticate, then again when the test policy authenticates on a 401.
834+
Assert.AreEqual(2, callCount);
835+
836+
await SendGetRequest(transport, policy, uri: new("https://example.com/1/Original"));
837+
Assert.AreEqual("de763a21-49f7-4b08-a8e1-52c8fbc103b4", tenantId);
838+
Assert.AreEqual(2, callCount);
839+
840+
await SendGetRequest(transport, policy, uri: new("https://example.com/1/Original"));
841+
Assert.AreEqual("72f988bf-86f1-41af-91ab-2d7cd011db47", tenantId);
842+
// An additional call to TokenCredential.GetTokenAsync is expected now that the tenant has changed.
843+
Assert.AreEqual(3, callCount);
844+
}
845+
846+
private class ChallengeBasedAuthenticationTestPolicy : BearerTokenAuthenticationPolicy
847+
{
848+
public string TenantId { get; private set; }
849+
850+
private readonly ConcurrentQueue<string> _tenantIds = new(
851+
new[]
852+
{
853+
"de763a21-49f7-4b08-a8e1-52c8fbc103b4",
854+
"72f988bf-86f1-41af-91ab-2d7cd011db47",
855+
});
856+
857+
public ChallengeBasedAuthenticationTestPolicy(TokenCredential credential, string scope) : base(credential, scope)
858+
{
859+
}
860+
861+
protected override void AuthorizeRequest(HttpMessage message) =>
862+
AuthorizeRequestAsync(message, false).EnsureCompleted();
863+
864+
protected override async ValueTask AuthorizeRequestAsync(HttpMessage message) =>
865+
await AuthorizeRequestAsync(message, true).ConfigureAwait(false);
866+
867+
private async ValueTask AuthorizeRequestAsync(HttpMessage message, bool isAsync)
868+
{
869+
if (!message.Request.Headers.Contains(HttpHeader.Names.Authorization))
870+
{
871+
TokenRequestContext context = new(new[] { "scope" });
872+
if (isAsync)
873+
{
874+
await AuthenticateAndAuthorizeRequestAsync(message, context);
875+
}
876+
else
877+
{
878+
AuthenticateAndAuthorizeRequest(message, context);
879+
}
880+
}
881+
}
882+
883+
protected override bool AuthorizeRequestOnChallenge(HttpMessage message) =>
884+
AuthorizeRequestOnChallengeAsync(message, false).EnsureCompleted();
885+
886+
protected override async ValueTask<bool> AuthorizeRequestOnChallengeAsync(HttpMessage message) =>
887+
await AuthorizeRequestOnChallengeAsync(message, true).ConfigureAwait(false);
888+
889+
private async ValueTask<bool> AuthorizeRequestOnChallengeAsync(HttpMessage message, bool isAsync)
890+
{
891+
Assert.IsTrue(_tenantIds.TryDequeue(out string tenantId));
892+
TenantId = tenantId;
893+
894+
TokenRequestContext context = new(new[] { "scope" }, tenantId: tenantId);
895+
if (isAsync)
896+
{
897+
await AuthenticateAndAuthorizeRequestAsync(message, context);
898+
}
899+
else
900+
{
901+
AuthenticateAndAuthorizeRequest(message, context);
902+
}
903+
904+
return true;
905+
}
906+
}
907+
792908
private class TokenCredentialStub : TokenCredential
793909
{
794910
public TokenCredentialStub(Func<TokenRequestContext, CancellationToken, AccessToken> handler, bool isAsync)

sdk/resourcemanager/Azure.ResourceManager/src/Resources/Custom/TenantResource.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@ namespace Azure.ResourceManager.Resources
1111
/// <summary>
1212
/// A class representing the operations that can be performed over a specific subscription.
1313
/// </summary>
14-
#pragma warning disable CA1825
1514
[CodeGenSuppress("TenantResource", typeof(ArmClient), typeof(TenantData))]
1615
[CodeGenSuppress("Get", typeof(CancellationToken))]
1716
[CodeGenSuppress("GetAsync", typeof(CancellationToken))]
1817
[CodeGenSuppress("GetAvailableLocations", typeof(CancellationToken))]
1918
[CodeGenSuppress("GetAvailableLocationsAsync", typeof(CancellationToken))]
2019
[CodeGenSuppress("GetTenants")]
2120
[CodeGenSuppress("CreateResourceIdentifier")]
22-
#pragma warning restore CA1825
2321
// [CodeGenSuppress("_tenantsRestClient")] // TODO: not working for private member
2422
public partial class TenantResource : ArmResource
2523
{

0 commit comments

Comments
 (0)