From 06c62e523d399fc1dc03d2fe944403c33c364e30 Mon Sep 17 00:00:00 2001 From: Alexander Korolev Date: Sun, 7 Apr 2024 15:34:56 +0200 Subject: [PATCH] make Bearer serialization/deserialization more predictable --- README.md | 20 ++++++++- src/bearer.rs | 111 ++++++++++++++++++++++++++------------------------ src/client.rs | 17 +++++--- 3 files changed, 87 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 7567f61..97e643d 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,28 @@ Implements [UMA2](https://docs.kantarainitiative.org/uma/wg/oauth-uma-federated- It supports Microsoft OIDC with feature `microsoft`. This adds methods for authentication and token validation, those skip issuer check. -This library is a quick and dirty rewrite of [inth-oauth2](https://crates.io/crates/inth-oauth2) and [oidc](https://crates.io/crates/oidc) to use async / await. The basic idea was to solve a specific problem, with the result that most of the good ideas from the original boxes were perverted and oversimplified. +Originally developed as a quick adaptation to leverage async/await functionality, based on [inth-oauth2](https://crates.io/crates/inth-oauth2) and [oidc](https://crates.io/crates/oidc), the library has since evolved into a mature and robust solution, offering expanded features and improved performance. Using [reqwest](https://crates.io/crates/reqwest) for the HTTP client and [biscuit](https://crates.io/crates/biscuit) for Javascript Object Signing and Encryption (JOSE). +## Support: + +You can contribute to the ongoing development and maintenance of OpenID library in various ways: + +### Sponsorship + +Your support, no matter how big or small, helps sustain the project and ensures its continued improvement. Reach out to explore sponsorship opportunities. + +### Feedback + +Whether you are a developer, user, or enthusiast, your feedback is invaluable. Share your thoughts, suggestions, and ideas to help shape the future of the library. + +### Contribution + +If you're passionate about open-source and have skills to share, consider contributing to the project. Every contribution counts! + +Thank you for being part of OpenID community. Together, we are making authentication processes more accessible, reliable, and efficient for everyone. + ## Usage Add dependency to Cargo.toml: diff --git a/src/bearer.rs b/src/bearer.rs index 8e4630f..a4cc58a 100644 --- a/src/bearer.rs +++ b/src/bearer.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Duration, Utc}; -use serde::{de::Visitor, ser::Serializer, Deserialize, Deserializer, Serialize}; -use std::fmt; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; /// The bearer token type. /// @@ -8,67 +8,46 @@ use std::fmt; #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] pub struct Bearer { pub access_token: String, + pub token_type: String, pub scope: Option, + pub state: Option, pub refresh_token: Option, - #[serde( - default, - rename = "expires_in", - deserialize_with = "expire_in_to_instant", - serialize_with = "serialize_expire_in" - )] - pub expires: Option>, + pub expires_in: Option, pub id_token: Option, + #[serde(flatten)] + pub extra: Option>, } -fn expire_in_to_instant<'de, D>(deserializer: D) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - struct ExpireInVisitor; - - impl<'de> Visitor<'de> for ExpireInVisitor { - type Value = Option>; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an integer containing seconds") - } - - fn visit_none(self) -> Result - where - E: serde::de::Error, - { - Ok(None) - } +/// Manages bearer tokens along with their expiration times. +pub struct TemporalBearerGuard { + bearer: Bearer, + expires_at: Option>, +} - fn visit_some(self, d: D) -> Result>, D::Error> - where - D: Deserializer<'de>, - { - let expire_in: u64 = serde::de::Deserialize::deserialize(d)?; - Ok(Some(Utc::now() + Duration::seconds(expire_in as i64))) - } +impl TemporalBearerGuard { + pub fn expired(&self) -> bool { + self.expires_at + .map(|expires_at| Utc::now() >= expires_at) + .unwrap_or_default() } - deserializer.deserialize_option(ExpireInVisitor) + pub fn expires_at(&self) -> Option> { + self.expires_at + } } -fn serialize_expire_in( - dt: &Option>, - serializer: S, -) -> Result { - match dt { - Some(dt) => serializer.serialize_some(&dt.timestamp()), - None => serializer.serialize_none(), +impl AsRef for TemporalBearerGuard { + fn as_ref(&self) -> &Bearer { + &self.bearer } } -impl Bearer { - pub fn expired(&self) -> bool { - if let Some(expires) = self.expires { - expires < Utc::now() - } else { - false - } +impl From for TemporalBearerGuard { + fn from(bearer: Bearer) -> Self { + let expires_at = bearer + .expires_in + .map(|expires_in| Utc::now() + Duration::seconds(expires_in as i64)); + Self { bearer, expires_at } } } @@ -76,6 +55,32 @@ impl Bearer { mod tests { use super::*; + #[test] + fn from_successful_response() { + let json = r#" + { + "access_token":"2YotnFZFEjr1zCsicMWpAA", + "token_type":"example", + "expires_in":3600, + "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", + "example_parameter":"example_value" + } + "#; + let bearer: Bearer = serde_json::from_str(json).unwrap(); + assert_eq!("2YotnFZFEjr1zCsicMWpAA", bearer.access_token); + assert_eq!("example", bearer.token_type); + assert_eq!(Some(3600), bearer.expires_in); + assert_eq!(Some("tGzv3JOkF0XG5Qx2TlKWIA".into()), bearer.refresh_token); + assert_eq!( + Some( + [("example_parameter".into(), "example_value".into())] + .into_iter() + .collect() + ), + bearer.extra + ); + } + #[test] fn from_response_refresh() { let json = r#" @@ -90,9 +95,7 @@ mod tests { assert_eq!("aaaaaaaa", bearer.access_token); assert_eq!(None, bearer.scope); assert_eq!(Some("bbbbbbbb".into()), bearer.refresh_token); - let expires = bearer.expires.unwrap(); - assert!(expires > (Utc::now() + Duration::seconds(3599))); - assert!(expires <= (Utc::now() + Duration::seconds(3600))); + assert_eq!(Some(3600), bearer.expires_in); } #[test] @@ -107,6 +110,6 @@ mod tests { assert_eq!("aaaaaaaa", bearer.access_token); assert_eq!(None, bearer.scope); assert_eq!(None, bearer.refresh_token); - assert_eq!(None, bearer.expires); + assert_eq!(None, bearer.expires_in); } } diff --git a/src/client.rs b/src/client.rs index 67adca9..4deef52 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ use crate::{ + bearer::TemporalBearerGuard, discovered, error::{ClientError, Decode, Error, Jose, Userinfo as ErrorUserinfo}, standard_claims_subject::StandardClaimsSubject, @@ -611,7 +612,7 @@ where /// See [RFC 6749, section 6](http://tools.ietf.org/html/rfc6749#section-6). pub async fn refresh_token( &self, - token: Bearer, + token: impl AsRef, scope: impl Into>, ) -> Result { // Ensure the non thread-safe `Serializer` is not kept across @@ -622,6 +623,7 @@ where body.append_pair( "refresh_token", token + .as_ref() .refresh_token .as_deref() .expect("No refresh_token field"), @@ -637,17 +639,20 @@ where let json = self.post_token(body).await?; let mut new_token: Bearer = serde_json::from_value(json)?; if new_token.refresh_token.is_none() { - new_token.refresh_token = token.refresh_token.clone(); + new_token.refresh_token = token.as_ref().refresh_token.clone(); } Ok(new_token) } /// Ensures an access token is valid by refreshing it if necessary. - pub async fn ensure_token(&self, token: Bearer) -> Result { - if token.expired() { - self.refresh_token(token, None).await + pub async fn ensure_token( + &self, + token_guard: TemporalBearerGuard, + ) -> Result { + if token_guard.expired() { + self.refresh_token(token_guard, None).await.map(From::from) } else { - Ok(token) + Ok(token_guard) } }