Skip to content

Commit b9e875b

Browse files
authored
Remove Scopes property from GetTokenOptions (Azure#50380)
1 parent 2d8794d commit b9e875b

File tree

6 files changed

+135
-58
lines changed

6 files changed

+135
-58
lines changed

sdk/core/Azure.Core/src/TokenRequestContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,9 @@ public TokenRequestContext(string[] scopes, string? parentRequestId = default, s
155155
/// </summary>
156156
internal static TokenRequestContext FromGetTokenOptions(GetTokenOptions getTokenOptions)
157157
{
158-
var scopes = getTokenOptions.Scopes.Span.ToArray();
158+
var scopes = getTokenOptions.Properties.TryGetValue(GetTokenOptions.ScopesPropertyName, out var scopesValue) && scopesValue is ReadOnlyMemory<string> memoryScopes
159+
? memoryScopes.ToArray()
160+
: throw new InvalidOperationException($"The '{GetTokenOptions.ScopesPropertyName}' property must be set in the {nameof(GetTokenOptions)}.");
159161
string? parentRequestId = getTokenOptions.Properties.TryGetValue("parentRequestId", out var parentRequestIdValue) && parentRequestIdValue is string ? (string)parentRequestIdValue : default;
160162
string? claims = getTokenOptions.Properties.TryGetValue("claims", out var claimsValue) && claimsValue is string ? (string)claimsValue : default;
161163
string? tenantId = getTokenOptions.Properties.TryGetValue("tenantId", out var tenantIdValue) && tenantIdValue is string ? (string)tenantIdValue : default;

sdk/core/System.ClientModel/api/System.ClientModel.net8.0.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,8 @@ public partial class GetTokenOptions
218218
public const string RefreshUrlPropertyName = "refreshUrl";
219219
public const string ScopesPropertyName = "scopes";
220220
public const string TokenUrlPropertyName = "tokenUrl";
221-
public GetTokenOptions(System.ReadOnlyMemory<string> scopes, System.Collections.Generic.IReadOnlyDictionary<string, object> properties) { }
221+
public GetTokenOptions(System.Collections.Generic.IReadOnlyDictionary<string, object> properties) { }
222222
public System.Collections.Generic.IReadOnlyDictionary<string, object> Properties { get { throw null; } }
223-
public System.ReadOnlyMemory<string> Scopes { get { throw null; } }
224-
public System.ClientModel.Primitives.GetTokenOptions WithAdditionalScopes(System.ReadOnlyMemory<string> additionalScopes) { throw null; }
225223
}
226224
public partial class HttpClientPipelineTransport : System.ClientModel.Primitives.PipelineTransport, System.IDisposable
227225
{

sdk/core/System.ClientModel/api/System.ClientModel.netstandard2.0.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,8 @@ public partial class GetTokenOptions
218218
public const string RefreshUrlPropertyName = "refreshUrl";
219219
public const string ScopesPropertyName = "scopes";
220220
public const string TokenUrlPropertyName = "tokenUrl";
221-
public GetTokenOptions(System.ReadOnlyMemory<string> scopes, System.Collections.Generic.IReadOnlyDictionary<string, object> properties) { }
221+
public GetTokenOptions(System.Collections.Generic.IReadOnlyDictionary<string, object> properties) { }
222222
public System.Collections.Generic.IReadOnlyDictionary<string, object> Properties { get { throw null; } }
223-
public System.ReadOnlyMemory<string> Scopes { get { throw null; } }
224-
public System.ClientModel.Primitives.GetTokenOptions WithAdditionalScopes(System.ReadOnlyMemory<string> additionalScopes) { throw null; }
225223
}
226224
public partial class HttpClientPipelineTransport : System.ClientModel.Primitives.PipelineTransport, System.IDisposable
227225
{
Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
using System.Collections.Generic;
54
using System.Collections.ObjectModel;
6-
using System.Linq;
75

86
namespace System.ClientModel.Primitives;
97

@@ -32,11 +30,6 @@ public class GetTokenOptions
3230
/// </summary>
3331
public const string RefreshUrlPropertyName = "refreshUrl";
3432

35-
/// <summary>
36-
/// Gets the scopes required to authenticate.
37-
/// </summary>
38-
public ReadOnlyMemory<string> Scopes { get; }
39-
4033
/// <summary>
4134
/// Gets the properties to be used for token requests.
4235
/// </summary>
@@ -45,34 +38,14 @@ public class GetTokenOptions
4538
/// <summary>
4639
/// Creates a new instance of <see cref="GetTokenOptions"/> with the specified scopes.
4740
/// </summary>
48-
/// <param name="scopes">The scopes to be used in a call to <see cref="AuthenticationTokenProvider.GetToken(GetTokenOptions, CancellationToken)"/> or <see cref="AuthenticationTokenProvider.GetTokenAsync(GetTokenOptions, CancellationToken)"/></param>
4941
/// <param name="properties">The additional properties to be used for token requests.</param>
50-
public GetTokenOptions(ReadOnlyMemory<string> scopes, IReadOnlyDictionary<string, object> properties)
42+
public GetTokenOptions(IReadOnlyDictionary<string, object> properties)
5143
{
52-
Scopes = scopes;
5344
Properties = properties switch
5445
{
5546
Dictionary<string, object> dict => new ReadOnlyDictionary<string, object>(dict),
5647
ReadOnlyDictionary<string, object> readOnlyDict => readOnlyDict,
5748
_ => new ReadOnlyDictionary<string, object>(properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))
5849
};
5950
}
60-
61-
/// <summary>
62-
/// Creates a new instance of <see cref="GetTokenOptions"/> by combining the current scopes with additional scopes.
63-
/// </summary>
64-
/// <param name="additionalScopes">Additional authentication scopes to be combined with existing ones.</param>
65-
/// <returns>A new <see cref="GetTokenOptions"/> instance containing both original and additional scopes.</returns>
66-
/// <remarks>
67-
/// This method creates a new options instance rather than modifying the existing one, maintaining immutability.
68-
/// The order of scopes is preserved, with original scopes followed by additional scopes.
69-
/// </remarks>
70-
public GetTokenOptions WithAdditionalScopes(ReadOnlyMemory<string> additionalScopes)
71-
{
72-
var originalScopes = Scopes;
73-
var combined = new string[originalScopes.Length + additionalScopes.Length];
74-
originalScopes.Span.CopyTo(combined.AsSpan(0, originalScopes.Length));
75-
additionalScopes.Span.CopyTo(combined.AsSpan(originalScopes.Length));
76-
return new GetTokenOptions(combined, Properties);
77-
}
7851
}

sdk/core/System.ClientModel/src/Pipeline/BearerTokenPolicy.cs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace System.ClientModel.Primitives;
1111
public class BearerTokenPolicy : AuthenticationPolicy
1212
{
1313
private readonly AuthenticationTokenProvider _tokenProvider;
14-
private readonly GetTokenOptions _flowContext;
14+
private readonly GetTokenOptions? _flowContext;
1515

1616
/// <summary>
1717
/// Creates a new instance of <see cref="BearerTokenPolicy"/>.
@@ -32,7 +32,10 @@ public BearerTokenPolicy(AuthenticationTokenProvider tokenProvider, IEnumerable<
3232
public BearerTokenPolicy(AuthenticationTokenProvider tokenProvider, string scope)
3333
{
3434
_tokenProvider = tokenProvider;
35-
_flowContext = new GetTokenOptions(new[] { scope }, new Dictionary<string, object>());
35+
_flowContext = new GetTokenOptions(new Dictionary<string, object>()
36+
{
37+
[GetTokenOptions.ScopesPropertyName] = new ReadOnlyMemory<string>([scope])
38+
});
3639
}
3740

3841
/// <inheritdoc />
@@ -53,30 +56,47 @@ private async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<Pipe
5356
{
5457
throw new InvalidOperationException("Bearer token authentication is not permitted for non TLS protected (https) endpoints.");
5558
}
56-
AuthenticationToken token;
57-
if (message.TryGetProperty(typeof(GetTokenOptions), out var rawContext) && rawContext is GetTokenOptions scopesContext)
59+
AuthenticationToken? token = null;
60+
61+
// The following scenarios are supported:
62+
// 1. If the message does not have a GetTokenOptions property.
63+
// - When _flowContext is null, this shall be treated as a NoAuth scenario. The message will be processed without authentication.
64+
// - When _flowContext is not null, the message will be processed with the service level flow context as-is.
65+
// 2. If the message has a GetTokenOptions property, it shall contain a IEnumerable<IReadOnlyDictionary<string, object>>
66+
// - When _flowContext is null, the property value shall contain the flows supported by the operation.
67+
// If the operation defines additional scopes, they will be embedded in the 'scopes' property of any IEnumerable<IReadOnlyDictionary<string, object>>.
68+
// - When _flowContext is not null it shall be ignored and the property value shall define the flows supported by the operation.
69+
70+
if (message.TryGetProperty(typeof(GetTokenOptions), out var rawContext) && rawContext is IEnumerable<IReadOnlyDictionary<string, object>> flowsContexts)
5871
{
59-
var context = _flowContext.WithAdditionalScopes(scopesContext.Scopes);
60-
token = async ? await _tokenProvider.GetTokenAsync(context, message.CancellationToken).ConfigureAwait(false) :
61-
_tokenProvider.GetToken(context, message.CancellationToken);
72+
var context = GetOptionsFromContexts(flowsContexts, _tokenProvider);
73+
if (context is not null)
74+
{
75+
token = async ? await _tokenProvider.GetTokenAsync(context, message.CancellationToken).ConfigureAwait(false) :
76+
_tokenProvider.GetToken(context, message.CancellationToken);
77+
}
6278
}
63-
else
79+
else if (_flowContext is not null && _flowContext.Properties.Count > 0)
6480
{
6581
token = _tokenProvider.GetToken(_flowContext, message.CancellationToken);
6682
}
67-
message.Request.Headers.Set("Authorization", $"Bearer {token.TokenValue}");
6883

69-
if (async)
70-
{
71-
await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
72-
}
73-
else
84+
if (token is not null)
7485
{
75-
ProcessNext(message, pipeline, currentIndex);
86+
message.Request.Headers.Set("Authorization", $"Bearer {token.TokenValue}");
7687
}
88+
89+
if (async)
90+
{
91+
await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
92+
}
93+
else
94+
{
95+
ProcessNext(message, pipeline, currentIndex);
96+
}
7797
}
7898

79-
internal static GetTokenOptions GetOptionsFromContexts(IEnumerable<IReadOnlyDictionary<string, object>> contexts, AuthenticationTokenProvider tokenProvider)
99+
internal static GetTokenOptions? GetOptionsFromContexts(IEnumerable<IReadOnlyDictionary<string, object>> contexts, AuthenticationTokenProvider tokenProvider)
80100
{
81101
foreach (var context in contexts)
82102
{
@@ -86,6 +106,6 @@ internal static GetTokenOptions GetOptionsFromContexts(IEnumerable<IReadOnlyDict
86106
return options;
87107
}
88108
}
89-
throw new InvalidOperationException($"The service does not support any of the auth flows implemented by the supplied token provider {tokenProvider.GetType().FullName}.");
109+
return null;
90110
}
91111
}

sdk/core/System.ClientModel/tests/Auth/AuthenticationTokenProviderTests.cs

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.ClientModel.Primitives;
55
using System.Collections.Generic;
6+
using System.Linq;
67
using System.Net.Http;
78
using System.Net.Http.Headers;
89
using System.Text;
@@ -25,21 +26,39 @@ public void SampleUsage()
2526
client.Get();
2627
}
2728

