Skip to content

Commit 2254b4b

Browse files
Enable token cache for client assertion login flow (#20297)
* Add client assert code for upcoming change * cache token for client assertion * work around AssemblyInformationalVersion issue * changelog * Update README.md Co-authored-by: dingmeng-xue <[email protected]>
1 parent a00ef6a commit 2254b4b

19 files changed

+1758
-3
lines changed

src/Accounts/Accounts/ChangeLog.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
-->
2020

2121
## Upcoming Release
22-
* Enabled caching tokens when logging in with a service principal. This could reduce network traffic and improve performance.
22+
* Enabled caching tokens when logging in with a service principal or client assertion. [#20013]
23+
- This could reduce extra network traffic and improve performance.
24+
- It also fixed the incorrectly short lifespan of tokens.
2325
* Upgraded target framework of Microsoft.Identity.Client to net461 [#20189]
2426
* Stored `ServicePrincipalSecret` and `CertificatePassword` into `AzKeyStore`.
2527
* Updated the reference of Azure PowerShell Common to 1.3.65-preview.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
//
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Threading;
19+
using System.Threading.Tasks;
20+
21+
namespace Microsoft.Azure.PowerShell.Authenticators.Identity
22+
{
23+
/// <summary>
24+
/// Primitive that combines async lock and value cache
25+
/// </summary>
26+
/// <typeparam name="T"></typeparam>
27+
internal sealed class AsyncLockWithValue<T>
28+
{
29+
private readonly object _syncObj = new object();
30+
private Queue<TaskCompletionSource<Lock>> _waiters;
31+
private bool _isLocked;
32+
private bool _hasValue;
33+
private T _value;
34+
35+
/// <summary>
36+
/// Method that either returns cached value or acquire a lock.
37+
/// If one caller has acquired a lock, other callers will be waiting for the lock to be released.
38+
/// If value is set, lock is released and all waiters get that value.
39+
/// If value isn't set, the next waiter in the queue will get the lock.
40+
/// </summary>
41+
/// <param name="async"></param>
42+
/// <param name="cancellationToken"></param>
43+
/// <returns></returns>
44+
public async ValueTask<Lock> GetLockOrValueAsync(bool async, CancellationToken cancellationToken = default)
45+
{
46+
TaskCompletionSource<Lock> valueTcs;
47+
lock (_syncObj)
48+
{
49+
// If there is a value, just return it
50+
if (_hasValue)
51+
{
52+
return new Lock(_value);
53+
}
54+
55+
// If lock isn't acquire yet, acquire it and return to the caller
56+
if (!_isLocked)
57+
{
58+
_isLocked = true;
59+
return new Lock(this);
60+
}
61+
62+
// Check cancellationToken before instantiating waiter
63+
cancellationToken.ThrowIfCancellationRequested();
64+
65+
// If lock is already taken, create a waiter and wait either until value is set or lock can be acquired by this waiter
66+
if(_waiters is null)
67+
{
68+
_waiters = new Queue<TaskCompletionSource<Lock>>();
69+
}
70+
// if async == false, valueTcs will be waited only in this thread and only synchronously, so RunContinuationsAsynchronously isn't needed.
71+
valueTcs = new TaskCompletionSource<Lock>(async ? TaskCreationOptions.RunContinuationsAsynchronously : TaskCreationOptions.None);
72+
_waiters.Enqueue(valueTcs);
73+
}
74+
75+
try
76+
{
77+
if (async)
78+
{
79+
return await valueTcs.Task.AwaitWithCancellation(cancellationToken);
80+
}
81+
82+
#pragma warning disable AZC0104 // Use EnsureCompleted() directly on asynchronous method return value.
83+
#pragma warning disable AZC0111 // DO NOT use EnsureCompleted in possibly asynchronous scope.
84+
valueTcs.Task.Wait(cancellationToken);
85+
return valueTcs.Task.EnsureCompleted();
86+
#pragma warning restore AZC0111 // DO NOT use EnsureCompleted in possibly asynchronous scope.
87+
#pragma warning restore AZC0104 // Use EnsureCompleted() directly on asynchronous method return value.
88+
}
89+
catch (OperationCanceledException)
90+
{
91+
// Throw OperationCanceledException only if another thread hasn't set a value to this waiter
92+
// by calling either Reset or SetValue
93+
if (valueTcs.TrySetCanceled(cancellationToken))
94+
{
95+
throw;
96+
}
97+
98+
return valueTcs.Task.Result;
99+
}
100+
}
101+
102+
/// <summary>
103+
/// Set value to the cache and to all the waiters
104+
/// </summary>
105+
/// <param name="value"></param>
106+
private void SetValue(T value)
107+
{
108+
Queue<TaskCompletionSource<Lock>> waiters;
109+
lock (_syncObj)
110+
{
111+
_value = value;
112+
_hasValue = true;
113+
_isLocked = false;
114+
if (_waiters == default)
115+
{
116+
return;
117+
}
118+
119+
waiters = _waiters;
120+
_waiters = default;
121+
}
122+
123+
while (waiters.Count > 0)
124+
{
125+
waiters.Dequeue().TrySetResult(new Lock(value));
126+
}
127+
}
128+
129+
/// <summary>
130+
/// Release the lock and allow next waiter acquire it
131+
/// </summary>
132+
private void Reset()
133+
{
134+
TaskCompletionSource<Lock> nextWaiter = UnlockOrGetNextWaiter();
135+
while (nextWaiter != default && !nextWaiter.TrySetResult(new Lock(this)))
136+
{
137+
nextWaiter = UnlockOrGetNextWaiter();
138+
}
139+
}
140+
141+
private TaskCompletionSource<Lock> UnlockOrGetNextWaiter()
142+
{
143+
lock (_syncObj)
144+
{
145+
if (!_isLocked)
146+
{
147+
return default;
148+
}
149+
150+
if (_waiters == default)
151+
{
152+
_isLocked = false;
153+
return default;
154+
}
155+
156+
while (_waiters.Count > 0)
157+
{
158+
var nextWaiter = _waiters.Dequeue();
159+
if (!nextWaiter.Task.IsCompleted)
160+
{
161+
// Return the waiter only if it wasn't canceled already
162+
return nextWaiter;
163+
}
164+
}
165+
166+
_isLocked = false;
167+
return default;
168+
}
169+
}
170+
171+
public readonly struct Lock : IDisposable
172+
{
173+
private readonly AsyncLockWithValue<T> _owner;
174+
public bool HasValue => _owner == default;
175+
public T Value { get; }
176+
177+
public Lock(T value)
178+
{
179+
_owner = default;
180+
Value = value;
181+
}
182+
183+
public Lock(AsyncLockWithValue<T> owner)
184+
{
185+
_owner = owner;
186+
Value = default;
187+
}
188+
189+
public void SetValue(T value) => _owner.SetValue(value);
190+
191+
public void Dispose() => _owner?.Reset();
192+
}
193+
}
194+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
//
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.IO;
19+
20+
namespace Microsoft.Azure.PowerShell.Authenticators.Identity
21+
{
22+
internal class Constants
23+
{
24+
public const string OrganizationsTenantId = "organizations";
25+
26+
public const string AdfsTenantId = "adfs";
27+
28+
// TODO: Currently this is piggybacking off the Azure CLI client ID, but needs to be switched once the Developer Sign On application is available
29+
public const string DeveloperSignOnClientId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46";
30+
31+
public static string SharedTokenCacheFilePath { get { return Path.Combine(DefaultMsalTokenCacheDirectory, DefaultMsalTokenCacheName); } }
32+
33+
public const int SharedTokenCacheAccessRetryCount = 100;
34+
35+
public static readonly TimeSpan SharedTokenCacheAccessRetryDelay = TimeSpan.FromMilliseconds(600);
36+
37+
public const string DefaultRedirectUrl = "http://localhost";
38+
39+
public static readonly string DefaultMsalTokenCacheDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ".IdentityService");
40+
41+
public const string DefaultMsalTokenCacheKeychainService = "Microsoft.Developer.IdentityService";
42+
43+
public const string DefaultMsalTokenCacheKeychainAccount = "MSALCache";
44+
45+
public const string DefaultMsalTokenCacheKeyringLabel = "MSALCache";
46+
47+
public const string DefaultMsalTokenCacheKeyringSchema = "msal.cache";
48+
49+
public const string DefaultMsalTokenCacheKeyringCollection = "default";
50+
51+
public static readonly KeyValuePair<string, string> DefaultMsaltokenCacheKeyringAttribute1 = new KeyValuePair<string, string>("MsalClientID", "Microsoft.Developer.IdentityService");
52+
53+
public static readonly KeyValuePair<string, string> DefaultMsaltokenCacheKeyringAttribute2 = new KeyValuePair<string, string>("Microsoft.Developer.IdentityService", "1.0.0.0");
54+
55+
public const string DefaultMsalTokenCacheName = "msal.cache";
56+
}
57+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// ----------------------------------------------------------------------------------
2+
//
3+
// Copyright Microsoft Corporation
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
// ----------------------------------------------------------------------------------
14+
//
15+
16+
using Azure.Core;
17+
using Azure.Core.Pipeline;
18+
using Azure.Identity;
19+
using System;
20+
21+
namespace Microsoft.Azure.PowerShell.Authenticators.Identity
22+
{
23+
internal class CredentialPipeline
24+
{
25+
private static readonly Lazy<CredentialPipeline> s_singleton = new Lazy<CredentialPipeline>(() => new CredentialPipeline(new TokenCredentialOptions()));
26+
27+
private CredentialPipeline(TokenCredentialOptions options)
28+
{
29+
AuthorityHost = options.AuthorityHost;
30+
31+
HttpPipeline = HttpPipelineBuilder.Build(options, Array.Empty<HttpPipelinePolicy>(), Array.Empty<HttpPipelinePolicy>(), new CredentialResponseClassifier());
32+
}
33+
34+
public CredentialPipeline(Uri authorityHost, HttpPipeline httpPipeline)
35+
{
36+
AuthorityHost = authorityHost;
37+
38+
HttpPipeline = httpPipeline;
39+
}
40+
41+
public static CredentialPipeline GetInstance(TokenCredentialOptions options)
42+
{
43+
return options is null ? s_singleton.Value : new CredentialPipeline(options);
44+
}
45+
46+
public Uri AuthorityHost { get; }
47+
48+
public HttpPipeline HttpPipeline { get; }
49+
50+
private class CredentialResponseClassifier : ResponseClassifier
51+
{
52+
public override bool IsRetriableResponse(HttpMessage message)
53+
{
54+
return base.IsRetriableResponse(message) || message.Response.Status == 404;
55+
}
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)