22// Licensed under the MIT License.
33
44use crate :: { credentials:: cache:: TokenCache , federated_credentials_flow, TokenCredentialOptions } ;
5+ use async_lock:: { RwLock , RwLockUpgradableReadGuard } ;
56use azure_core:: {
67 credentials:: { AccessToken , Secret , TokenCredential } ,
78 error:: { ErrorKind , ResultExt } ,
89 Error , HttpClient , Url ,
910} ;
10- use std:: { str, sync:: Arc , time:: Duration } ;
11+ use std:: {
12+ str,
13+ sync:: Arc ,
14+ time:: { Duration , Instant } ,
15+ } ;
1116use time:: OffsetDateTime ;
1217
1318const AZURE_TENANT_ID_ENV_KEY : & str = "AZURE_TENANT_ID" ;
@@ -19,14 +24,13 @@ const AZURE_FEDERATED_TOKEN: &str = "AZURE_FEDERATED_TOKEN";
1924///
2025/// More information on how to configure a client secret can be found here:
2126/// <https://learn.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-credentials-to-your-web-application>
22-
2327#[ derive( Debug ) ]
2428pub struct WorkloadIdentityCredential {
2529 http_client : Arc < dyn HttpClient > ,
2630 authority_host : Url ,
2731 tenant_id : String ,
2832 client_id : String ,
29- token : Secret ,
33+ token : Token ,
3034 cache : TokenCache ,
3135}
3236
@@ -47,7 +51,7 @@ impl WorkloadIdentityCredential {
4751 authority_host,
4852 tenant_id,
4953 client_id,
50- token : token. into ( ) ,
54+ token : Token :: Value ( token. into ( ) ) ,
5155 cache : TokenCache :: new ( ) ,
5256 } ) )
5357 }
@@ -100,22 +104,14 @@ impl WorkloadIdentityCredential {
100104 . var ( AZURE_FEDERATED_TOKEN_FILE )
101105 . map_kind ( ErrorKind :: Credential )
102106 {
103- let token = std:: fs:: read_to_string ( token_file. clone ( ) ) . with_context (
104- ErrorKind :: Credential ,
105- || {
106- format ! (
107- "failed to read federated token from file {}" ,
108- token_file. as_str( )
109- )
110- } ,
111- ) ?;
112- return WorkloadIdentityCredential :: new (
107+ return Ok ( Arc :: new ( Self {
113108 http_client,
114109 authority_host,
115110 tenant_id,
116111 client_id,
117- token,
118- ) ;
112+ token : Token :: with_file ( token_file. as_ref ( ) ) ?,
113+ cache : TokenCache :: new ( ) ,
114+ } ) ) ;
119115 }
120116
121117 Err ( Error :: with_message ( ErrorKind :: Credential , || {
@@ -124,10 +120,11 @@ impl WorkloadIdentityCredential {
124120 }
125121
126122 async fn get_token ( & self , scopes : & [ & str ] ) -> azure_core:: Result < AccessToken > {
123+ let token = self . token . secret ( ) . await ?;
127124 let res: AccessToken = federated_credentials_flow:: authorize (
128125 self . http_client . clone ( ) ,
129126 & self . client_id ,
130- self . token . secret ( ) ,
127+ & token,
131128 scopes,
132129 & self . tenant_id ,
133130 & self . authority_host ,
@@ -155,3 +152,60 @@ impl TokenCredential for WorkloadIdentityCredential {
155152 self . cache . clear ( ) . await
156153 }
157154}
155+
156+ #[ derive( Debug ) ]
157+ enum Token {
158+ Value ( Secret ) ,
159+ File {
160+ path : String ,
161+ cache : Arc < RwLock < FileCache > > ,
162+ } ,
163+ }
164+
165+ #[ derive( Debug ) ]
166+ struct FileCache {
167+ token : Secret ,
168+ last_read : Instant ,
169+ }
170+
171+ impl Token {
172+ fn with_file ( path : & str ) -> azure_core:: Result < Self > {
173+ let last_read = Instant :: now ( ) ;
174+ let token = std:: fs:: read_to_string ( path) . with_context ( ErrorKind :: Credential , || {
175+ format ! ( "failed to read federated token from file {}" , path)
176+ } ) ?;
177+
178+ Ok ( Self :: File {
179+ path : path. into ( ) ,
180+ cache : Arc :: new ( RwLock :: new ( FileCache {
181+ token : Secret :: new ( token) ,
182+ last_read,
183+ } ) ) ,
184+ } )
185+ }
186+
187+ async fn secret ( & self ) -> azure_core:: Result < String > {
188+ match self {
189+ Self :: Value ( secret) => Ok ( secret. secret ( ) . into ( ) ) ,
190+ Self :: File { path, cache } => {
191+ const TIMEOUT : Duration = Duration :: from_secs ( 600 ) ;
192+
193+ let now = Instant :: now ( ) ;
194+ let cache = cache. upgradable_read ( ) . await ;
195+ if now - cache. last_read > TIMEOUT {
196+ let token = std:: fs:: read_to_string ( path)
197+ . with_context ( ErrorKind :: Credential , || {
198+ format ! ( "failed to read federated token from file {}" , path)
199+ } ) ?;
200+ let mut write_cache = RwLockUpgradableReadGuard :: upgrade ( cache) . await ;
201+ write_cache. token = Secret :: new ( token) ;
202+ write_cache. last_read = now;
203+
204+ return Ok ( write_cache. token . secret ( ) . into ( ) ) ;
205+ }
206+
207+ Ok ( cache. token . secret ( ) . into ( ) )
208+ }
209+ }
210+ }
211+ }
0 commit comments