Skip to content

Commit

Permalink
make Bearer serialization/deserialization more predictable
Browse files Browse the repository at this point in the history
  • Loading branch information
kilork committed Apr 7, 2024
1 parent 92345eb commit 06c62e5
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 61 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
111 changes: 57 additions & 54 deletions src/bearer.rs
Original file line number Diff line number Diff line change
@@ -1,81 +1,86 @@
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.
///
/// See [RFC 6750](http://tools.ietf.org/html/rfc6750).
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct Bearer {
pub access_token: String,
pub token_type: String,
pub scope: Option<String>,
pub state: Option<String>,
pub refresh_token: Option<String>,
#[serde(
default,
rename = "expires_in",
deserialize_with = "expire_in_to_instant",
serialize_with = "serialize_expire_in"
)]
pub expires: Option<DateTime<Utc>>,
pub expires_in: Option<u64>,
pub id_token: Option<String>,
#[serde(flatten)]
pub extra: Option<HashMap<String, serde_json::Value>>,
}

fn expire_in_to_instant<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: Deserializer<'de>,
{
struct ExpireInVisitor;

impl<'de> Visitor<'de> for ExpireInVisitor {
type Value = Option<DateTime<Utc>>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer containing seconds")
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
/// Manages bearer tokens along with their expiration times.
pub struct TemporalBearerGuard {
bearer: Bearer,
expires_at: Option<DateTime<Utc>>,
}

fn visit_some<D>(self, d: D) -> Result<Option<DateTime<Utc>>, 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<DateTime<Utc>> {
self.expires_at
}
}

fn serialize_expire_in<S: Serializer>(
dt: &Option<DateTime<Utc>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match dt {
Some(dt) => serializer.serialize_some(&dt.timestamp()),
None => serializer.serialize_none(),
impl AsRef<Bearer> 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<Bearer> 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 }
}
}

#[cfg(test)]
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#"
Expand All @@ -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]
Expand All @@ -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);
}
}
17 changes: 11 additions & 6 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
bearer::TemporalBearerGuard,
discovered,
error::{ClientError, Decode, Error, Jose, Userinfo as ErrorUserinfo},
standard_claims_subject::StandardClaimsSubject,
Expand Down Expand Up @@ -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<Bearer>,
scope: impl Into<Option<&str>>,
) -> Result<Bearer, ClientError> {
// Ensure the non thread-safe `Serializer` is not kept across
Expand All @@ -622,6 +623,7 @@ where
body.append_pair(
"refresh_token",
token
.as_ref()
.refresh_token
.as_deref()
.expect("No refresh_token field"),
Expand All @@ -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<Bearer, ClientError> {
if token.expired() {
self.refresh_token(token, None).await
pub async fn ensure_token(
&self,
token_guard: TemporalBearerGuard,
) -> Result<TemporalBearerGuard, ClientError> {
if token_guard.expired() {
self.refresh_token(token_guard, None).await.map(From::from)
} else {
Ok(token)
Ok(token_guard)
}
}

Expand Down

0 comments on commit 06c62e5

Please sign in to comment.