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}