29+
[Test]
30+
public void SupportsNoServiceLevelAuth()
31+
{
32+
// usage for TokenProvider2 abstract type
33+
AuthenticationTokenProvider provider = new ClientCredentialTokenProvider("myClientId", "myClientSecret");
34+
var client = new NoAuthClient(new Uri("http://localhost"), provider);
35+
client.Get();
36+
}
37+
2838
public class FooClient
2939
{
3040
// Generated from the TypeSpec spec.
31-
private readonly Dictionary<string, object>[] flows = [
41+
42+
private static readonly string readScope = "read";
43+
private readonly Dictionary<string, object>[] serviceFlows = [
3244
new Dictionary<string, object> {
3345
{ GetTokenOptions.ScopesPropertyName, new string[] { "baselineScope" } },
3446
{ GetTokenOptions.TokenUrlPropertyName , "https://myauthserver.com/token"},
3547
{ GetTokenOptions.RefreshUrlPropertyName, "https://myauthserver.com/refresh"}
3648
}
3749
];
3850

51+
private readonly Dictionary<string, object>[] getOperationsFlows = [
52+
new Dictionary<string, object> {
53+
{ GetTokenOptions.ScopesPropertyName, new string[] { "baselineScope", readScope } },
54+
{ GetTokenOptions.TokenUrlPropertyName , "https://myauthserver.com/token"},
55+
{ GetTokenOptions.RefreshUrlPropertyName, "https://myauthserver.com/refresh"}
56+
}
57+
];
58+
3959
private readonly IReadOnlyDictionary<string, object> _emptyProperties = new Dictionary<string, object>();
4060

4161
private ClientPipeline _pipeline;
42-
private static readonly string[] readScope = ["read"];
4362

4463
public FooClient(Uri uri, ApiKeyCredential credential)
4564
{
@@ -62,7 +81,7 @@ public FooClient(Uri uri, AuthenticationTokenProvider credential)
6281
});
6382
ClientPipeline pipeline = ClientPipeline.Create(options,
6483
perCallPolicies: ReadOnlySpan<PipelinePolicy>.Empty,
65-
perTryPolicies: [new BearerTokenPolicy(credential, flows)],
84+
perTryPolicies: [new BearerTokenPolicy(credential, serviceFlows)],
6685
beforeTransportPolicies: ReadOnlySpan<PipelinePolicy>.Empty);
6786
_pipeline = pipeline;
6887
}
@@ -71,7 +90,7 @@ public ClientResult Get()
7190
{
7291
var message = _pipeline.CreateMessage();
7392
message.ResponseClassifier = PipelineMessageClassifier.Create([200]);
74-
message.SetProperty(typeof(GetTokenOptions), new GetTokenOptions(readScope, _emptyProperties));
93+
message.SetProperty(typeof(GetTokenOptions), getOperationsFlows);
7594

7695
PipelineRequest request = message.Request;
7796
request.Method = "GET";
@@ -81,6 +100,53 @@ public ClientResult Get()
81100
}
82101
}
83102

