Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make Bearer serialization/deserialization more predictable #49

Merged
merged 1 commit into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading