Skip to content

Commit 00da31e

Browse files
authored
Add ACR challenge-based authentication and remove basic auth policy (Azure#19696)
* add challenge-based auth policy * retrieve aad access token from request header. * use two http pipelines * test cleanup and re-record * update api * rename policy and begin work on test. * upgrade to latest swagger to reduce number of auth clients * add policy test * update api for changes from generated code * parameter refactor
1 parent b43221f commit 00da31e

File tree

57 files changed

+4270
-959
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4270
-959
lines changed

sdk/containerregistry/Azure.Containers.ContainerRegistry/api/Azure.Containers.ContainerRegistry.netstandard2.0.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ namespace Azure.Containers.ContainerRegistry
33
public partial class ContainerRegistryClient
44
{
55
protected ContainerRegistryClient() { }
6-
public ContainerRegistryClient(System.Uri endpoint, string username, string password) { }
7-
public ContainerRegistryClient(System.Uri endpoint, string username, string password, Azure.Containers.ContainerRegistry.ContainerRegistryClientOptions options) { }
6+
public ContainerRegistryClient(System.Uri endpoint, Azure.Core.TokenCredential credential) { }
7+
public ContainerRegistryClient(System.Uri endpoint, Azure.Core.TokenCredential credential, Azure.Containers.ContainerRegistry.ContainerRegistryClientOptions options) { }
88
public virtual Azure.Pageable<string> GetRepositories(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
99
public virtual Azure.AsyncPageable<string> GetRepositoriesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
1010
}
@@ -19,8 +19,8 @@ public enum ServiceVersion
1919
public partial class ContainerRepositoryClient
2020
{
2121
protected ContainerRepositoryClient() { }
22-
public ContainerRepositoryClient(System.Uri endpoint, string repository, string username, string password) { }
23-
public ContainerRepositoryClient(System.Uri endpoint, string repository, string username, string password, Azure.Containers.ContainerRegistry.ContainerRegistryClientOptions options) { }
22+
public ContainerRepositoryClient(System.Uri endpoint, string repository, Azure.Core.TokenCredential credential) { }
23+
public ContainerRepositoryClient(System.Uri endpoint, string repository, Azure.Core.TokenCredential credential, Azure.Containers.ContainerRegistry.ContainerRegistryClientOptions options) { }
2424
public virtual System.Uri Endpoint { get { throw null; } }
2525
public virtual Azure.Response DeleteTag(string tag, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
2626
public virtual System.Threading.Tasks.Task<Azure.Response> DeleteTagAsync(string tag, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
@@ -44,23 +44,21 @@ public ContentProperties() { }
4444
public partial class RepositoryProperties
4545
{
4646
internal RepositoryProperties() { }
47-
public System.DateTimeOffset? CreatedOn { get { throw null; } }
47+
public System.DateTimeOffset CreatedOn { get { throw null; } }
4848
public System.DateTimeOffset? LastUpdatedOn { get { throw null; } }
4949
public string Name { get { throw null; } }
50-
public string Registry { get { throw null; } }
51-
public int? RegistryArtifactCount { get { throw null; } }
52-
public int? TagCount { get { throw null; } }
50+
public int RegistryArtifactCount { get { throw null; } }
51+
public int TagCount { get { throw null; } }
5352
public Azure.Containers.ContainerRegistry.ContentProperties WriteableProperties { get { throw null; } }
5453
}
5554
public partial class TagProperties
5655
{
5756
internal TagProperties() { }
58-
public System.DateTimeOffset? CreatedOn { get { throw null; } }
57+
public System.DateTimeOffset CreatedOn { get { throw null; } }
5958
public string Digest { get { throw null; } }
60-
public System.DateTimeOffset? LastUpdatedOn { get { throw null; } }
61-
public Azure.Containers.ContainerRegistry.ContentProperties ModifiableProperties { get { throw null; } }
59+
public System.DateTimeOffset LastUpdatedOn { get { throw null; } }
6260
public string Name { get { throw null; } }
63-
public string Registry { get { throw null; } }
6461
public string Repository { get { throw null; } }
62+
public Azure.Containers.ContainerRegistry.ContentProperties WriteableProperties { get { throw null; } }
6563
}
6664
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
namespace Azure.Containers.ContainerRegistry
5+
{
6+
internal partial class AuthenticationRestClient : IContainerRegistryAuthenticationClient
7+
{
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Azure.Core;
8+
using Azure.Core.Pipeline;
9+
10+
namespace Azure.Containers.ContainerRegistry
11+
{
12+
/// <summary>
13+
/// Challenge-based authentication policy for Container Registry Service.
14+
///
15+
/// The challenge-based authorization flow for ACR is illustrated in the following steps.
16+
/// For example, GET /api/v1/acr/repositories translates into the following calls.
17+
///
18+
/// Step 1: GET /api/v1/acr/repositories
19+
/// Return Header: 401: www-authenticate header - Bearer realm="{url}",service="{serviceName}",scope="{scope}",error="invalid_token"
20+
///
21+
/// Step 2: Retrieve the serviceName, scope from the WWW-Authenticate header. (Parse the string.)
22+
///
23+
/// Step 3: POST /api/oauth2/exchange
24+
/// Request Body : { service, scope, grant-type, aadToken with ARM scope }
25+
/// Response Body: { acrRefreshToken }
26+
///
27+
/// Step 4: POST /api/oauth2/token
28+
/// Request Body: { acrRefreshToken, scope, grant-type }
29+
/// Response Body: { acrAccessToken }
30+
///
31+
/// Step 5: GET /api/v1/acr/repositories
32+
/// Request Header: { Bearer acrTokenAccess }
33+
/// </summary>
34+
internal class ContainerRegistryChallengeAuthenticationPolicy : BearerTokenChallengeAuthenticationPolicy
35+
{
36+
private readonly IContainerRegistryAuthenticationClient _authenticationClient;
37+
38+
public ContainerRegistryChallengeAuthenticationPolicy(TokenCredential credential, string aadScope, IContainerRegistryAuthenticationClient authenticationClient)
39+
: base(credential, aadScope)
40+
{
41+
Argument.AssertNotNull(credential, nameof(credential));
42+
Argument.AssertNotNull(aadScope, nameof(aadScope));
43+
44+
_authenticationClient = authenticationClient;
45+
}
46+
47+
protected override async ValueTask<bool> AuthenticateRequestOnChallengeAsync(HttpMessage message, bool async)
48+
{
49+
// Once we're here, we've completed Step 1.
50+
51+
// Step 2: Parse challenge string to retrieve serviceName and scope, where scope is the ACR Scope
52+
var service = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "service");
53+
var scope = AuthorizationChallengeParser.GetChallengeParameterFromResponse(message.Response, "Bearer", "scope");
54+
55+
string acrAccessToken = string.Empty;
56+
if (async)
57+
{
58+
// Step 3: Exchange AAD Access Token for ACR Refresh Token
59+
string acrRefreshToken = await ExchangeAadAccessTokenForAcrRefreshTokenAsync(message, service, true).ConfigureAwait(false);
60+
61+
// Step 4: Send in acrRefreshToken and get back acrAccessToken
62+
acrAccessToken = await ExchangeAcrRefreshTokenForAcrAccessTokenAsync(acrRefreshToken, service, scope, true, message.CancellationToken).ConfigureAwait(false);
63+
}
64+
else
65+
{
66+
// Step 3: Exchange AAD Access Token for ACR Refresh Token
67+
string acrRefreshToken = ExchangeAadAccessTokenForAcrRefreshTokenAsync(message, service, false).EnsureCompleted();
68+
69+
// Step 4: Send in acrRefreshToken and get back acrAccessToken
70+
acrAccessToken = ExchangeAcrRefreshTokenForAcrAccessTokenAsync(acrRefreshToken, service, scope, false, message.CancellationToken).EnsureCompleted();
71+
}
72+
73+
// Step 5 - Authorize Request. Note, we don't use SetAuthorizationHeader here, because it
74+
// sets an AAD access token header, and at this point we're done with AAD and using an ACR access token.
75+
message.Request.Headers.SetValue(HttpHeader.Names.Authorization, $"Bearer {acrAccessToken}");
76+
77+
return true;
78+
}
79+
80+
private async Task<string> ExchangeAadAccessTokenForAcrRefreshTokenAsync(HttpMessage message, string service, bool async)
81+
{
82+
string aadAccessToken = GetAuthorizationToken(message);
83+
84+
Response<AcrRefreshToken> acrRefreshToken = null;
85+
if (async)
86+
{
87+
acrRefreshToken = await _authenticationClient.ExchangeAadAccessTokenForAcrRefreshTokenAsync(service, aadAccessToken, message.CancellationToken).ConfigureAwait(false);
88+
}
89+
else
90+
{
91+
acrRefreshToken = _authenticationClient.ExchangeAadAccessTokenForAcrRefreshToken(service, aadAccessToken, message.CancellationToken);
92+
}
93+
94+
return acrRefreshToken.Value.RefreshToken;
95+
}
96+
97+
private async Task<string> ExchangeAcrRefreshTokenForAcrAccessTokenAsync(string acrRefreshToken, string service, string scope, bool async, CancellationToken cancellationToken)
98+
{
99+
Response<AcrAccessToken> acrAccessToken = null;
100+
if (async)
101+
{
102+
acrAccessToken = await _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessTokenAsync(service, scope, acrRefreshToken, cancellationToken).ConfigureAwait(false);
103+
}
104+
else
105+
{
106+
acrAccessToken = _authenticationClient.ExchangeAcrRefreshTokenForAcrAccessToken(service, scope, acrRefreshToken, cancellationToken);
107+
}
108+
109+
return acrAccessToken.Value.AccessToken;
110+
}
111+
112+
private static string GetAuthorizationToken(HttpMessage message)
113+
{
114+
string aadAuthHeader;
115+
if (!message.Request.Headers.TryGetValue(HttpHeader.Names.Authorization, out aadAuthHeader))
116+
{
117+
throw new InvalidOperationException("Failed to retrieve Authentication header from message request.");
118+
}
119+
120+
return aadAuthHeader.Remove(0, "Bearer ".Length);
121+
}
122+
}
123+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
7+
namespace Azure.Containers.ContainerRegistry
8+
{
9+
internal interface IContainerRegistryAuthenticationClient
10+
{
11+
Task<Response<AcrRefreshToken>> ExchangeAadAccessTokenForAcrRefreshTokenAsync(string service, string aadAccessToken, CancellationToken token = default);
12+
Response<AcrRefreshToken> ExchangeAadAccessTokenForAcrRefreshToken(string service, string aadAccessToken, CancellationToken token = default);
13+
14+
Task<Response<AcrAccessToken>> ExchangeAcrRefreshTokenForAcrAccessTokenAsync(string service, string scope, string acrRefreshToken, CancellationToken token = default);
15+
Response<AcrAccessToken> ExchangeAcrRefreshTokenForAcrAccessToken(string service, string scope, string acrRefreshToken, CancellationToken token = default);
16+
}
17+
}

sdk/containerregistry/Azure.Containers.ContainerRegistry/src/Azure.Containers.ContainerRegistry.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<Description></Description>
44
<AssemblyTitle>Microsoft Azure.Containers.ContainerRegistry client library</AssemblyTitle>
55
<Version>1.0.0-beta.1</Version>
66
<PackageTags>Azure Container Registry;$(PackageCommonTags)</PackageTags>
77
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
88
</PropertyGroup>
9-
9+
1010
<ItemGroup>
1111
<PackageReference Include="System.Text.Json" />
1212
</ItemGroup>
@@ -15,7 +15,9 @@
1515
<ItemGroup>
1616
<Compile Include="$(AzureCoreSharedSources)Argument.cs" LinkBase="Shared" />
1717
<Compile Include="$(AzureCoreSharedSources)ArrayBufferWriter.cs" LinkBase="Shared" />
18+
<Compile Include="$(AzureCoreSharedSources)AuthorizationChallengeParser.cs" LinkBase="Shared" />
1819
<Compile Include="$(AzureCoreSharedSources)AzureResourceProviderNamespaceAttribute.cs" LinkBase="Shared" />
20+
<Compile Include="$(AzureCoreSharedSources)BearerTokenChallengeAuthenticationPolicy.cs" LinkBase="Shared" />
1921
<Compile Include="$(AzureCoreSharedSources)ClientDiagnostics.cs" LinkBase="Shared" />
2022
<Compile Include="$(AzureCoreSharedSources)ContentTypeUtilities.cs" LinkBase="Shared" />
2123
<Compile Include="$(AzureCoreSharedSources)DiagnosticScope.cs" LinkBase="Shared" />

sdk/containerregistry/Azure.Containers.ContainerRegistry/src/ContainerRegistryClient.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,34 @@ public partial class ContainerRegistryClient
1616
{
1717
private readonly Uri _endpoint;
1818
private readonly HttpPipeline _pipeline;
19+
private readonly HttpPipeline _acrAuthPipeline;
1920
private readonly ClientDiagnostics _clientDiagnostics;
2021
private readonly ContainerRegistryRestClient _restClient;
2122

23+
private readonly AuthenticationRestClient _acrAuthClient;
24+
private readonly string AcrAadScope = "https://management.core.windows.net/.default";
25+
2226
/// <summary>
23-
/// <paramref name="endpoint"/>
2427
/// </summary>
25-
public ContainerRegistryClient(Uri endpoint, string username, string password) : this(endpoint, username, password, new ContainerRegistryClientOptions())
28+
public ContainerRegistryClient(Uri endpoint, TokenCredential credential) : this(endpoint, credential, new ContainerRegistryClientOptions())
2629
{
2730
}
2831

2932
/// <summary>
3033
/// </summary>
31-
/// <param name="endpoint"></param>
32-
/// <param name="username"></param>
33-
/// <param name="password"></param>
34-
/// <param name="options"></param>
35-
public ContainerRegistryClient(Uri endpoint, string username, string password, ContainerRegistryClientOptions options)
34+
public ContainerRegistryClient(Uri endpoint, TokenCredential credential, ContainerRegistryClientOptions options)
3635
{
3736
Argument.AssertNotNull(endpoint, nameof(endpoint));
37+
Argument.AssertNotNull(credential, nameof(credential));
3838
Argument.AssertNotNull(options, nameof(options));
3939

40-
_pipeline = HttpPipelineBuilder.Build(options, new BasicAuthenticationPolicy(username, password));
41-
40+
_endpoint = endpoint;
4241
_clientDiagnostics = new ClientDiagnostics(options);
4342

44-
_endpoint = endpoint;
43+
_acrAuthPipeline = HttpPipelineBuilder.Build(options);
44+
_acrAuthClient = new AuthenticationRestClient(_clientDiagnostics, _acrAuthPipeline, endpoint.AbsoluteUri);
4545

46+
_pipeline = HttpPipelineBuilder.Build(options, new ContainerRegistryChallengeAuthenticationPolicy(credential, AcrAadScope, _acrAuthClient));
4647
_restClient = new ContainerRegistryRestClient(_clientDiagnostics, _pipeline, _endpoint.AbsoluteUri);
4748
}
4849

0 commit comments

Comments
 (0)