1+ using System . Collections . Concurrent ;
2+ using System . Net . Http . Json ;
13using System . Security . Cryptography ;
24using System . Text ;
3- using System . Text . Json ;
45using System . Text . Json . Serialization ;
56using MaIN . Domain . Configuration . Vertex ;
67
78namespace MaIN . Services . Services . LLMService . Auth ;
89
9- internal sealed class GoogleServiceAccountTokenProvider
10+ internal sealed class GoogleServiceAccountTokenProvider : IDisposable
1011{
1112 private const string Scope = "https://www.googleapis.com/auth/cloud-platform" ;
1213 private const int TokenLifetimeSeconds = 3600 ;
@@ -15,9 +16,8 @@ internal sealed class GoogleServiceAccountTokenProvider
1516 private readonly GoogleServiceAccountConfig _config ;
1617 private readonly RSA _rsa ;
1718
18- // Static cache shared across all VertexService instances (keyed by ClientEmail)
19- private static readonly System . Collections . Concurrent . ConcurrentDictionary < string , CachedToken > _tokenCache = new ( ) ;
20- private static readonly SemaphoreSlim _refreshLock = new ( 1 , 1 ) ;
19+ private static readonly ConcurrentDictionary < string , CachedToken > TokenCache = new ( ) ;
20+ private static readonly ConcurrentDictionary < string , SemaphoreSlim > RefreshLocks = new ( ) ;
2121
2222 public GoogleServiceAccountTokenProvider ( GoogleServiceAccountConfig config )
2323 {
@@ -28,45 +28,46 @@ public GoogleServiceAccountTokenProvider(GoogleServiceAccountConfig config)
2828
2929 public async Task < string > GetAccessTokenAsync ( HttpClient httpClient )
3030 {
31- if ( _tokenCache . TryGetValue ( _config . ClientEmail , out var cached ) && DateTime . UtcNow < cached . Expiry )
31+ var email = _config . ClientEmail ;
32+
33+ if ( TokenCache . TryGetValue ( email , out var cached ) && ! cached . IsExpired )
3234 return cached . Token ;
3335
34- await _refreshLock . WaitAsync ( ) ;
36+ var refreshLock = RefreshLocks . GetOrAdd ( email , _ => new SemaphoreSlim ( 1 , 1 ) ) ;
37+ await refreshLock . WaitAsync ( ) ;
3538 try
3639 {
3740 // Double-check after acquiring lock
38- if ( _tokenCache . TryGetValue ( _config . ClientEmail , out cached ) && DateTime . UtcNow < cached . Expiry )
41+ if ( TokenCache . TryGetValue ( email , out cached ) && ! cached . IsExpired )
3942 return cached . Token ;
4043
4144 var jwt = BuildSignedJwt ( ) ;
4245 var token = await ExchangeJwtForTokenAsync ( httpClient , jwt ) ;
4346
4447 var accessToken = token . AccessToken
45- ?? throw new InvalidOperationException ( "Token response missing access_token." ) ;
48+ ?? throw new InvalidOperationException ( "Vertex AI token response missing access_token." ) ;
4649 var expiry = DateTime . UtcNow . AddSeconds ( token . ExpiresIn ) . AddMinutes ( - RefreshBufferMinutes ) ;
4750
48- _tokenCache [ _config . ClientEmail ] = new CachedToken ( accessToken , expiry ) ;
51+ TokenCache [ email ] = new CachedToken ( accessToken , expiry ) ;
4952 return accessToken ;
5053 }
5154 finally
5255 {
53- _refreshLock . Release ( ) ;
56+ refreshLock . Release ( ) ;
5457 }
5558 }
5659
57- private sealed record CachedToken ( string Token , DateTime Expiry ) ;
58-
5960 private string BuildSignedJwt ( )
6061 {
6162 var now = DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) ;
6263
63- var header = Base64UrlEncode ( JsonSerializer . SerializeToUtf8Bytes ( new
64+ var header = Base64UrlEncode ( System . Text . Json . JsonSerializer . SerializeToUtf8Bytes ( new
6465 {
6566 alg = "RS256" ,
6667 typ = "JWT"
6768 } ) ) ;
6869
69- var payload = Base64UrlEncode ( JsonSerializer . SerializeToUtf8Bytes ( new
70+ var payload = Base64UrlEncode ( System . Text . Json . JsonSerializer . SerializeToUtf8Bytes ( new
7071 {
7172 iss = _config . ClientEmail ,
7273 scope = Scope ,
@@ -81,15 +82,15 @@ private string BuildSignedJwt()
8182 return $ "{ header } .{ payload } .{ Base64UrlEncode ( signature ) } ";
8283 }
8384
84- private async Task < TokenResponse > ExchangeJwtForTokenAsync ( HttpClient httpClient , string jwt )
85+ private static async Task < TokenResponse > ExchangeJwtForTokenAsync ( HttpClient httpClient , string jwt )
8586 {
86- var content = new FormUrlEncodedContent ( new Dictionary < string , string >
87+ using var content = new FormUrlEncodedContent ( new Dictionary < string , string >
8788 {
8889 [ "grant_type" ] = "urn:ietf:params:oauth:grant-type:jwt-bearer" ,
8990 [ "assertion" ] = jwt
9091 } ) ;
9192
92- using var response = await httpClient . PostAsync ( _config . TokenUri , content ) ;
93+ using var response = await httpClient . PostAsync ( "https://oauth2.googleapis.com/token" , content ) ;
9394
9495 if ( ! response . IsSuccessStatusCode )
9596 {
@@ -98,14 +99,20 @@ private async Task<TokenResponse> ExchangeJwtForTokenAsync(HttpClient httpClient
9899 $ "Vertex AI token exchange failed ({ response . StatusCode } ): { error } ") ;
99100 }
100101
101- var json = await response . Content . ReadAsStringAsync ( ) ;
102- return JsonSerializer . Deserialize < TokenResponse > ( json )
102+ return await response . Content . ReadFromJsonAsync < TokenResponse > ( )
103103 ?? throw new InvalidOperationException ( "Failed to parse Vertex AI token response." ) ;
104104 }
105105
106+ public void Dispose ( ) => _rsa . Dispose ( ) ;
107+
106108 private static string Base64UrlEncode ( byte [ ] data )
107109 => Convert . ToBase64String ( data ) . TrimEnd ( '=' ) . Replace ( '+' , '-' ) . Replace ( '/' , '_' ) ;
108110
111+ private sealed record CachedToken ( string Token , DateTime Expiry )
112+ {
113+ public bool IsExpired => DateTime . UtcNow >= Expiry ;
114+ }
115+
109116 private sealed class TokenResponse
110117 {
111118 [ JsonPropertyName ( "access_token" ) ] public string ? AccessToken { get ; set ; }
0 commit comments