103+
public class NoAuthClient
104+
{
105+
private readonly IReadOnlyDictionary<string, object> _emptyProperties = new Dictionary<string, object>();
106+
107+
private ClientPipeline _pipeline;
108+
109+
public NoAuthClient(Uri uri, ApiKeyCredential credential)
110+
{
111+
var options = new ClientPipelineOptions();
112+
options.Transport = new MockPipelineTransport("foo", m => new MockPipelineResponse(200));
113+
ClientPipeline pipeline = ClientPipeline.Create(options,
114+
perCallPolicies: ReadOnlySpan<PipelinePolicy>.Empty,
115+
perTryPolicies: [ApiKeyAuthenticationPolicy.CreateBasicAuthorizationPolicy(credential)],
116+
beforeTransportPolicies: ReadOnlySpan<PipelinePolicy>.Empty);
117+
_pipeline = pipeline;
118+
}
119+
120+
public NoAuthClient(Uri uri, AuthenticationTokenProvider credential)
121+
{
122+
var options = new ClientPipelineOptions();
123+
options.Transport = new MockPipelineTransport("foo",
124+
m =>
125+
{
126+
// Assert that the request has no authentication headers
127+
Assert.IsFalse(m.Request.Headers.TryGetValue("Authorization", out _), "Request should not have an Authorization header.");
128+
return new MockPipelineResponse(200);
129+
});
130+
ClientPipeline pipeline = ClientPipeline.Create(options,
131+
perCallPolicies: ReadOnlySpan<PipelinePolicy>.Empty,
132+
perTryPolicies: [new BearerTokenPolicy(credential, [_emptyProperties])],
133+
beforeTransportPolicies: ReadOnlySpan<PipelinePolicy>.Empty);
134+
_pipeline = pipeline;
135+
}
136+
137+
public ClientResult Get()
138+
{
139+
var message = _pipeline.CreateMessage();
140+
message.ResponseClassifier = PipelineMessageClassifier.Create([200]);
141+
142+
PipelineRequest request = message.Request;
143+
request.Method = "GET";
144+
request.Uri = new Uri("https://localhost/noAuth");
145+
_pipeline.Send(message);
146+
return ClientResult.FromResponse(message.Response!);
147+
}
148+
}
149+
84150
public class ClientCredentialTokenProvider : AuthenticationTokenProvider
85151
{
86152
private string _clientId;
@@ -148,8 +214,9 @@ public override async ValueTask<AuthenticationToken> GetTokenAsync(GetTokenOptio
148214
properties.TryGetValue(GetTokenOptions.TokenUrlPropertyName, out var tokenUri) && tokenUri is string tokenUriValue &&
149215
properties.TryGetValue(GetTokenOptions.RefreshUrlPropertyName, out var refreshUri) && refreshUri is string refreshUriValue)
150216
{
151-
return new GetTokenOptions(scopeArray, new Dictionary<string, object>
217+
return new GetTokenOptions(new Dictionary<string, object>
152218
{
219+
{ GetTokenOptions.ScopesPropertyName, new ReadOnlyMemory<string>(scopeArray) },
153220
{ GetTokenOptions.TokenUrlPropertyName, tokenUriValue },
154221
{ GetTokenOptions.RefreshUrlPropertyName, refreshUriValue }
155222
});
@@ -169,12 +236,13 @@ internal async ValueTask<AuthenticationToken> GetAccessTokenInternal(bool async,
169236
var authBytes = System.Text.Encoding.ASCII.GetBytes($"{_clientId}:{_clientSecret}");
170237
var authHeader = Convert.ToBase64String(authBytes);
171238
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authHeader);
239+
var scopes = ExtractScopes(properties.Properties);
172240

173241
// Create form content
174242
var formContent = new FormUrlEncodedContent(
175243
[
176244
new KeyValuePair<string, string>("grant_type", "client_credentials"),
177-
new KeyValuePair<string, string>("scope", string.Join(" ", properties.Scopes.Span.ToArray()))
245+
new KeyValuePair<string, string>("scope", string.Join(" ", scopes.ToArray()))
178246
]);
179247

180248
request.Content = formContent;
@@ -201,6 +269,24 @@ await _client.SendAsync(request) :
201269

202270
return new AuthenticationToken(accessToken!, tokenType!, expiresOn, refreshOn);
203271
}
272+
273+
private static ReadOnlyMemory<string> ExtractScopes(IReadOnlyDictionary<string, object> properties)
274+
{
275+
if (!properties.TryGetValue(GetTokenOptions.ScopesPropertyName, out var scopesValue) || scopesValue is null)
276+
{
277+
return ReadOnlyMemory<string>.Empty;
278+
}
279+
280+
return scopesValue switch
281+
{
282+
ReadOnlyMemory<string> memory => memory,
283+
Memory<string> memory => memory,
284+
string[] array => new ReadOnlyMemory<string>(array),
285+
ICollection<string> collection => new ReadOnlyMemory<string>([.. collection]),
286+
IEnumerable<string> enumerable => new ReadOnlyMemory<string>([.. enumerable]),
287+
_ => ReadOnlyMemory<string>.Empty
288+
};
289+
}
204290
}
205291

206292
public class ClientCredentialToken : AuthenticationToken

0 commit comments

Comments
 (0)