Skip to content

Commit f0ad01c

Browse files
feat: impl cached ICredentialsProvider (ydb-platform#293)
- **Breaking Change**: `Ydb.Sdk.Yc.Auth` version <= 0.1.0 is not compatible with newer versions. - Added `IAuthClient` to fetch auth token. - Added the `CachedCredentialsProvider` class, which streamlines token lifecycle management. - **Breaking Change**: Deleted `AnonymousProvider`. Now users don't need to do anything for anonymous authentication. Migration guide: ```c# var config = new DriverConfig(...); // Using AnonymousProvider if Credentials property is null ``` - **Breaking Change**: Deleted `StaticCredentialsProvider`. Users are now recommended to use the `User` and `Password` properties in `DriverConfig` for configuring authentication. Migration guide: ```c# var config = new DriverConfig(...) { User = "your_username", Password = "your_password" }; ```
1 parent 04c471f commit f0ad01c

23 files changed

+731
-357
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
1+
- **Breaking Change**: `Ydb.Sdk.Yc.Auth` version <= 0.1.0 is not compatible with newer versions.
2+
- Added `IAuthClient` to fetch auth token.
3+
- Added the `CachedCredentialsProvider` class, which streamlines token lifecycle management.
4+
- **Breaking Change**: Deleted `AnonymousProvider`. Now users don't need to do anything for anonymous authentication.
5+
Migration guide:
6+
```c#
7+
var config = new DriverConfig(...); // Using AnonymousProvider if Credentials property is null
8+
```
9+
- **Breaking Change**: Deleted `StaticCredentialsProvider`. Users are now recommended to use the `User` and `Password`
10+
properties in `DriverConfig` for configuring authentication. Migration guide:
11+
```c#
12+
var config = new DriverConfig(...)
13+
{
14+
User = "your_username",
15+
Password = "your_password"
16+
};
17+
```
18+
119
## v0.15.4
220

321
- Added `KeepAlivePingTimeout`, with a default value of 10 seconds.

examples/src/BasicExample/BasicExample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ private BasicExample(TableClient client, string database, string path)
1515
public static async Task Run(
1616
string endpoint,
1717
string database,
18-
ICredentialsProvider credentialsProvider,
18+
ICredentialsProvider? credentialsProvider,
1919
X509Certificate? customServerCertificate,
2020
string path,
2121
ILoggerFactory loggerFactory)

examples/src/Common/AuthUtils.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Ydb.Sdk.Examples;
99

1010
public static class AuthUtils
1111
{
12-
public static async Task<ICredentialsProvider> MakeCredentialsFromEnv(
12+
public static async Task<ICredentialsProvider?> MakeCredentialsFromEnv(
1313
bool fallbackAnonymous = false,
1414
ILoggerFactory? loggerFactory = null)
1515
{
@@ -26,7 +26,7 @@ public static async Task<ICredentialsProvider> MakeCredentialsFromEnv(
2626
var anonymousValue = Environment.GetEnvironmentVariable("YDB_ANONYMOUS_CREDENTIALS");
2727
if (anonymousValue != null && IsTrueValue(anonymousValue))
2828
{
29-
return new AnonymousProvider();
29+
return null;
3030
}
3131

3232
var metadataValue = Environment.GetEnvironmentVariable("YDB_METADATA_CREDENTIALS");
@@ -46,7 +46,7 @@ public static async Task<ICredentialsProvider> MakeCredentialsFromEnv(
4646

4747
if (fallbackAnonymous)
4848
{
49-
return new AnonymousProvider();
49+
return null;
5050
}
5151

5252
throw new InvalidOperationException("Failed to parse credentials from environmet, no valid options found.");

examples/src/QueryExample/QueryExample.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ private QueryExample(QueryClient client, string database, string path)
2020
public static async Task Run(
2121
string endpoint,
2222
string database,
23-
ICredentialsProvider credentialsProvider,
23+
ICredentialsProvider? credentialsProvider,
2424
X509Certificate? customServerCertificate,
2525
string path,
2626
ILoggerFactory loggerFactory)

src/Ydb.Sdk/src/Ado/YdbConnectionStringBuilder.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,12 @@ public override object this[string keyword]
220220

221221
internal Task<Driver> BuildDriver()
222222
{
223-
var credentialsProvider = CredentialsProvider ??
224-
(User != null ? new StaticCredentialsProvider(User, Password) : null);
225223
var cert = RootCertificate != null ? X509Certificate.CreateFromCertFile(RootCertificate) : null;
226224

227225
return Driver.CreateInitialized(new DriverConfig(
228226
endpoint: Endpoint,
229227
database: Database,
230-
credentials: credentialsProvider,
228+
credentials: CredentialsProvider,
231229
customServerCertificate: cert,
232230
customServerCertificates: ServerCertificates
233231
)
@@ -237,7 +235,9 @@ internal Task<Driver> BuildDriver()
237235
: TimeSpan.FromSeconds(KeepAlivePingDelay),
238236
KeepAlivePingTimeout = KeepAlivePingTimeout == 0
239237
? Timeout.InfiniteTimeSpan
240-
: TimeSpan.FromSeconds(KeepAlivePingTimeout)
238+
: TimeSpan.FromSeconds(KeepAlivePingTimeout),
239+
User = User,
240+
Password = Password
241241
}, LoggerFactory);
242242
}
243243

src/Ydb.Sdk/src/Auth/AnonymousProvider.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Logging.Abstractions;
3+
4+
namespace Ydb.Sdk.Auth;
5+
6+
public class CachedCredentialsProvider : ICredentialsProvider
7+
{
8+
private readonly IClock _clock;
9+
private readonly IAuthClient _authClient;
10+
11+
private ILogger<CachedCredentialsProvider> Logger { get; }
12+
13+
private volatile ITokenState _tokenState;
14+
15+
public CachedCredentialsProvider(
16+
IAuthClient authClient,
17+
ILoggerFactory? loggerFactory = null
18+
)
19+
{
20+
_clock = new SystemClock();
21+
_authClient = authClient;
22+
_tokenState = new SyncState(this);
23+
_tokenState.Init();
24+
25+
loggerFactory ??= new NullLoggerFactory();
26+
Logger = loggerFactory.CreateLogger<CachedCredentialsProvider>();
27+
}
28+
29+
internal CachedCredentialsProvider(IAuthClient authClient, IClock clock) : this(authClient)
30+
{
31+
_clock = clock;
32+
}
33+
34+
public async ValueTask<string> GetAuthInfoAsync() =>
35+
(await _tokenState.Validate(_clock.UtcNow)).TokenResponse.Token;
36+
37+
private Task<TokenResponse> FetchToken() => _authClient.FetchToken();
38+
39+
private ITokenState UpdateState(ITokenState current, ITokenState next)
40+
{
41+
if (Interlocked.CompareExchange(ref _tokenState, next, current) == current)
42+
{
43+
next.Init();
44+
}
45+
46+
return _tokenState;
47+
}
48+
49+
private interface ITokenState
50+
{
51+
TokenResponse TokenResponse { get; }
52+
53+
ValueTask<ITokenState> Validate(DateTime now);
54+
55+
void Init()
56+
{
57+
}
58+
}
59+
60+
private class ActiveState : ITokenState
61+
{
62+
private readonly CachedCredentialsProvider _cachedCredentialsProvider;
63+
64+
public ActiveState(TokenResponse tokenResponse, CachedCredentialsProvider cachedCredentialsProvider)
65+
{
66+
TokenResponse = tokenResponse;
67+
_cachedCredentialsProvider = cachedCredentialsProvider;
68+
}
69+
70+
public TokenResponse TokenResponse { get; }
71+
72+
public async ValueTask<ITokenState> Validate(DateTime now)
73+
{
74+
if (now < TokenResponse.ExpiredAt)
75+
{
76+
return now >= TokenResponse.RefreshAt
77+
? _cachedCredentialsProvider.UpdateState(
78+
this,
79+
new BackgroundState(TokenResponse, _cachedCredentialsProvider)
80+
)
81+
: this;
82+
}
83+
84+
_cachedCredentialsProvider.Logger.LogWarning(
85+
"Token has expired. ExpiredAt: {ExpiredAt}, CurrentTime: {CurrentTime}. " +
86+
"Switching to synchronous state to fetch a new token",
87+
TokenResponse.ExpiredAt, now
88+
);
89+
90+
return await _cachedCredentialsProvider
91+
.UpdateState(this, new SyncState(_cachedCredentialsProvider))
92+
.Validate(now);
93+
}
94+
}
95+
96+
private class SyncState : ITokenState
97+
{
98+
private readonly TaskCompletionSource<TokenResponse> _fetchTokenResponseTcs = new();
99+
100+
private readonly CachedCredentialsProvider _cachedCredentialsProvider;
101+
102+
public SyncState(CachedCredentialsProvider cachedCredentialsProvider)
103+
{
104+
_cachedCredentialsProvider = cachedCredentialsProvider;
105+
}
106+
107+
public TokenResponse TokenResponse =>
108+
throw new InvalidOperationException("Get token for unfinished sync state");
109+
110+
public async ValueTask<ITokenState> Validate(DateTime now)
111+
{
112+
try
113+
{
114+
var tokenResponse = await _fetchTokenResponseTcs.Task;
115+
116+
_cachedCredentialsProvider.Logger.LogDebug(
117+
"Successfully fetched token. ExpiredAt: {ExpiredAt}, RefreshAt: {RefreshAt}",
118+
tokenResponse.ExpiredAt, tokenResponse.RefreshAt
119+
);
120+
121+
return _cachedCredentialsProvider.UpdateState(this,
122+
new ActiveState(tokenResponse, _cachedCredentialsProvider));
123+
}
124+
catch (Exception e)
125+
{
126+
_cachedCredentialsProvider.Logger.LogCritical(e, "Error on authentication token update");
127+
128+
return _cachedCredentialsProvider.UpdateState(this, new ErrorState(e, _cachedCredentialsProvider));
129+
}
130+
}
131+
132+
public async void Init()
133+
{
134+
try
135+
{
136+
var tokenResponse = await _cachedCredentialsProvider.FetchToken();
137+
138+
_fetchTokenResponseTcs.SetResult(tokenResponse);
139+
}
140+
catch (Exception e)
141+
{
142+
_fetchTokenResponseTcs.SetException(e);
143+
}
144+
}
145+
}
146+
147+
private class BackgroundState : ITokenState
148+
{
149+
private readonly TaskCompletionSource<TokenResponse> _fetchTokenResponseTcs = new();
150+
151+
private readonly CachedCredentialsProvider _cachedCredentialsProvider;
152+
153+
public BackgroundState(TokenResponse tokenResponse,
154+
CachedCredentialsProvider cachedCredentialsProvider)
155+
{
156+
TokenResponse = tokenResponse;
157+
_cachedCredentialsProvider = cachedCredentialsProvider;
158+
}
159+
160+
public TokenResponse TokenResponse { get; }
161+
162+
public async ValueTask<ITokenState> Validate(DateTime now)
163+
{
164+
var fetchTokenTask = _fetchTokenResponseTcs.Task;
165+
166+
if (fetchTokenTask.IsCanceled || fetchTokenTask.IsFaulted)
167+
{
168+
_cachedCredentialsProvider.Logger.LogWarning(
169+
"Fetching token task failed. Status: {Status}, Retrying login...",
170+
fetchTokenTask.IsCanceled ? "Canceled" : "Faulted"
171+
);
172+
173+
return now >= TokenResponse.ExpiredAt
174+
? await _cachedCredentialsProvider
175+
.UpdateState(this, new SyncState(_cachedCredentialsProvider))
176+
.Validate(now)
177+
: _cachedCredentialsProvider
178+
.UpdateState(this, new BackgroundState(TokenResponse, _cachedCredentialsProvider));
179+
}
180+
181+
if (fetchTokenTask.IsCompleted)
182+
{
183+
return _cachedCredentialsProvider
184+
.UpdateState(this, new ActiveState(await fetchTokenTask, _cachedCredentialsProvider));
185+
}
186+
187+
if (now < TokenResponse.ExpiredAt)
188+
{
189+
return this;
190+
}
191+
192+
try
193+
{
194+
var tokenResponse = await fetchTokenTask;
195+
196+
_cachedCredentialsProvider.Logger.LogDebug(
197+
"Successfully fetched token. ExpiredAt: {ExpiredAt}, RefreshAt: {RefreshAt}",
198+
tokenResponse.ExpiredAt, tokenResponse.RefreshAt
199+
);
200+
201+
return _cachedCredentialsProvider.UpdateState(this,
202+
new ActiveState(tokenResponse, _cachedCredentialsProvider));
203+
}
204+
catch (Exception e)
205+
{
206+
_cachedCredentialsProvider.Logger.LogCritical(e, "Error on authentication token update");
207+
208+
return _cachedCredentialsProvider.UpdateState(this, new ErrorState(e, _cachedCredentialsProvider));
209+
}
210+
}
211+
212+
public async void Init()
213+
{
214+
try
215+
{
216+
var tokenResponse = await _cachedCredentialsProvider.FetchToken();
217+
218+
_fetchTokenResponseTcs.SetResult(tokenResponse);
219+
}
220+
catch (Exception e)
221+
{
222+
_fetchTokenResponseTcs.SetException(e);
223+
}
224+
}
225+
}
226+
227+
private class ErrorState : ITokenState
228+
{
229+
private readonly Exception _exception;
230+
private readonly CachedCredentialsProvider _managerCredentialsProvider;
231+
232+
public ErrorState(Exception exception, CachedCredentialsProvider managerCredentialsProvider)
233+
{
234+
_exception = exception;
235+
_managerCredentialsProvider = managerCredentialsProvider;
236+
}
237+
238+
public TokenResponse TokenResponse => throw _exception;
239+
240+
public ValueTask<ITokenState> Validate(DateTime now) => _managerCredentialsProvider
241+
.UpdateState(this, new SyncState(_managerCredentialsProvider))
242+
.Validate(now);
243+
}
244+
}
245+
246+
public class TokenResponse
247+
{
248+
private const double RefreshInterval = 0.5;
249+
250+
public TokenResponse(string token, DateTime expiredAt, DateTime? refreshAt = null)
251+
{
252+
var now = DateTime.UtcNow;
253+
254+
Token = token;
255+
ExpiredAt = expiredAt.ToUniversalTime();
256+
RefreshAt = refreshAt?.ToUniversalTime() ?? now + (ExpiredAt - now) * RefreshInterval;
257+
}
258+
259+
public string Token { get; }
260+
public DateTime ExpiredAt { get; }
261+
public DateTime RefreshAt { get; }
262+
}
263+
264+
public interface IClock
265+
{
266+
DateTime UtcNow { get; }
267+
}
268+
269+
internal class SystemClock : IClock
270+
{
271+
public DateTime UtcNow => DateTime.UtcNow;
272+
}
Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
using Ydb.Sdk.Services.Auth;
2-
3-
namespace Ydb.Sdk.Auth;
1+
namespace Ydb.Sdk.Auth;
42

53
public interface ICredentialsProvider
64
{
7-
// For removal in 1.*
8-
string? GetAuthInfo();
9-
10-
ValueTask<string?> GetAuthInfoAsync() => ValueTask.FromResult(GetAuthInfo());
5+
ValueTask<string> GetAuthInfoAsync();
6+
}
117

12-
Task ProvideAuthClient(AuthClient authClient) => Task.CompletedTask;
8+
public interface IAuthClient
9+
{
10+
Task<TokenResponse> FetchToken();
1311
}

0 commit comments

Comments
 (0)