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 futures:: channel:: oneshot;
12+ use std:: {
13+ fs, str,
14+ sync:: Arc ,
15+ thread,
16+ time:: { Duration , Instant } ,
17+ } ;
1118use time:: OffsetDateTime ;
1219
1320const AZURE_TENANT_ID_ENV_KEY : & str = "AZURE_TENANT_ID" ;
@@ -19,14 +26,13 @@ const AZURE_FEDERATED_TOKEN: &str = "AZURE_FEDERATED_TOKEN";
1926///
2027/// More information on how to configure a client secret can be found here:
2128/// <https://learn.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-credentials-to-your-web-application>
22-
2329#[ derive( Debug ) ]
2430pub struct WorkloadIdentityCredential {
2531 http_client : Arc < dyn HttpClient > ,
2632 authority_host : Url ,
2733 tenant_id : String ,
2834 client_id : String ,
29- token : Secret ,
35+ token : Token ,
3036 cache : TokenCache ,
3137}
3238
@@ -47,7 +53,7 @@ impl WorkloadIdentityCredential {
4753 authority_host,
4854 tenant_id,
4955 client_id,
50- token : token. into ( ) ,
56+ token : Token :: Value ( token. into ( ) ) ,
5157 cache : TokenCache :: new ( ) ,
5258 } ) )
5359 }
@@ -100,22 +106,14 @@ impl WorkloadIdentityCredential {
100106 . var ( AZURE_FEDERATED_TOKEN_FILE )
101107 . map_kind ( ErrorKind :: Credential )
102108 {
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 (
109+ return Ok ( Arc :: new ( Self {
113110 http_client,
114111 authority_host,
115112 tenant_id,
116113 client_id,
117- token,
118- ) ;
114+ token : Token :: with_file ( token_file. as_ref ( ) ) ?,
115+ cache : TokenCache :: new ( ) ,
116+ } ) ) ;
119117 }
120118
121119 Err ( Error :: with_message ( ErrorKind :: Credential , || {
@@ -124,10 +122,11 @@ impl WorkloadIdentityCredential {
124122 }
125123
126124 async fn get_token ( & self , scopes : & [ & str ] ) -> azure_core:: Result < AccessToken > {
125+ let token = self . token . secret ( ) . await ?;
127126 let res: AccessToken = federated_credentials_flow:: authorize (
128127 self . http_client . clone ( ) ,
129128 & self . client_id ,
130- self . token . secret ( ) ,
129+ & token,
131130 scopes,
132131 & self . tenant_id ,
133132 & self . authority_host ,
@@ -155,3 +154,71 @@ impl TokenCredential for WorkloadIdentityCredential {
155154 self . cache . clear ( ) . await
156155 }
157156}
157+
158+ #[ derive( Debug ) ]
159+ enum Token {
160+ Value ( Secret ) ,
161+ File {
162+ path : String ,
163+ cache : Arc < RwLock < FileCache > > ,
164+ } ,
165+ }
166+
167+ #[ derive( Debug ) ]
168+ struct FileCache {
169+ token : Secret ,
170+ last_read : Instant ,
171+ }
172+
173+ impl Token {
174+ fn with_file ( path : & str ) -> azure_core:: Result < Self > {
175+ let last_read = Instant :: now ( ) ;
176+ let token = std:: fs:: read_to_string ( path) . with_context ( ErrorKind :: Credential , || {
177+ format ! ( "failed to read federated token from file {}" , path)
178+ } ) ?;
179+
180+ Ok ( Self :: File {
181+ path : path. into ( ) ,
182+ cache : Arc :: new ( RwLock :: new ( FileCache {
183+ token : Secret :: new ( token) ,
184+ last_read,
185+ } ) ) ,
186+ } )
187+ }
188+
189+ async fn secret ( & self ) -> azure_core:: Result < String > {
190+ match self {
191+ Self :: Value ( secret) => Ok ( secret. secret ( ) . into ( ) ) ,
192+ Self :: File { path, cache } => {
193+ const TIMEOUT : Duration = Duration :: from_secs ( 600 ) ;
194+
195+ let now = Instant :: now ( ) ;
196+ let cache = cache. upgradable_read ( ) . await ;
197+ if now - cache. last_read > TIMEOUT {
198+ // TODO: https://github.com/Azure/azure-sdk-for-rust/issues/2002
199+ let path = path. clone ( ) ;
200+ let ( tx, rx) = oneshot:: channel ( ) ;
201+ thread:: spawn ( move || {
202+ let token = fs:: read_to_string ( & path)
203+ . with_context ( ErrorKind :: Credential , || {
204+ format ! ( "failed to read federated token from file {}" , & path)
205+ } ) ;
206+ tx. send ( token)
207+ } ) ;
208+
209+ let mut write_cache = RwLockUpgradableReadGuard :: upgrade ( cache) . await ;
210+ let token = rx. await . map_err ( |err| {
211+ azure_core:: Error :: full ( ErrorKind :: Io , err, "canceled reading certificate" )
212+ } ) ??;
213+
214+ write_cache. token = Secret :: new ( token) ;
215+ write_cache. last_read = now;
216+
217+ return Ok ( write_cache. token . secret ( ) . into ( ) ) ;
218+ }
219+
220+ Ok ( cache. token . secret ( ) . into ( ) )
221+ }
222+ }
223+ }
224+ }
0 commit comments