feat: claims

This commit is contained in:
cduvray 2023-05-18 17:02:28 +02:00 committed by cduvray
parent 93325dce96
commit d3fc883006
6 changed files with 318 additions and 125 deletions

View file

@ -11,6 +11,7 @@ keywords = ["jwt","axum","authorisation","jwks"]
[dependencies]
axum = { version = "0.6", features = ["headers"] }
chrono = "0.4"
futures-util = "0.3"
futures-core = "0.3"
headers = "0.3"

View file

@ -19,20 +19,15 @@ JWT authoriser Layer for Axum and Tonic.
## Usage Example
```rust
# use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
# use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims, RegisteredClaims};
# use axum::{routing::get, Router};
# use serde::Deserialize;
# async {
// struct representing the authorized caller, deserializable from JWT claims
#[derive(Debug, Deserialize, Clone)]
struct User {
sub: String,
}
// let's create an authorizer builder from a JWKS Endpoint
let jwt_auth: JwtAuthorizer<User> =
// (a serializable struct can be used to represent jwt claims, JwtAuthorizer<RegisteredClaims> is the default)
let jwt_auth: JwtAuthorizer =
JwtAuthorizer::from_jwks_url("http://localhost:3000/oidc/jwks");
// adding the authorization layer
@ -40,9 +35,9 @@ JWT authoriser Layer for Axum and Tonic.
.layer(jwt_auth.layer().await.unwrap());
// proteced handler with user injection (mapping some jwt claims)
async fn protected(JwtClaims(user): JwtClaims<User>) -> Result<String, AuthError> {
async fn protected(JwtClaims(user): JwtClaims<RegisteredClaims>) -> Result<String, AuthError> {
// Send the protected data to the user
Ok(format!("Welcome: {}", user.sub))
Ok(format!("Welcome: {:?}", user.sub))
}
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())

View file

@ -0,0 +1,105 @@
use chrono::{DateTime, TimeZone, Utc};
use std::fmt;
use serde::{de, Deserialize, Deserializer};
/// Seconds since the epoch
#[derive(Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct NumericDate(i64);
impl From<NumericDate> for DateTime<Utc> {
fn from(t: NumericDate) -> Self {
Utc.timestamp_opt(t.0, 0).unwrap()
}
}
#[derive(PartialEq, Debug, Clone)]
pub struct StringList(Vec<String>);
/// Claims mentioned in the JWT specifications.
///
/// https://www.rfc-editor.org/rfc/rfc7519#section-4.1
#[derive(Deserialize, Clone, Debug)]
pub struct RegisteredClaims {
pub iss: Option<String>,
pub sub: Option<String>,
pub aud: Option<StringList>,
pub exp: Option<NumericDate>,
pub nbf: Option<NumericDate>,
pub iat: Option<NumericDate>,
pub jti: Option<String>,
}
impl<'de> Deserialize<'de> for StringList {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct StringListVisitor;
impl<'de> de::Visitor<'de> for StringListVisitor {
type Value = StringList;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "a space seperated strings")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
let auds: Vec<String> = v.split(' ').map(|s| s.to_owned()).collect();
Ok(StringList(auds))
}
}
deserializer.deserialize_string(StringListVisitor)
}
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, TimeZone, Utc};
use serde::Deserialize;
use serde_json::json;
use crate::claims::{NumericDate, RegisteredClaims, StringList};
#[derive(Deserialize)]
struct TestStruct {
v: StringList,
}
#[test]
fn rfc_claims_aud() {
let a: TestStruct = serde_json::from_str(r#"{"v":"a b"}"#).unwrap();
assert_eq!(a.v, StringList(vec!["a".to_owned(), "b".to_owned()]));
}
#[test]
fn from_numeric_date() {
let exp: DateTime<Utc> = NumericDate(1516239022).into();
assert_eq!(exp, Utc.timestamp_opt(1516239022, 0).unwrap());
assert_eq!(exp, DateTime::parse_from_rfc3339("2018-01-18T01:30:22.000Z").unwrap());
}
#[test]
fn rfc_claims() {
let jwt_json = json!({
"iss": "http://localhost:3001",
"aud": "aud1 aud2",
"sub": "bob",
"exp": 1516240122,
"iat": 1516239022,
}
);
let claims: RegisteredClaims = serde_json::from_value(jwt_json).expect("Failed RfcClaims deserialisation");
assert_eq!(claims.iss.unwrap(), "http://localhost:3001");
assert_eq!(claims.aud.unwrap(), StringList(vec!["aud1".to_owned(), "aud2".to_owned()]));
assert_eq!(claims.exp.unwrap(), NumericDate(1516240122));
let dt: DateTime<Utc> = claims.iat.unwrap().into();
assert_eq!(dt, Utc.timestamp_opt(1516239022, 0).unwrap());
}
}

View file

@ -13,6 +13,7 @@ use tower_layer::Layer;
use tower_service::Service;
use crate::authorizer::{Authorizer, FnClaimsChecker, KeySourceType};
use crate::claims::RegisteredClaims;
use crate::error::InitError;
use crate::jwks::key_store_manager::Refresh;
use crate::validation::Validation;
@ -22,7 +23,7 @@ use crate::{layer, AuthError, RefreshStrategy};
///
/// - initialisation of the Authorizer from jwks, rsa, ed, ec or secret
/// - can define a checker (jwt claims check)
pub struct JwtAuthorizer<C>
pub struct JwtAuthorizer<C = RegisteredClaims>
where
C: Clone + DeserializeOwned,
{

View file

@ -6,11 +6,13 @@ use jsonwebtoken::TokenData;
use serde::de::DeserializeOwned;
pub use self::error::AuthError;
pub use claims::{NumericDate, RegisteredClaims, StringList};
pub use jwks::key_store_manager::{Refresh, RefreshStrategy};
pub use layer::JwtAuthorizer;
pub use validation::Validation;
pub mod authorizer;
pub mod claims;
pub mod error;
pub mod jwks;
pub mod layer;