azure_jwt_async/
lib.rs

1//! # Authenticates Azure JWT tokens.
2//!
3//! This library will fetch public keys from Microsoft and use those keys to validate the
4//! authenticity of a token you provide. It defaults to validating and mapping Azure Id tokens for
5//! you out of the box, but should work with other tokens as well if you use a custom validator.
6//!
7//!
8//! # Default validation
9//!
10//! **There are mainly six conditions a well formed token will need to meet to be validated:**
11//! 1. That the token is issued by Azure and is not tampered with
12//! 2. That this token is issued for use in your application
13//! 3. That the token is not expired
14//! 4. That the token is not used before it's valid
15//! 5. That the token is not issued in the future
16//! 6. That the algorithm in the token header is the same as we use*
17//!
18//! * Note that we do NOT use the token header to set the algorithm for us, look [at this article
19//! for more information on why that would be bad](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/)
20//!
21//! The validation will `Error` on a failed validation providing more granularity for library users
22//! to find out why the token was rejected.
23//!
24//! If the token is invalid it will return an Error instead of a boolean. The main reason for this
25//! is easier logging of what type of test it failed.
26//!
27//! You also have a `validate_custom` mathod which gives you full control over the mapping of the token
28//! fields and more control over the validation.
29//!
30//! # Security
31//!
32//! You will need a private app_id created by Azure for your application to be able to verify that
33//! the token is created for your application (and not anyone with a valid Azure token can log in)
34//! and you will need to authenticate that the user has the right access to your system.
35//!
36//! For more information, see this artice: <https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens>
37//!
38//! ```rust, no_run, ignore
39//!  use azure_jwt_async::*;
40//! # use jsonwebtoken as jwt;
41//! # use tokio_test::block_on;
42//! #
43//! # const PUBLIC_KEY_N: &str = "AOx0GOQcSt5AZu02nlGWUuXXppxeV9Cu_9LcgpVBg_WQb-5DBHZpqs8AMek5u5iI4hkHCcOyMbQrBsDIVa9xxZxR2kq_8GtERsnd6NClQimspxT1WVgX5_WCAd5rk__Iv0GocP2c_1CcdT8is2OZHeWQySyQNSgyJYg6Up7kFtYabiCyU5q9tTIHQPXiwY53IGsNvSkqbk-OsdWPT3E4dqp3vNraMqXhuSZ-52kLCHqwPgAsbztfFJxSAEBcp-TS3uNuHeSJwNWjvDKTPy2oMacNpbsKb2gZgzubR6hTjvupRjaQ9SHhXyL9lmSZOpCzz2XJSVRopKUUtB-VGA0qVlk";
44//! # const PUBLIC_KEY_E: &str = "AQAB";
45//! # const PRIVATE_KEY_TEST: &str = "MIIEowIBAAKCAQEA7HQY5BxK3kBm7TaeUZZS5demnF5X0K7/0tyClUGD9ZBv7kMEdmmqzwAx6Tm7mIjiGQcJw7IxtCsGwMhVr3HFnFHaSr/wa0RGyd3o0KVCKaynFPVZWBfn9YIB3muT/8i/Qahw/Zz/UJx1PyKzY5kd5ZDJLJA1KDIliDpSnuQW1hpuILJTmr21MgdA9eLBjncgaw29KSpuT46x1Y9PcTh2qne82toypeG5Jn7naQsIerA+ACxvO18UnFIAQFyn5NLe424d5InA1aO8MpM/Lagxpw2luwpvaBmDO5tHqFOO+6lGNpD1IeFfIv2WZJk6kLPPZclJVGikpRS0H5UYDSpWWQIDAQABAoIBAQC982Yrmi7q7IHC/qWglUpzKhLGe2PAWVVaZ5rfnIoNs8K3fU8QcUKumFGAMsjpeM1pnaXSeExFmGsMY+Ox1YwSUA81DYxuH6Ned86YDqpgIDr5M0Ba7JmDOLWXoIR8byB19oMOuhjBAW+PEKlb0Z2a1f1Gt3J8oAxWq8PDsShHRdjyesVS36QZpIgjZskcNws/zqqqDRrLWuLmAvk6E+tMD6sqo9xpzEqHF7rmwtt5yAtM1oZdWoEg2O+wZH5DBX2GhLlNZi/8sIiFMo+jouQn+l6Qc4G65vnnoZ+yEuf9fTJPnTHBFMViUcmTPsdbD4eLfrRXwAE9GYrvR/RVusABAoGBAPgsQ4kAChpzU2aP21NQV1XTBW+eoHVbcJoYuOlmwB6x5o8lDUz/EQVVYZavfNY1AjhEkfltCDjm1GHyWofrtGKTy7DHSZwPw5CxuqDtaiC6PMpFEu+Oxa09s7IZxpgInlrhY5JskOkH495BQ0xIU8UDxuP6sdtVNeQmWGjKG7kBAoGBAPPpNid4QEV4XleyAXT/JQGugdpa7TirWOEATNo10YPPqz7GphRhucT0ipNKMi/0XKh3U0IC7XxjUvtE2LP9TVGAcV/Wzi4EYp1fziFuF9QcUds2tJ60SpfgIQrmVcF1zHxn4/mSABoIyFxZSb4Tq9f+KXPAO5/l0NjgrVwk6gVZAoGAbMVZxE4UH4u0XhtnEZkA7kjS9R0dTtKJA8EaKpIyWkG2v76JmdmhaCkH4LeBi5EoK+lB4YR8OhRRuawzKaeRJDOK7ywpgxEVsfFzzty/yyBVTIIBzqVQ1qFYhRLvC+ubHFH1BlQ3HyuqH9uS13hL3unM3lceZPdv61MzJJqQlAECgYAWg0MFV5sPDnIexAZQZzBiPFot7lCQ93fHpMBzL557/RIARFOV9AMyg6O6vpFtTa+zuPfNUvnajkxddthNnKajTCiqwOfc5Xi4r9wVx9SZNlfz1NPNBjUQWZaTK/lkVtwd63TmVyx9OqxLoc4lpikpUYM/9NFMC+k/61T0+U9EWQKBgCdZV3yxwkz3pi6/E40EXfUsj8HQG/UtFJGeUNQiysBrxTmtmwLyvJeCGruG96j1JcehpbcWKV+ObyMQuk65dM94uM7Wa+2NCA/MvorVcU7wdPbq7/eczZU4xMd+OWT6JsInVM1ASh1mcn+Q0/Z3WqxxetCQLqaMs+FATn059dGf";
46//! #
47//! # fn test_token_header() -> String {
48//! #     format!(
49//! #         r#"{{
50//! #                 "typ": "JWT",
51//! #                 "alg": "RS256",
52//! #                 "kid": "i6lGk3FZzxRcUb2C3nEQ7syHJlY"
53//! #             }}"#
54//! #     )
55//! # }
56//! #
57//! # fn test_token_claims() -> String {
58//! #     format!(
59//! #             r#"{{
60//! #                 "aud": "6e74172b-be56-4843-9ff4-e66a39bb12e3",
61//! #                 "iss": "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0",
62//! #                 "iat": {},
63//! #                 "nbf": {},
64//! #                 "exp": {},
65//! #                 "aio": "AXQAi/8IAAAAtAaZLo3ChMif6KOnttRB7eBq4/DccQzjcJGxPYy/C3jDaNGxXd6wNIIVGRghNRnwJ1lOcAnNZcjvkoyrFxCttv33140RioOFJ4bCCGVuoCag1uOTT22222gHwLPYQ/uf79QX+0KIijdrmp69RctzmQ==",
66//! #                 "azp": "6e74172b-be56-4843-9ff4-e66a39bb12e3",
67//! #                 "name": "Abe Lincoln",
68//! #                 "azpacr": "0",
69//! #                 "appidacr": 1,
70//! #                 "oid": "690222be-ff1a-4d56-abd1-7e4f7d38e474",
71//! #                 "preferred_username": "abeli@microsoft.com",
72//! #                 "rh": "I",
73//! #                 "scp": "access_as_user",
74//! #                 "sub": "HKZpfaHyWadeOouYlitjrI-KffTm222X5rrV3xDqfKQ",
75//! #                 "tid": "72f988bf-86f1-41af-91ab-2d7cd011db47",
76//! #                 "uti": "fqiBqXLPj0eQa82S-IYFAA",
77//! #                 "ver": "2.0"
78//! #             }}"#,
79//! #         chrono::Utc::now().timestamp() - 1000,
80//! #         chrono::Utc::now().timestamp() - 2000,
81//! #         chrono::Utc::now().timestamp() + 1000)
82//! # }
83//! #
84//! # use simple_base64::{engine::general_purpose, Engine};
85//! #
86//! # async fn generate_test_token() -> String {
87//! #     let private_key = jwt::EncodingKey::from_base64_secret(PRIVATE_KEY_TEST).unwrap();
88//! #     let test_token_playload = test_token_claims();
89//! #     let test_token_header = test_token_header();
90//! #     let test_token = [
91//! #         general_purpose::URL_SAFE.encode(&test_token_header),
92//! #         general_purpose::URL_SAFE.encode(&test_token_playload),
93//! #     ]
94//! #     .join(".");
95//! #     let signature = jwt::crypto::sign(&test_token, &private_key, jwt::Algorithm::RS256).expect("Signed");
96//! #     let public_key = Jwk {
97//! #         kid: "".to_string(),
98//! #         n: PUBLIC_KEY_N.to_string(),
99//! #         e: PUBLIC_KEY_E.to_string(),
100//! #     };
101//! #     let public_key = jwt::DecodingKey::from_rsa_components(&public_key.n, &public_key.e);
102//! #     let complete_token = format!("{}.{}", test_token, signature);
103//! #     let verified = jwt::crypto::verify(&signature, &test_token, &public_key, jwt::Algorithm::RS256)
104//! #         .expect("verified");
105//! #     assert!(verified);
106//! #     complete_token
107//! # }
108//! #
109//! #
110//! # tokio_test::block_on(async {
111//! # let token = generate_test_token().await;
112//! # let n: &str = "AOx0GOQcSt5AZu02nlGWUuXXppxeV9Cu_9LcgpVBg_WQb-5DBHZpqs8AMek5u5iI4hkHCcOyMbQrBsDIVa9xxZxR2kq_8GtERsnd6NClQimspxT1WVgX5_WCAd5rk__Iv0GocP2c_1CcdT8is2OZHeWQySyQNSgyJYg6Up7kFtYabiCyU5q9tTIHQPXiwY53IGsNvSkqbk-OsdWPT3E4dqp3vNraMqXhuSZ-52kLCHqwPgAsbztfFJxSAEBcp-TS3uNuHeSJwNWjvDKTPy2oMacNpbsKb2gZgzubR6hTjvupRjaQ9SHhXyL9lmSZOpCzz2XJSVRopKUUtB-VGA0qVlk";
113//! # let e: &str = "AQAB";
114//! #
115//! # let key = Jwk {
116//! #         kid: "i6lGk3FZzxRcUb2C3nEQ7syHJlY".to_string(),
117//! #         n: n.to_string(),
118//! #         e: e.to_string(),
119//! #     };
120//!  
121//!  let mut az_auth = AzureAuth::new("6e74172b-be56-4843-9ff4-e66a39bb12e3").await.unwrap();
122//!  
123//!  let decoded_token = az_auth.validate_token(&token).await.expect("validated");
124//! #   assert_eq!(decoded_token.claims.preferred_username, Some("abeli@microsoft.com".to_string()));
125//! # });
126//! #
127//!  ```
128//!
129//! # Example in webserver
130//!
131//! ```rust, no_run, ignore
132//!  struct AppState {
133//!     azure_auth: auth::AzureAuth,
134//!  }
135//!
136//!  pub async fn start_web_server(port: &str) -> Result<(), Error> {
137//!
138//!     // since this calls windows api, wrap in Arc<Mutex<_>> and share the validator
139//!     let app_state = Arc::new(Mutex::new(AppState {
140//!         azure_auth: auth::AzureAuth::new("32166c25-5e31-4cfc-a29b-04d0dfdb019a").await.unwrap(),
141//!     }));
142//!     println!("Starting web server on: http://localhost:8000");
143//!
144//!     server::new(move || app(app_state.clone())).bind(port)?.run();
145//!
146//!     Ok(())
147//!  }
148//! ```
149
150use async_recursion::async_recursion;
151use chrono::{Duration, Local, NaiveDateTime};
152use jsonwebtoken as jwt;
153use jwt::DecodingKey;
154use reqwest::{self, Response};
155use serde::{Deserialize, Serialize};
156
157mod error;
158pub use error::AuthErr;
159use serde_aux::field_attributes::deserialize_number_from_string;
160
161const AZ_OPENID_URL: &str =
162    "https://login.microsoftonline.com/common/.well-known/openid-configuration";
163
164/// AzureAuth is the what you'll use to validate your token.
165///
166/// # Defaults
167///
168/// - Public key expiration: dafault set to 24h, use `set_expiration` to set a different expiration
169///   in hours.
170/// - Hashing algorithm: Sha256, you can't change this setting. Submit an issue in the github repo
171///   if this is important to you
172/// - Retry on no match. If no matching key is found and our keys are older than an hour, we
173///   refresh the keys and try once more. Limited to once in an hour. You can disable this by
174///   calling `set_no_retry()`.
175/// - The timestamps are given a 60s "leeway" to account for time skew between servers
176///
177/// # Errors
178///
179/// - If one of Microsofts enpoints for public keys are down
180/// - If the token can't be parsed as a valid Azure token
181/// - If the tokens fails it's authenticity test
182/// - If the token is invalid
183#[derive(Debug, Clone)]
184pub struct AzureAuth {
185    aud_to_val: String,
186    jwks_uri: String,
187    public_keys: Option<Vec<Jwk>>,
188    last_refresh: Option<NaiveDateTime>,
189    exp_hours: i64,
190    retry_counter: u32,
191    is_retry_enabled: bool,
192    is_offline: bool,
193}
194
195impl AzureAuth {
196    /// Creates a new dafault instance. This method will call the Microsoft apis to fetch the current keys
197    /// which can fail. The public keys are fetched since we need them to perform
198    /// verification. Please note that fetching the OpenID manifest and public keys are quite slow
199    /// since we call an external API in a blocking manner. Try keeping a single instance
200    /// alive instead of creating new ones for every validation. If you need to pass around an
201    /// instance of the object, creating a pool of instances at startup or wrapping a single
202    /// instance in a `Mutex` is better than creating many new instances.
203    ///
204    /// # Errors
205    ///
206    /// If there is a connection issue to the Microsoft APIs.
207    pub async fn new(aud: impl Into<String>) -> Result<Self, AuthErr> {
208        Ok(AzureAuth {
209            aud_to_val: aud.into(),
210            jwks_uri: AzureAuth::get_jwks_uri().await?,
211            public_keys: None,
212            last_refresh: None,
213            exp_hours: 24,
214            retry_counter: 0,
215            is_retry_enabled: true,
216            is_offline: false,
217        })
218    }
219
220    /// Does not call the Microsoft openid configuration endpoint or fetches the JWK set.
221    /// Use this if you want to handle updating the public keys yourself
222    pub fn new_offline(aud: impl Into<String>, public_keys: Vec<Jwk>) -> Result<Self, AuthErr> {
223        Ok(AzureAuth {
224            aud_to_val: aud.into(),
225            jwks_uri: String::new(),
226            public_keys: Some(public_keys),
227            last_refresh: Some(Local::now().naive_local()),
228            exp_hours: 24,
229            retry_counter: 0,
230            is_retry_enabled: true,
231            is_offline: true,
232        })
233    }
234
235    /// Dafault validation, see `AzureAuth` documentation for the defaults.
236    pub async fn validate_token(&mut self, token: &str) -> Result<Token<AzureJwtClaims>, AuthErr> {
237        let mut validator = jwt::Validation::new(jwt::Algorithm::RS256);
238
239        // exp, nbf, iat is set to validate as default
240        validator.leeway = 60;
241        validator.set_audience(&[&self.aud_to_val]);
242        let decoded: Token<AzureJwtClaims> =
243            self.validate_token_authenticity(token, &validator).await?;
244
245        Ok(decoded)
246    }
247
248    /// Allows for a custom validator and mapping the token to your own type.
249    /// Useful in situations where you get fields you that are not covered by
250    /// the default mapping or want to change the validaion requirements (i.e
251    /// if you want the leeway set to two minutes instead of one).
252    ///
253    /// # Note
254    /// You'll need to pull in `jsonwebtoken` to use `Validation` from that crate.
255    ///
256    /// # Example
257    ///
258    /// ```rust, no_run
259    /// use azure_jwt_async::AzureAuth;
260    /// use jsonwebtoken::{Algorithm, TokenData, Validation};
261    /// use serde::{Deserialize, Serialize};
262    /// use tokio_test::block_on;
263    ///
264    /// let mut validator = Validation::new(Algorithm::HS256);
265    /// validator.leeway = 120;
266    ///
267    /// #[derive(Serialize, Deserialize)]
268    /// struct MyClaims {
269    ///     group: String,
270    ///     roles: Vec<String>,
271    /// }
272    ///
273    /// tokio_test::block_on(async {
274    ///     let mut auth = AzureAuth::new("my_client_id_from_azure").await.unwrap();
275    ///
276    ///     let valid_token: TokenData<MyClaims> = auth
277    ///         .validate_custom("some-token", &validator)
278    ///         .await
279    ///         .unwrap();
280    /// });
281    /// ```
282    pub async fn validate_custom<T>(
283        &mut self,
284        token: &str,
285        validator: &jwt::Validation,
286    ) -> Result<Token<T>, AuthErr>
287    where
288        for<'de> T: Serialize + Deserialize<'de>,
289    {
290        let decoded: Token<T> = self.validate_token_authenticity(token, validator).await?;
291        Ok(decoded)
292    }
293
294    #[async_recursion]
295    async fn validate_token_authenticity<T>(
296        &mut self,
297        token: &str,
298        validator: &jwt::Validation,
299    ) -> Result<Token<T>, AuthErr>
300    where
301        for<'de> T: Serialize + Deserialize<'de>,
302    {
303        // if we´re in offline, we never refresh the keys. It's up to the user to do that.
304        if !self.is_keys_valid() && !self.is_offline {
305            self.refresh_pub_keys().await?;
306        }
307        // does not validate the token!
308        let decoded = jwt::decode_header(token)?;
309
310        let key = match &self.public_keys {
311            None => return Err(AuthErr::Other("Internal err. No public keys found.".into())),
312            Some(keys) => match &decoded.kid {
313                None => return Err(AuthErr::Other("No `kid` in token.".into())),
314                Some(kid) => keys.iter().find(|k| k.kid == *kid),
315            },
316        };
317
318        let auth_key = match key {
319            None => {
320                // the first time this happens let's go and refresh the keys and try once more.
321                // It could be that our keys are out of date. Limit to once in an hour.
322                if self.should_retry() {
323                    self.refresh_pub_keys().await?;
324                    self.retry_counter += 1;
325                    self.validate_token(token).await?;
326                    unreachable!()
327                } else {
328                    self.retry_counter = 0;
329                    return Err(AuthErr::Other(
330                        "Invalid token. Could not verify authenticity.".into(),
331                    ));
332                }
333            }
334            Some(key) => {
335                self.retry_counter = 0;
336                key
337            }
338        };
339
340        let key = DecodingKey::from_rsa_components(auth_key.modulus(), auth_key.exponent());
341        let valid: Token<T> = jwt::decode(token, &key, validator)?;
342
343        Ok(valid)
344    }
345
346    fn should_retry(&mut self) -> bool {
347        if self.is_offline || !self.is_retry_enabled {
348            return false;
349        }
350
351        match &self.last_refresh {
352            Some(lr) => {
353                self.retry_counter == 0 && Local::now().naive_local() - *lr > Duration::hours(1)
354            }
355            None => false,
356        }
357    }
358
359    /// Sets the expiration of the cached public keys in hours. Pr. 04.2019 Microsoft rotates these
360    /// every 24h.
361    pub fn set_expiration(&mut self, hours: i64) {
362        self.exp_hours = hours;
363    }
364
365    pub fn set_no_retry(&mut self) {
366        self.is_retry_enabled = false;
367    }
368
369    fn is_keys_valid(&self) -> bool {
370        match self.last_refresh {
371            None => false,
372            Some(lr) => (Local::now().naive_local() - lr) <= Duration::hours(self.exp_hours),
373        }
374    }
375
376    async fn refresh_pub_keys(&mut self) -> Result<(), AuthErr> {
377        let resp: Response = reqwest::get(&self.jwks_uri).await?;
378        let resp: JwkSet = resp.json().await?;
379        self.last_refresh = Some(Local::now().naive_local());
380        self.public_keys = Some(resp.keys);
381        Ok(())
382    }
383
384    /// Refreshes the jwks_uri by re-fetching it from the the OpenID metadata
385    /// document. See: <https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata>
386    /// Usually, this is not needed but for some cases you might want to try
387    /// to fetch a new uri on receiving an error.
388    pub async fn refresh_rwks_uri(&mut self) -> Result<(), AuthErr> {
389        self.jwks_uri = AzureAuth::get_jwks_uri().await?;
390        Ok(())
391    }
392
393    async fn get_jwks_uri() -> Result<String, AuthErr> {
394        let resp: Response = reqwest::get(AZ_OPENID_URL).await?;
395        let resp: OpenIdResponse = resp.json().await?;
396
397        Ok(resp.jwks_uri)
398    }
399
400    /// If you use the "offline" variant you'll need this to update the public keys, if you don't
401    /// use the offline version you probably don't want to change these unless you're testing.
402    pub fn set_public_keys(&mut self, pub_keys: Vec<Jwk>) {
403        self.last_refresh = Some(Local::now().naive_local());
404        self.public_keys = Some(pub_keys);
405    }
406}
407
408pub struct AzureJwtHeader {
409    /// Indicates that the token is a JWT.
410    pub typ: String,
411    /// Indicates the algorithm that was used to sign the token. Example: "RS256"
412    pub alg: String,
413    /// Thumbprint for the public key used to sign this token. Emitted in both
414    /// v1.0 and v2.0 id_tokens
415    pub kid: String,
416}
417
418#[derive(Clone, Debug, Serialize, Deserialize)]
419pub struct AzureJwtClaims {
420    /// dentifies the intended recipient of the token. In id_tokens, the audience
421    /// is your app's Application ID, assigned to your app in the Azure portal.
422    /// Your app should validate this value, and reject the token if the value
423    /// does not match.
424    pub aud: String,
425
426    /// The application ID of the client using the token. The application can
427    /// act as itself or on behalf of a user. The application ID typically
428    /// represents an application object, but it can also represent a service
429    /// principal object in Azure AD.
430    pub azp: Option<String>,
431
432    /// Indicates how the client was authenticated. For a public client, the
433    /// value is "0". If client ID and client secret are used, the value is "1".
434    /// If a client certificate was used for authentication, the value is "2".
435    pub azpacr: Option<String>,
436
437    /// The application ID of the client using the token.
438    /// The application can act as itself or on behalf of a user.
439    /// The application ID typically represents an application object, but it
440    /// can also represent a service principal object in Azure AD.
441    pub appid: Option<String>,
442
443    /// The "Authentication context class" claim.
444    /// A value of "0" indicates the end-user authentication did not meet the requirements of ISO/IEC 29115
445    pub acr: Option<String>,
446
447    /// Indicates how the client was authenticated. For a public client, the value is "0".
448    /// If client ID and client secret are used, the value is "1".
449    /// If a client certificate was used for authentication, the value is "2".
450    #[serde(deserialize_with = "deserialize_number_from_string")]
451    pub appidacr: u32,
452
453    /// Identifies how the subject of the token was authenticated.
454    /// Microsoft identities can authenticate in a variety of ways, which may be relevant to your application
455    pub amr: Option<Vec<String>>,
456
457    /// Identifies the security token service (STS) that constructs and returns
458    /// the token, and the Azure AD tenant in which the user was authenticated.
459    /// If the token was issued by the v2.0 endpoint, the URI will end in /v2.0.
460    /// The GUID that indicates that the user is a consumer user from a Microsoft
461    /// account is 9188040d-6c67-4c5b-b112-36a304b66dad.
462    ///
463    /// Your app should use the GUID portion of the claim to restrict the set of
464    /// tenants that can sign in to the app, if applicable.
465    pub iss: String,
466
467    /// Unix timestamp. "Issued At" indicates when the authentication for this
468    /// token occurred.
469    pub iat: u64,
470
471    /// Records the identity provider that authenticated the subject of the token.
472    /// This value is identical to the value of the Issuer claim unless the user
473    /// account not in the same tenant as the issuer - guests, for instance. If
474    /// the claim isn't present, it means that the value of iss can be used
475    /// instead. For personal accounts being used in an organizational context
476    /// (for instance, a personal account invited to an Azure AD tenant), the idp
477    /// claim may be 'live.com' or an STS URI containing the Microsoft account
478    /// tenant 9188040d-6c67-4c5b-b112-36a304b66dad
479    pub idp: Option<String>,
480
481    /// The IP address the user authenticated from.
482    pub ipddr: Option<String>,
483
484    /// Unix timestamp. The "nbf" (not before) claim identifies the time before
485    /// which the JWT MUST NOT be accepted for processing.
486    pub nbf: u64,
487
488    /// Unix timestamp. he "exp" (expiration time) claim identifies the
489    /// expiration time on or after which the JWT MUST NOT be accepted for
490    /// processing. It's important to note that a resource may reject the token
491    /// before this time as well - if, for example, a change in authentication
492    /// is required or a token revocation has been detected.
493    pub exp: u64,
494
495    /// The code hash is included in ID tokens only when the ID token is issued
496    /// with an OAuth 2.0 authorization code. It can be used to validate the
497    /// authenticity of an authorization code. For details about performing this
498    /// validation, see the OpenID Connect specification.
499    pub c_hash: Option<String>,
500
501    /// The access token hash is included in ID tokens only when the ID token is
502    /// issued with an OAuth 2.0 access token. It can be used to validate the
503    /// authenticity of an access token. For details about performing this
504    /// validation, see the OpenID Connect specification.
505    pub at_hash: Option<String>,
506
507    /// Provides the last name, surname, or family name of the user as defined on the user object.
508    pub family_name: Option<String>,
509
510    /// Provides the first or given name of the user, as set on the user object.
511    pub given_name: Option<String>,
512
513    /// The email claim is present by default for guest accounts that have an
514    /// email address. Your app can request the email claim for managed users
515    /// (those from the same tenant as the resource) using the email optional
516    /// claim. On the v2.0 endpoint, your app can also request the email OpenID
517    /// Connect scope - you don't need to request both the optional claim and
518    /// the scope to get the claim. The email claim only supports addressable
519    /// mail from the user's profile information.
520    pub preferred_username: Option<String>,
521
522    /// The name claim provides a human-readable value that identifies the
523    /// subject of the token. The value isn't guaranteed to be unique, it is
524    /// mutable, and it's designed to be used only for display purposes. The
525    /// profile scope is required to receive this claim.
526    pub name: Option<String>,
527
528    /// The nonce matches the parameter included in the original /authorize
529    /// request to the IDP. If it does not match, your application should reject
530    /// the token.
531    pub nonce: Option<String>,
532
533    /// Guid. The immutable identifier for an object in the Microsoft identity system,
534    /// in this case, a user account. This ID uniquely identifies the user
535    /// across applications - two different applications signing in the same
536    /// user will receive the same value in the oid claim. The Microsoft Graph
537    /// will return this ID as the id property for a given user account. Because
538    /// the oid allows multiple apps to correlate users, the profile scope is
539    /// required to receive this claim. Note that if a single user exists in
540    /// multiple tenants, the user will contain a different object ID in each
541    /// tenant - they're considered different accounts, even though the user
542    /// logs into each account with the same credentials.
543    pub oid: String,
544
545    /// The set of roles that were assigned to the user who is logging in.
546    pub roles: Option<Vec<String>>,
547
548    /// The set of scopes exposed by your application for which the client
549    /// application has requested (and received) consent. Your app should verify
550    /// that these scopes are valid ones exposed by your app, and make authorization
551    /// decisions based on the value of these scopes. Only included for user tokens.
552    pub scp: Option<String>,
553
554    /// The principal about which the token asserts information, such as the
555    /// user of an app. This value is immutable and cannot be reassigned or
556    /// reused. The subject is a pairwise identifier - it is unique to a
557    /// particular application ID. If a single user signs into two different
558    /// apps using two different client IDs, those apps will receive two
559    /// different values for the subject claim. This may or may not be wanted
560    /// depending on your architecture and privacy requirements.
561    pub sub: String,
562
563    /// A GUID that represents the Azure AD tenant that the user is from.
564    /// For work and school accounts, the GUID is the immutable tenant ID of
565    /// the organization that the user belongs to. For personal accounts,
566    /// the value is 9188040d-6c67-4c5b-b112-36a304b66dad. The profile scope is
567    /// required to receive this claim.
568    pub tid: String,
569
570    /// Provides a human readable value that identifies the subject of the
571    /// token. This value isn't guaranteed to be unique within a tenant and
572    /// should be used only for display purposes. Only issued in v1.0 id_tokens.
573    pub unique_name: Option<String>,
574
575    /// The username of the user. May be a phone number, email address, or unformatted string.
576    /// Should only be used for display purposes and providing username hints in reauthentication scenarios.
577    pub upn: Option<String>,
578
579    /// Indicates the version of the id_token. Either 1.0 or 2.0.
580    pub ver: String,
581}
582
583#[derive(Debug, Deserialize)]
584struct JwkSet {
585    keys: Vec<Jwk>,
586}
587
588#[derive(Debug, Deserialize, Clone)]
589pub struct Jwk {
590    pub kid: String,
591    pub n: String,
592    pub e: String,
593}
594
595impl Jwk {
596    fn modulus(&self) -> &str {
597        &self.n
598    }
599
600    fn exponent(&self) -> &str {
601        &self.e
602    }
603}
604
605#[derive(Deserialize)]
606struct OpenIdResponse {
607    jwks_uri: String,
608}
609
610type Token<T> = jwt::TokenData<T>;
611
612#[cfg(test)]
613mod tests {
614    use simple_base64::{engine::general_purpose, Engine};
615
616    use super::*;
617
618    const PUBLIC_KEY_N: &str = "AOx0GOQcSt5AZu02nlGWUuXXppxeV9Cu_9LcgpVBg_WQb-5DBHZpqs8AMek5u5iI4hkHCcOyMbQrBsDIVa9xxZxR2kq_8GtERsnd6NClQimspxT1WVgX5_WCAd5rk__Iv0GocP2c_1CcdT8is2OZHeWQySyQNSgyJYg6Up7kFtYabiCyU5q9tTIHQPXiwY53IGsNvSkqbk-OsdWPT3E4dqp3vNraMqXhuSZ-52kLCHqwPgAsbztfFJxSAEBcp-TS3uNuHeSJwNWjvDKTPy2oMacNpbsKb2gZgzubR6hTjvupRjaQ9SHhXyL9lmSZOpCzz2XJSVRopKUUtB-VGA0qVlk";
619    const PUBLIC_KEY_E: &str = "AQAB";
620
621    const PRIVATE_KEY_TEST: &str =
622        "MIIEowIBAAKCAQEA7HQY5BxK3kBm7TaeUZZS5demnF5X0K7/0tyClUGD9ZBv7kME\
623dmmqzwAx6Tm7mIjiGQcJw7IxtCsGwMhVr3HFnFHaSr/wa0RGyd3o0KVCKaynFPVZ\
624WBfn9YIB3muT/8i/Qahw/Zz/UJx1PyKzY5kd5ZDJLJA1KDIliDpSnuQW1hpuILJT\
625mr21MgdA9eLBjncgaw29KSpuT46x1Y9PcTh2qne82toypeG5Jn7naQsIerA+ACxv\
626O18UnFIAQFyn5NLe424d5InA1aO8MpM/Lagxpw2luwpvaBmDO5tHqFOO+6lGNpD1\
627IeFfIv2WZJk6kLPPZclJVGikpRS0H5UYDSpWWQIDAQABAoIBAQC982Yrmi7q7IHC\
628/qWglUpzKhLGe2PAWVVaZ5rfnIoNs8K3fU8QcUKumFGAMsjpeM1pnaXSeExFmGsM\
629Y+Ox1YwSUA81DYxuH6Ned86YDqpgIDr5M0Ba7JmDOLWXoIR8byB19oMOuhjBAW+P\
630EKlb0Z2a1f1Gt3J8oAxWq8PDsShHRdjyesVS36QZpIgjZskcNws/zqqqDRrLWuLm\
631Avk6E+tMD6sqo9xpzEqHF7rmwtt5yAtM1oZdWoEg2O+wZH5DBX2GhLlNZi/8sIiF\
632Mo+jouQn+l6Qc4G65vnnoZ+yEuf9fTJPnTHBFMViUcmTPsdbD4eLfrRXwAE9GYrv\
633R/RVusABAoGBAPgsQ4kAChpzU2aP21NQV1XTBW+eoHVbcJoYuOlmwB6x5o8lDUz/\
634EQVVYZavfNY1AjhEkfltCDjm1GHyWofrtGKTy7DHSZwPw5CxuqDtaiC6PMpFEu+O\
635xa09s7IZxpgInlrhY5JskOkH495BQ0xIU8UDxuP6sdtVNeQmWGjKG7kBAoGBAPPp\
636Nid4QEV4XleyAXT/JQGugdpa7TirWOEATNo10YPPqz7GphRhucT0ipNKMi/0XKh3\
637U0IC7XxjUvtE2LP9TVGAcV/Wzi4EYp1fziFuF9QcUds2tJ60SpfgIQrmVcF1zHxn\
6384/mSABoIyFxZSb4Tq9f+KXPAO5/l0NjgrVwk6gVZAoGAbMVZxE4UH4u0XhtnEZkA\
6397kjS9R0dTtKJA8EaKpIyWkG2v76JmdmhaCkH4LeBi5EoK+lB4YR8OhRRuawzKaeR\
640JDOK7ywpgxEVsfFzzty/yyBVTIIBzqVQ1qFYhRLvC+ubHFH1BlQ3HyuqH9uS13hL\
6413unM3lceZPdv61MzJJqQlAECgYAWg0MFV5sPDnIexAZQZzBiPFot7lCQ93fHpMBz\
642L557/RIARFOV9AMyg6O6vpFtTa+zuPfNUvnajkxddthNnKajTCiqwOfc5Xi4r9wV\
643x9SZNlfz1NPNBjUQWZaTK/lkVtwd63TmVyx9OqxLoc4lpikpUYM/9NFMC+k/61T0\
644+U9EWQKBgCdZV3yxwkz3pi6/E40EXfUsj8HQG/UtFJGeUNQiysBrxTmtmwLyvJeC\
645GruG96j1JcehpbcWKV+ObyMQuk65dM94uM7Wa+2NCA/MvorVcU7wdPbq7/eczZU4\
646xMd+OWT6JsInVM1ASh1mcn+Q0/Z3WqxxetCQLqaMs+FATn059dGf";
647
648    fn test_token_header() -> String {
649        r#"{
650                "typ": "JWT",
651                "alg": "RS256",
652                "kid": "i6lGk3FZzxRcUb2C3nEQ7syHJlY"
653            }"#
654        .to_string()
655    }
656
657    fn test_token_claims() -> String {
658        format!(
659            r#"{{
660                "aud": "6e74172b-be56-4843-9ff4-e66a39bb12e3",
661                "iss": "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47/v2.0",
662                "iat": {},
663                "nbf": {},
664                "exp": {},
665                "aio": "AXQAi/8IAAAAtAaZLo3ChMif6KOnttRB7eBq4/DccQzjcJGxPYy/C3jDaNGxXd6wNIIVGRghNRnwJ1lOcAnNZcjvkoyrFxCttv33140RioOFJ4bCCGVuoCag1uOTT22222gHwLPYQ/uf79QX+0KIijdrmp69RctzmQ==",
666                "azp": "6e74172b-be56-4843-9ff4-e66a39bb12e3",
667                "appid": "6e74172b-be56-4843-9ff4-e66a39bb12e3",
668                "appidacr": "1",
669                "name": "Abe Lincoln",
670                "azpacr": "0",
671                "oid": "690222be-ff1a-4d56-abd1-7e4f7d38e474",
672                "preferred_username": "abeli@microsoft.com",
673                "rh": "I",
674                "scp": "access_as_user",
675                "sub": "HKZpfaHyWadeOouYlitjrI-KffTm222X5rrV3xDqfKQ",
676                "tid": "72f988bf-86f1-41af-91ab-2d7cd011db47",
677                "uti": "fqiBqXLPj0eQa82S-IYFAA",
678                "ver": "2.0"
679            }}"#,
680            chrono::Utc::now().timestamp() - 1000,
681            chrono::Utc::now().timestamp() - 2000,
682            chrono::Utc::now().timestamp() + 1000
683        )
684    }
685
686    // We create a test token from parts here. We use the v2 token used as example
687    // in https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens
688    fn generate_test_token() -> String {
689        let private_key = jwt::EncodingKey::from_base64_secret(PRIVATE_KEY_TEST).unwrap();
690
691        // we need to construct the calims in a function since we need to set
692        // the expiration relative to current time
693        let test_token_payload = test_token_claims();
694        let test_token_header = test_token_header();
695
696        // we base64 (url-safe-base64) the header and claims and arrange
697        // as a jwt payload -> header_as_base64.claims_as_base64
698        let test_token = [
699            general_purpose::URL_SAFE.encode(test_token_header),
700            general_purpose::URL_SAFE.encode(test_token_payload),
701        ]
702        .join(".");
703
704        // we create the signature using our private key
705        let signature =
706            jwt::crypto::sign(&test_token, &private_key, jwt::Algorithm::RS256).expect("Signed");
707
708        let public_key = Jwk {
709            kid: "".to_string(),
710            n: PUBLIC_KEY_N.to_string(),
711            e: PUBLIC_KEY_E.to_string(),
712        };
713
714        let public_key = DecodingKey::from_rsa_components(&public_key.n, &public_key.e);
715
716        // we construct a complete token which looks like: header.claims.signature
717        let complete_token = format!("{}.{}", test_token, signature);
718
719        // we verify the signature here as well to catch errors in our testing
720        // code early
721
722        let verified =
723            jwt::crypto::verify(&signature, &test_token, &public_key, jwt::Algorithm::RS256)
724                .expect("verified");
725        assert!(verified);
726
727        complete_token
728    }
729
730    #[tokio::test]
731    async fn decode_token() {
732        let token = generate_test_token();
733
734        // we need to construct our own key object that matches on `kid` field
735        // just as it should if we used the fetched keys from microsofts servers
736        // since our validation methods converts the base64 data to bytes for us
737        // we don't need to worry about that here.
738        // let from_std = base64::decode_config(PUBLIC_KEY_TEST, base64::STANDARD).unwrap();
739        // let to_url_safe = base64::encode_config(&from_std, base64::URL_SAFE);
740        let key = Jwk {
741            kid: "i6lGk3FZzxRcUb2C3nEQ7syHJlY".to_string(),
742            n: PUBLIC_KEY_N.to_string(),
743            e: PUBLIC_KEY_E.to_string(),
744        };
745
746        let mut az_auth =
747            AzureAuth::new_offline("6e74172b-be56-4843-9ff4-e66a39bb12e3", vec![key]).unwrap();
748
749        az_auth.validate_token(&token).await.unwrap();
750    }
751
752    // TODO: we need a test for the retry operation.
753
754    #[tokio::test]
755    async fn refresh_rwks_uri() {
756        let _az_auth = AzureAuth::new("app_secret").await.unwrap();
757    }
758
759    #[tokio::test]
760    async fn azure_ad_get_public_keys() {
761        let mut az_auth = AzureAuth::new("app_secret").await.unwrap();
762        az_auth.refresh_pub_keys().await.unwrap();
763    }
764
765    #[tokio::test]
766    async fn azure_ad_get_refresh_rwks_uri() {
767        let mut az_auth = AzureAuth::new("app_secret").await.unwrap();
768        az_auth.refresh_rwks_uri().await.unwrap();
769    }
770
771    #[tokio::test]
772    async fn is_not_valid_more_than_24h() {
773        let mut az_auth = AzureAuth::new("app_secret").await.unwrap();
774        az_auth.last_refresh = Some(Local::now().naive_local() - Duration::hours(25));
775
776        assert!(!az_auth.is_keys_valid());
777    }
778}