chore: fmt

This commit is contained in:
cduvray 2023-01-08 13:45:21 +01:00
parent f77a7ce54f
commit b0667729a3
32 changed files with 3596 additions and 7 deletions

9
.gitignore vendored
View file

@ -1,10 +1,5 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
.idea/
.vscode
# These are backup files generated by rustfmt
**/*.rs.bk

1977
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

5
Cargo.toml Normal file
View file

@ -0,0 +1,5 @@
[workspace]
members = [
"demo-server",
"jwt-authorizer",
]

40
README.md Normal file
View file

@ -0,0 +1,40 @@
# jwt-authorizer
JWT authorizer Layer for Axum.
## Features
- JWT token verification (Bearer)
- Claims extraction
- JWKS endpoint support (with refresh)
- algoritms: ECDSA, RSA, EdDSA, HS
## Usage
See documentation of the [`jwt-authorizer`](./jwt-authorizer/docs/README.md) module or the [`demo-server`](./demo-server/) example.
## Development
### Key generation
EC (ECDSA) - (algorigthm ES256 - ECDSA using SHA-256)
curve name: prime256v1 (secp256r1, secp384r1)
> openssl ecparam -genkey -noout -name prime256v1 | openssl pkcs8 -topk8 -nocrypt -out ec-private.pem
> openssl ec -in ec-private.pem -pubout -out ec-public-key.pem
EdDSA (Edwards-curve Digital Signature Algorithm)
(Ed25519 - implémentation spécifique de EdDSA, utilisant la Courbe d'Edwards tordue)
> openssl genpkey -algorithm ed25519
## Contributing
Contributions are wellcome!
## License
MIT

5
config/ec256-private.pem Normal file
View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgH614zrAA67VgIGav
JQU718PqSvV95Z/QO0I4oxxpekmhRANCAAQxmLBzkRU/8Te+R3agp52viVZUw32+
B3IEGkEhUUmPBtZ6S1O+QejNJm9Nc1AFaHmCUgzpUCZr4wXaiz4yb8t2
-----END PRIVATE KEY-----

4
config/ec256-public.pem Normal file
View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMZiwc5EVP/E3vkd2oKedr4lWVMN9
vgdyBBpBIVFJjwbWektTvkHozSZvTXNQBWh5glIM6VAma+MF2os+Mm/Ldg==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIEoAdOduNX+lt7bOPEkqAOwYabpsGhyxiAaUbMw184Ca
-----END PRIVATE KEY-----

View file

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAuWtSkE+I9aTMYTTvuTE1rtu0rNdxp3DU33cJ/ksL1Gk=
-----END PUBLIC KEY-----

8
config/ed256-jwk.json Normal file
View file

@ -0,0 +1,8 @@
{
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
"kid": "key-ed",
"alg": "EdDSA"
}

55
config/jwks.json Normal file
View file

@ -0,0 +1,55 @@
{
"keys": [
{
"kty": "RSA",
"n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s",
"e": "AQAB",
"kid": "key-rsa",
"alg": "RS256",
"use": "sig"
},
{
"kty": "RSA",
"n": "yRE6rHuNR0QbHO3H3Kt2pOKGVhQqGZXInOduQNxXzuKlvQTLUTv4l4sggh5_CYYi_cvI-SXVT9kPWSKXxJXBXd_4LkvcPuUakBoAkfh-eiFVMh2VrUyWyj3MFl0HTVF9KwRXLAcwkREiS3npThHRyIxuy0ZMeZfxVL5arMhw1SRELB8HoGfG_AtH89BIE9jDBHZ9dLelK9a184zAf8LwoPLxvJb3Il5nncqPcSfKDDodMFBIMc4lQzDKL5gvmiXLXB1AGLm8KBjfE8s3L5xqi-yUod-j8MtvIj812dkS4QMiRVN_by2h3ZY8LYVGrqZXZTcgn2ujn8uKjXLZVD5TdQ",
"e": "AQAB",
"kid": "rsa01",
"alg": "RS256",
"use": "sig"
},
{
"kty": "EC",
"crv": "P-256",
"x": "MZiwc5EVP_E3vkd2oKedr4lWVMN9vgdyBBpBIVFJjwY",
"y": "1npLU75B6M0mb01zUAVoeYJSDOlQJmvjBdqLPjJvy3Y",
"kid": "key-ec",
"alg": "ES256",
"use": "sig"
},
{
"kty": "EC",
"crv": "P-256",
"x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ",
"y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4",
"kid": "ec01",
"alg": "ES256",
"use": "sig"
},
{
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
"kid": "key-ed",
"alg": "EdDSA"
},
{
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "2-Jj2UvNCvQiUPNYRgSi0cJSPiJI6Rs6D0UTeEpQVj8",
"kid": "ed01",
"alg": "EdDSA"
}
]
}

51
config/jwtRS256.key Normal file
View file

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

14
config/jwtRS256.key.pub Normal file
View file

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2pQeZdxa7q093K7bj5h6
+leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy/tw+5
e+Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV
+fcGGLhJnXl0+5/z7tKC7RvBoT3SGwlc/AmJqpFtTpEBn/fDnyqiZbpcjXYLExFp
Exm41xDitRKHWIwfc3dV8/vlNntlxCPGy/THkjdXJoHv2IJmlhvmr5/h03iGMLWD
KSywxOol/4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO+
XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4N
DX+A4KRMgaxcfAcui/x6gybksq6gF90+9nfQfmVMVJctZ6M+FvRr+itd1Nef5WAt
wUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY/eZhQxk33VBK9lavqNKPg6Q/
PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy/Grae1iRnsDt
dSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPW
c6JEotQqI0HwhzU0KHyoY4sCAwEAAQ==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWTFfCGljY6aw3Hrt
kHmPRiazukxPLb6ilpRAewjW8nihRANCAATDskChT+Altkm9X7MI69T3IUmrQU0L
950IxEzvw/x5BMEINRMrXLBJhqzO9Bm+d6JbqA21YQmd1Kt4RzLJR1W+
-----END PRIVATE KEY-----

View file

@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIGrD/e7uKYqSY4twDEsRfMMuLSrODf14dpTiTK6K1YI0
-----END PRIVATE KEY-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJETqse41HRBsc
7cfcq3ak4oZWFCoZlcic525A3FfO4qW9BMtRO/iXiyCCHn8JhiL9y8j5JdVP2Q9Z
IpfElcFd3/guS9w+5RqQGgCR+H56IVUyHZWtTJbKPcwWXQdNUX0rBFcsBzCRESJL
eelOEdHIjG7LRkx5l/FUvlqsyHDVJEQsHwegZ8b8C0fz0EgT2MMEdn10t6Ur1rXz
jMB/wvCg8vG8lvciXmedyo9xJ8oMOh0wUEgxziVDMMovmC+aJctcHUAYubwoGN8T
yzcvnGqL7JSh36Pwy28iPzXZ2RLhAyJFU39vLaHdljwthUaupldlNyCfa6Ofy4qN
ctlUPlN1AgMBAAECggEAdESTQjQ70O8QIp1ZSkCYXeZjuhj081CK7jhhp/4ChK7J
GlFQZMwiBze7d6K84TwAtfQGZhQ7km25E1kOm+3hIDCoKdVSKch/oL54f/BK6sKl
qlIzQEAenho4DuKCm3I4yAw9gEc0DV70DuMTR0LEpYyXcNJY3KNBOTjN5EYQAR9s
2MeurpgK2MdJlIuZaIbzSGd+diiz2E6vkmcufJLtmYUT/k/ddWvEtz+1DnO6bRHh
xuuDMeJA/lGB/EYloSLtdyCF6sII6C6slJJtgfb0bPy7l8VtL5iDyz46IKyzdyzW
tKAn394dm7MYR1RlUBEfqFUyNK7C+pVMVoTwCC2V4QKBgQD64syfiQ2oeUlLYDm4
CcKSP3RnES02bcTyEDFSuGyyS1jldI4A8GXHJ/lG5EYgiYa1RUivge4lJrlNfjyf
dV230xgKms7+JiXqag1FI+3mqjAgg4mYiNjaao8N8O3/PD59wMPeWYImsWXNyeHS
55rUKiHERtCcvdzKl4u35ZtTqQKBgQDNKnX2bVqOJ4WSqCgHRhOm386ugPHfy+8j
m6cicmUR46ND6ggBB03bCnEG9OtGisxTo/TuYVRu3WP4KjoJs2LD5fwdwJqpgtHl
yVsk45Y1Hfo+7M6lAuR8rzCi6kHHNb0HyBmZjysHWZsn79ZM+sQnLpgaYgQGRbKV
DZWlbw7g7QKBgQCl1u+98UGXAP1jFutwbPsx40IVszP4y5ypCe0gqgon3UiY/G+1
zTLp79GGe/SjI2VpQ7AlW7TI2A0bXXvDSDi3/5Dfya9ULnFXv9yfvH1QwWToySpW
Kvd1gYSoiX84/WCtjZOr0e0HmLIb0vw0hqZA4szJSqoxQgvF22EfIWaIaQKBgQCf
34+OmMYw8fEvSCPxDxVvOwW2i7pvV14hFEDYIeZKW2W1HWBhVMzBfFB5SE8yaCQy
pRfOzj9aKOCm2FjjiErVNpkQoi6jGtLvScnhZAt/lr2TXTrl8OwVkPrIaN0bG/AS
aUYxmBPCpXu3UjhfQiWqFq/mFyzlqlgvuCc9g95HPQKBgAscKP8mLxdKwOgX8yFW
GcZ0izY/30012ajdHY+/QK5lsMoxTnn0skdS+spLxaS5ZEO4qvPVb8RAoCkWMMal
2pOhmquJQVDPDLuZHdrIiKiDM20dy9sMfHygWcZjQ4WSxf/J7T9canLZIXFhHAZT
3wc9h4G8BBCtWN2TN/LsGZdB
-----END PRIVATE KEY-----

View file

@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEw7JAoU/gJbZJvV+zCOvU9yFJq0FN
C/edCMRM78P8eQTBCDUTK1ywSYaszvQZvneiW6gNtWEJndSreEcyyUdVvg==
-----END PUBLIC KEY-----

View file

@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA2+Jj2UvNCvQiUPNYRgSi0cJSPiJI6Rs6D0UTeEpQVj8=
-----END PUBLIC KEY-----

23
demo-server/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "demo-server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.64"
axum = { version = "0.6.1", features = ["headers"] }
headers = "0.3"
josekit = "0.8.1"
jsonwebtoken = "8.2.0"
once_cell = "1.8"
reqwest = { version = "0.11.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.34"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.3.4", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
jwt-authorizer = { path = "../jwt-authorizer" }

35
demo-server/request.http Normal file
View file

@ -0,0 +1,35 @@
###
POST http://localhost:3000/oidc/authorize
Content-Type: application/json
{"client_id":"foo","client_secret":"bar"}
### Protected RSA
GET http://localhost:3000/api/protected
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtleS1yc2EifQ.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.K9QFjvVquRF2-Wt1QRfipOGwiYsmRs7SAwqKskHemFb9BRRZutpfV4oEoHaXMLomTUe8rH0TMjpKcweYK_H1I8D4r-mAN216oUfxCQiFWDB8T2VBI8um-efUg67i2myDZJr5VXdZH8ywj7bn9LyNS4I_xT-J3XvsngeCpuxVSRiYu4FkcUkLrPzbu2sDyBXFqYO9FOorZ8sl0Ninc93fWT2uUrEG8jRyWCa4xpoqbKbm7CN7T2tOKF7mx_xdSPTeSM-U9mUiHsMOrXi1S05IM0hvNJrBduLS6sMTFWrVhis6zqnuxDOirwZS-aN0_SgMDnZTFPsCh8dkqFde1Pv1IYjZfr5OOHjQ9QWj6UDjam6M1eWVPK6QLlxv5bU_gnlAiHm9wJX38-REwmVhIJIBzKxsgJAu1gnRBxe36OM3rkgYxpB86YvfDyOoFlqx8erdxYv38AtvJibe4HB6KLndp_QMm5XXQsbfyEXWGe8hzDwozdhGeQsJXz7PcI3KPlv19PrUM8njElFpOiyfAEXwbtp1EZTzMZ4ZNF6LLFy1fpLcosgyp05o_2YMvngltSnN3v0IPncJx50StdYsoxPN9Ac_nH8VbNlHfmPHMklD1plof0pYf5SiL8yCQP9Uiw9NrN2PeQzbveMKF1T1UNbn2tefxoxr3k6sgWiMH_g_kkk
### Protected EC
GET http://localhost:3000/api/protected
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImtleS1lYyJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.r0qaeYJWhjEybhNPgrrFwLBTeLMYtL4IJqOxZyxH9m-JYWqzV0R0WyYQIkf_tQ1UmzqHc9_xzUZnzSjTeEwDHw
### Protected Ed
GET http://localhost:3000/api/protected
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImtleS1lZCJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.XAx9msioheXEH1XUEIWMHGBg25JOpBHqcgL_ou_S3fwVht2TbKRiDZ4G6ZyEtn57hCbOy250zTD_g0EbaMGwAg
###
GET http://localhost:3000/api/protected
Content-Type: application/json
Authorization: Bearer blablabla.xxxx.xxxx
###
GET http://localhost:3000/oidc/jwks
Content-Type: application/json
###
GET http://localhost:3000/oidc/tokens
Content-Type: application/json

70
demo-server/src/main.rs Normal file
View file

@ -0,0 +1,70 @@
use axum::{
routing::{get, post},
Router,
};
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
use serde::Deserialize;
use std::{fmt::Display, net::SocketAddr};
use tower_http::trace::TraceLayer;
use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod oidc_provider;
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,axum_poc=debug,tower_http=debug".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
fn claim_checker(u: &User) -> bool {
info!("checking claims: {} -> {}", u.sub, u.sub.contains('@'));
u.sub.contains('@') // must be an email
}
// First let's create an authorizer builder from a JWKS Endpoint
// User is a struct deserializable from JWT claims representing the authorized user
let jwt_auth: JwtAuthorizer<User> = JwtAuthorizer::new()
.from_jwks_url("http://localhost:3000/oidc/jwks")
.with_check(claim_checker);
let oidc = Router::new()
.route("/authorize", post(oidc_provider::authorize))
.route("/jwks", get(oidc_provider::jwks))
.route("/tokens", get(oidc_provider::tokens));
let api = Router::new()
.route("/protected", get(protected))
.layer(jwt_auth.layer());
// .layer(jwt_auth.check_claims(|_: User| true));
let app = Router::new()
.nest("/oidc/", oidc)
.nest("/api", api)
.layer(TraceLayer::new_for_http());
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("listening on {}", addr);
axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap();
}
async fn protected(JwtClaims(user): JwtClaims<User>) -> Result<String, AuthError> {
// Send the protected data to the user
Ok(format!("Welcome: {}", user.sub))
}
#[derive(Debug, Deserialize, Clone)]
struct User {
sub: String,
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "User: {:?}", self.sub)
}
}

View file

@ -0,0 +1,240 @@
use axum::{
async_trait,
extract::{FromRequestParts, TypedHeader},
headers::{authorization::Bearer, Authorization},
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
Json,
};
use josekit::jwk::{
alg::{ec::EcCurve, ec::EcKeyPair, ed::EdKeyPair, rsa::RsaKeyPair},
Jwk,
};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::fmt::Display;
pub static KEYS: Lazy<Keys> = Lazy::new(|| {
//let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
// Keys::new("xxxxx".as_bytes())
Keys::load_rsa()
});
pub struct Keys {
pub alg: Algorithm,
pub encoding: EncodingKey,
pub decoding: DecodingKey,
}
impl Keys {
fn new(secret: &[u8]) -> Self {
Self {
alg: Algorithm::HS256,
encoding: EncodingKey::from_secret(secret),
decoding: DecodingKey::from_secret(secret),
}
}
fn load_rsa() -> Self {
Self {
alg: Algorithm::RS256,
encoding: EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(),
decoding: DecodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key.pub")).unwrap(),
}
}
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
struct JwkSet {
keys: Vec<Jwk>,
}
pub async fn jwks() -> Json<Value> {
// let mut ksmap = serde_json::Map::new();
let mut kset = JwkSet {
keys: Vec::<Jwk>::new(),
};
let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap();
let mut pk = keypair.to_jwk_public_key();
pk.set_key_id("key-rsa");
pk.set_algorithm("RS256");
pk.set_key_use("sig");
kset.keys.push(pk);
let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/private_rsa_key_pkcs8.pem")).unwrap();
let mut pk = keypair.to_jwk_public_key();
pk.set_key_id("rsa01");
pk.set_algorithm("RS256");
pk.set_key_use("sig");
kset.keys.push(pk);
let keypair =
EcKeyPair::from_pem(include_bytes!("../../../config/ec256-private.pem"), Some(EcCurve::P256)).unwrap();
let mut pk = keypair.to_jwk_public_key();
pk.set_key_id("key-ec");
pk.set_algorithm("ES256");
pk.set_key_use("sig");
kset.keys.push(pk);
let keypair = EcKeyPair::from_pem(
include_bytes!("../../../config/private_ecdsa_key.pem"),
Some(EcCurve::P256),
)
.unwrap();
let mut pk = keypair.to_jwk_public_key();
pk.set_key_id("ec01");
pk.set_algorithm("ES256");
pk.set_key_use("sig");
kset.keys.push(pk);
let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap();
let mut pk = keypair.to_jwk_public_key();
pk.set_key_id("key-ed");
pk.set_algorithm("EdDSA");
pk.set_key_use("sig");
kset.keys.push(pk);
let keypair = EdKeyPair::from_pem(include_bytes!("../../../config/private_ed25519_key.pem")).unwrap();
let mut pk = keypair.to_jwk_public_key();
pk.set_key_id("ed01");
pk.set_algorithm("EdDSA");
pk.set_key_use("sig");
kset.keys.push(pk);
Json(json!(kset))
}
fn build_header(alg: Algorithm, kid: &str) -> Header {
Header {
typ: Some("JWT".to_string()),
alg,
kid: Some(kid.to_owned()),
cty: None,
jku: None,
jwk: None,
x5u: None,
x5c: None,
x5t: None,
x5t_s256: None,
}
}
pub async fn tokens() -> Json<Value> {
let claims = Claims {
sub: "b@b.com".to_owned(),
exp: 2000000000, // May 2033
};
let rsa_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap();
let ec_key = EncodingKey::from_ec_pem(include_bytes!("../../../config/ec256-private.pem")).unwrap();
let ed_key = EncodingKey::from_ed_pem(include_bytes!("../../../config/ed25519-private.pem")).unwrap();
let rsa_token = encode(&build_header(Algorithm::RS256, "key-rsa"), &claims, &rsa_key).unwrap();
let ec_token = encode(&build_header(Algorithm::ES256, "key-ec"), &claims, &ec_key).unwrap();
let ed_token = encode(&build_header(Algorithm::EdDSA, "key-ed"), &claims, &ed_key).unwrap();
Json(json!({
"rsa": rsa_token,
"ec": ec_token,
"ed": ed_token
}))
}
pub async fn authorize(Json(payload): Json<AuthPayload>) -> Result<Json<AuthBody>, AuthError> {
tracing::info!("authorizing ...");
if payload.client_id.is_empty() || payload.client_secret.is_empty() {
return Err(AuthError::MissingCredentials);
}
// Here you can check the user credentials from a database
if payload.client_id != "foo" || payload.client_secret != "bar" {
return Err(AuthError::WrongCredentials);
}
let claims = Claims {
sub: "b@b.com".to_owned(),
// Mandatory expiry time as UTC timestamp
exp: 2000000000, // May 2033
};
// Create the authorization token
let token = encode(&Header::new(KEYS.alg), &claims, &KEYS.encoding).map_err(|_| AuthError::TokenCreation)?;
// Send the authorized token
Ok(Json(AuthBody::new(token)))
}
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
exp: usize,
}
#[derive(Debug, Serialize)]
pub struct AuthBody {
access_token: String,
token_type: String,
}
#[derive(Debug, Deserialize)]
pub struct AuthPayload {
client_id: String,
client_secret: String,
}
#[derive(Debug)]
pub enum AuthError {
WrongCredentials,
MissingCredentials,
TokenCreation,
InvalidToken,
}
impl Display for Claims {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "sub: {}", self.sub)
}
}
impl AuthBody {
fn new(access_token: String) -> Self {
Self {
access_token,
token_type: "Bearer".to_string(),
}
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// Extract the token from the authorization header
let TypedHeader(Authorization(bearer)) = TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
.await
.map_err(|_| AuthError::InvalidToken)?;
let token_data = decode::<Claims>(bearer.token(), &KEYS.decoding, &Validation::default())
.map_err(|_| AuthError::InvalidToken)?;
Ok(token_data.claims)
}
}

33
jwt-authorizer/Cargo.toml Normal file
View file

@ -0,0 +1,33 @@
[package]
name = "jwt-authorizer"
description = "jwt authorizer middleware for axum"
version = "0.2.0"
edition = "2021"
authors = ["cduvray <c_duvray@proton.me>"]
license = "MIT"
readme = "docs/README.md"
repository = "https://github.com/cduvray/jwt-authorizer"
keywords = ["jwt","axum","authorisation"]
[dependencies]
axum = { version = "0.6.1", features = ["headers"] }
futures-util = "0.3.25"
futures-core = "0.3.25"
headers = "0.3"
jsonwebtoken = "8.2.0"
http = "0.2.8"
# pin-project-lite = "0.2.9"
pin-project = "1.0.12"
reqwest = { version = "0.11.13", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.37"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.3.4", features = ["trace", "auth"] }
tower-layer = "0.3"
tower-service = "0.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
wiremock = "0.5"

View file

View file

@ -0,0 +1,36 @@
# jwt-authorizer
JWT authoriser Layer for Axum.
Example:
```rust
use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims};
use axum::{routing::get, Router};
use serde::Deserialize;
// Authorized entity, struct 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> = JwtAuthorizer::new()
.from_jwks_url("http://localhost:3000/oidc/jwks");
// adding the authorization layer
let app = Router::new().route("/protected", get(protected))
.layer(jwt_auth.layer());
// proteced handler with user injection (mapping some jwt claims)
async fn protected(JwtClaims(user): JwtClaims<User>) -> Result<String, AuthError> {
// Send the protected data to the user
Ok(format!("Welcome: {}", user.sub))
}
# async {
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service()).await.expect("server failed");
# };
```

View file

@ -0,0 +1,170 @@
use std::{io::Read, time::Duration};
use jsonwebtoken::{decode, decode_header, jwk::JwkSet, DecodingKey, TokenData, Validation};
use serde::de::DeserializeOwned;
use crate::{
error::AuthError,
jwks::{key_store_manager::KeyStoreManager, KeySource},
};
pub trait ClaimsChecker<C> {
fn check(&self, claims: &C) -> bool;
}
#[derive(Clone)]
pub struct FnClaimsChecker<C>
where
C: Clone,
{
pub checker_fn: fn(&C) -> bool,
}
impl<C> ClaimsChecker<C> for FnClaimsChecker<C>
where
C: Clone,
{
fn check(&self, claims: &C) -> bool {
(self.checker_fn)(claims)
}
}
pub struct Authorizer<C>
where
C: Clone,
{
pub key_source: KeySource,
pub claims_checker: Option<FnClaimsChecker<C>>,
}
fn read_data(path: &str) -> Result<Vec<u8>, AuthError> {
let mut data = Vec::<u8>::new();
let mut f = std::fs::File::open(path)?;
f.read_to_end(&mut data)?;
Ok(data)
}
impl<C> Authorizer<C>
where
C: DeserializeOwned + Clone + Send + Sync,
{
pub fn from_jwks(jwks: &str, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, AuthError> {
let set: JwkSet = serde_json::from_str(jwks)?;
let k = DecodingKey::from_jwk(&set.keys[0])?;
Ok(Authorizer {
key_source: KeySource::DecodingKeySource(k),
claims_checker,
})
}
pub fn from_rsa_file(path: &str) -> Result<Authorizer<C>, AuthError> {
Ok(Authorizer {
key_source: KeySource::DecodingKeySource(DecodingKey::from_rsa_pem(&read_data(path)?)?),
claims_checker: None,
})
}
pub fn from_ec_file(path: &str) -> Result<Authorizer<C>, AuthError> {
let k = DecodingKey::from_ec_der(&read_data(path)?);
Ok(Authorizer {
key_source: KeySource::DecodingKeySource(k),
claims_checker: None,
})
}
pub fn from_ed_file(path: &str) -> Result<Authorizer<C>, AuthError> {
let k = DecodingKey::from_ed_der(&read_data(path)?);
Ok(Authorizer {
key_source: KeySource::DecodingKeySource(k),
claims_checker: None,
})
}
pub fn from_secret(secret: &str) -> Result<Authorizer<C>, AuthError> {
let k = DecodingKey::from_secret(secret.as_bytes());
Ok(Authorizer {
key_source: KeySource::DecodingKeySource(k),
claims_checker: None,
})
}
pub fn from_jwks_url(url: &str, claims_checker: Option<FnClaimsChecker<C>>) -> Result<Authorizer<C>, AuthError> {
let key_store_manager = KeyStoreManager::with_refresh_interval(url, Duration::from_secs(60));
Ok(Authorizer {
key_source: KeySource::KeyStoreSource(key_store_manager),
claims_checker,
})
}
pub async fn check_auth(&self, token: &str) -> Result<TokenData<C>, AuthError> {
let header = decode_header(token)?;
let validation = Validation::new(header.alg);
let decoding_key = self.key_source.get_key(header).await?;
let token_data = decode::<C>(token, &decoding_key, &validation)?;
if let Some(ref checker) = self.claims_checker {
if !checker.check(&token_data.claims) {
return Err(AuthError::InvalidClaims());
}
}
Ok(token_data)
}
}
#[cfg(test)]
mod tests {
use jsonwebtoken::{Algorithm, Header};
use serde_json::Value;
use super::Authorizer;
#[tokio::test]
async fn from_secret() {
let h = Header::new(Algorithm::HS256);
let a = Authorizer::<Value>::from_secret("xxxxxx").unwrap();
let k = a.key_source.get_key(h);
assert!(k.await.is_ok());
}
#[tokio::test]
async fn from_jwks() {
let jwks = r#"
{"keys": [{
"kid": "1",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "2pQeZdxa7q093K7bj5h6-leIpxfTnuAxzXdhjfGEJHxmt2ekHyCBWWWXCBiDn2RTcEBcy6gZqOW45Uy_tw-5e-Px1xFj1PykGEkRlOpYSAeWsNaAWvvpGB9m4zQ0PgZeMDDXE5IIBrY6YAzmGQxV-fcGGLhJnXl0-5_z7tKC7RvBoT3SGwlc_AmJqpFtTpEBn_fDnyqiZbpcjXYLExFpExm41xDitRKHWIwfc3dV8_vlNntlxCPGy_THkjdXJoHv2IJmlhvmr5_h03iGMLWDKSywxOol_4Wc1BT7Hb6byMxW40GKwSJJ4p7W8eI5mqggRHc8jlwSsTN9LZ2VOvO-XiVShZRVg7JeraGAfWwaIgIJ1D8C1h5Pi0iFpp2suxpHAXHfyLMJXuVotpXbDh4NDX-A4KRMgaxcfAcui_x6gybksq6gF90-9nfQfmVMVJctZ6M-FvRr-itd1Nef5WAtwUp1qyZygAXU3cH3rarscajmurOsP6dE1OHl3grY_eZhQxk33VBK9lavqNKPg6Q_PLiq1ojbYBj3bcYifJrsNeQwxldQP83aWt5rGtgZTehKVJwa40Uy_Grae1iRnsDtdSy5sTJIJ6EiShnWAdMoGejdiI8vpkjrdU8SWH8lv1KXI54DsbyAuke2cYz02zPWc6JEotQqI0HwhzU0KHyoY4s",
"e": "AQAB"
}]}
"#;
let a = Authorizer::<Value>::from_jwks(jwks, None).unwrap();
let k = a.key_source.get_key(Header::new(Algorithm::RS256));
assert!(k.await.is_ok());
}
#[tokio::test]
async fn from_file() {
let a = Authorizer::<Value>::from_rsa_file("../config/jwtRS256.key.pub").unwrap();
let k = a.key_source.get_key(Header::new(Algorithm::RS256));
assert!(k.await.is_ok());
let a = Authorizer::<Value>::from_ec_file("../config/ec256-public.pem").unwrap();
let k = a.key_source.get_key(Header::new(Algorithm::ES256));
assert!(k.await.is_ok());
let a = Authorizer::<Value>::from_ed_file("../config/ed25519-public.pem").unwrap();
let k = a.key_source.get_key(Header::new(Algorithm::EdDSA));
assert!(k.await.is_ok());
}
#[tokio::test]
async fn from_file_errors() {
let a = Authorizer::<Value>::from_rsa_file("./config/does-not-exist.pem");
println!("{:?}", a.as_ref().err());
assert!(a.is_err());
}
}

View file

@ -0,0 +1,60 @@
use axum::{
extract::rejection::TypedHeaderRejection,
http::StatusCode,
response::{IntoResponse, Response},
};
use jsonwebtoken::Algorithm;
use thiserror::Error;
use tracing::log::warn;
#[derive(Debug, Error)]
pub enum AuthError {
#[error(transparent)]
JwksSerialisationError(#[from] serde_json::Error),
#[error(transparent)]
JwksRefreshError(#[from] reqwest::Error),
#[error(transparent)]
KeyFileError(#[from] std::io::Error),
#[error("InvalidKey {0}")]
InvalidKey(String),
#[error("Invalid Kid {0}")]
InvalidKid(String),
#[error("Invalid Key Algorithm {0:?}")]
InvalidKeyAlg(Algorithm),
#[error(transparent)]
InvalidTokenHeader(#[from] TypedHeaderRejection),
#[error(transparent)]
InvalidToken(#[from] jsonwebtoken::errors::Error),
#[error("Invalid Claim")]
InvalidClaims(),
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
warn!("AuthError: {}", &self);
let (status, error_message) = match self {
AuthError::JwksRefreshError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
AuthError::KeyFileError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
AuthError::InvalidKid(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
AuthError::InvalidTokenHeader(_) => (StatusCode::BAD_REQUEST, self.to_string()),
AuthError::InvalidToken(_) => (StatusCode::BAD_REQUEST, self.to_string()),
AuthError::InvalidKey(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
AuthError::JwksSerialisationError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
AuthError::InvalidKeyAlg(_) => (StatusCode::BAD_REQUEST, self.to_string()),
AuthError::InvalidClaims() => (StatusCode::FORBIDDEN, self.to_string()),
};
let body = axum::Json(serde_json::json!({
"error": error_message,
}));
(status, body).into_response()
}
}

View file

@ -0,0 +1,373 @@
use jsonwebtoken::{
jwk::{Jwk, JwkSet},
Algorithm, DecodingKey,
};
use std::{
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::Mutex;
use crate::error::AuthError;
#[derive(Clone)]
pub enum RefreshStrategy {
/// refresh periodicaly
Interval(Duration),
/// when kid not found in the store
KeyNotFound,
// other strategies? KeyNotFoundOrInterval(Duration), Once,
}
#[derive(Clone)]
pub struct KeyStoreManager {
key_url: String,
refresh: RefreshStrategy,
/// in case of fail loading (error or key not found), minimal interval
minimal_refresh_interval: Duration,
keystore: Arc<Mutex<KeyStore>>,
}
pub struct KeyStore {
/// key set
jwks: JwkSet,
/// time of the last successfully loaded jwkset
load_time: Option<Instant>,
/// time of the last failed load
fail_time: Option<Instant>,
}
impl KeyStoreManager {
pub(crate) fn new(url: &str, refresh: RefreshStrategy) -> KeyStoreManager {
KeyStoreManager {
key_url: url.to_owned(),
refresh,
minimal_refresh_interval: Duration::from_secs(5), // TODO: make configurable
keystore: Arc::new(Mutex::new(KeyStore {
jwks: JwkSet { keys: vec![] },
load_time: None,
fail_time: None,
})),
}
}
pub(crate) fn with_refresh(url: &str) -> KeyStoreManager {
KeyStoreManager::new(url, RefreshStrategy::KeyNotFound)
}
pub(crate) fn with_refresh_interval(url: &str, interval: Duration) -> KeyStoreManager {
KeyStoreManager::new(url, RefreshStrategy::Interval(interval))
}
pub(crate) async fn get_key(&self, header: &jsonwebtoken::Header) -> Result<jsonwebtoken::DecodingKey, AuthError> {
let kstore = self.keystore.clone();
let mut ks_gard = kstore.lock().await;
let key = match self.refresh {
RefreshStrategy::Interval(refresh_interval) => {
if ks_gard.should_refresh(refresh_interval) && ks_gard.can_refresh(self.minimal_refresh_interval) {
ks_gard.refresh(&self.key_url, &[]).await?;
}
if let Some(ref kid) = header.kid {
ks_gard
.find_kid(kid)
.ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))?
} else {
ks_gard
.find_alg(&header.alg)
.ok_or(AuthError::InvalidKeyAlg(header.alg))?
}
}
RefreshStrategy::KeyNotFound => {
if let Some(ref kid) = header.kid {
let jwk_opt = ks_gard.find_kid(kid);
if let Some(jwk) = jwk_opt {
jwk
} else if ks_gard.can_refresh(self.minimal_refresh_interval) {
ks_gard.refresh(&self.key_url, &[("kid", kid)]).await?;
ks_gard
.find_kid(kid)
.ok_or_else(|| AuthError::InvalidKid(kid.to_owned()))?
} else {
return Err(AuthError::InvalidKid(kid.to_owned()));
}
} else {
let jwk_opt = ks_gard.find_alg(&header.alg);
// .ok_or(AuthError::InvalidKeyAlg(header.alg))?
if let Some(jwk) = jwk_opt {
jwk
} else if ks_gard.can_refresh(self.minimal_refresh_interval) {
ks_gard
.refresh(
&self.key_url,
&[(
"alg",
&serde_json::to_string(&header.alg)
.map_err(|_| AuthError::InvalidKeyAlg(header.alg))?,
)],
)
.await?;
ks_gard
.find_alg(&header.alg)
.ok_or_else(|| AuthError::InvalidKeyAlg(header.alg))?
} else {
return Err(AuthError::InvalidKeyAlg(header.alg));
}
}
}
};
DecodingKey::from_jwk(key).map_err(|err| AuthError::InvalidKey(err.to_string()))
}
}
impl KeyStore {
fn should_refresh(&self, refresh_interval: Duration) -> bool {
if let Some(t) = self.load_time {
t.elapsed() > refresh_interval
} else {
true
}
}
fn can_refresh(&self, minimal_refresh_interval: Duration) -> bool {
if let Some(ft) = self.fail_time {
if let Some(lt) = self.load_time {
ft.elapsed() > minimal_refresh_interval && lt.elapsed() > minimal_refresh_interval
} else {
ft.elapsed() > minimal_refresh_interval
}
} else if let Some(lt) = self.load_time {
lt.elapsed() > minimal_refresh_interval
} else {
true
}
}
async fn refresh(&mut self, key_url: &str, qparam: &[(&str, &str)]) -> Result<(), AuthError> {
reqwest::Client::new()
.get(key_url)
.query(qparam)
.send()
.await
.map_err(AuthError::JwksRefreshError)?
.json::<JwkSet>()
.await
.map(|jwks| {
self.load_time = Some(Instant::now());
self.jwks = jwks;
Ok(())
})
.map_err(|e| {
self.fail_time = Some(Instant::now());
AuthError::JwksRefreshError(e)
})?
}
/// Find the key in the set that matches the given key id, if any.
pub fn find_kid(&self, kid: &str) -> Option<&Jwk> {
self.jwks.find(kid)
}
/// Find the key in the set that matches the given key id, if any.
pub fn find_alg(&self, alg: &Algorithm) -> Option<&Jwk> {
self.jwks.keys.iter().find(|jwk| {
if let Some(ref a) = jwk.common.algorithm {
alg == a
} else {
false
}
})
}
/// Find first key.
pub fn find_first(&self) -> Option<&Jwk> {
self.jwks.keys.get(0)
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use jsonwebtoken::Algorithm;
use jsonwebtoken::{jwk::Jwk, Header};
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};
use crate::jwks::key_store_manager::{KeyStore, KeyStoreManager};
#[test]
fn keystore_should_refresh() {
let ks = KeyStore {
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
fail_time: None,
load_time: Some(Instant::now()),
};
assert!(!ks.should_refresh(Duration::from_secs(5)));
let ks = KeyStore {
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
fail_time: None,
load_time: Some(Instant::now() - Duration::from_secs(6)),
};
assert!(ks.should_refresh(Duration::from_secs(5)));
}
#[test]
fn keystore_can_refresh() {
let ks = KeyStore {
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
fail_time: Some(Instant::now() - Duration::from_secs(5)),
load_time: None,
};
assert!(ks.can_refresh(Duration::from_secs(4)));
assert!(!ks.can_refresh(Duration::from_secs(6)));
let ks = KeyStore {
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![] },
fail_time: None,
load_time: Some(Instant::now() - Duration::from_secs(5)),
};
assert!(ks.can_refresh(Duration::from_secs(4)));
assert!(!ks.can_refresh(Duration::from_secs(6)));
}
#[test]
fn find_kid() {
let jwk0: Jwk = serde_json::from_str(r#"{"kid":"1","kty":"RSA","alg":"RS256","n":"xxxx","e":"AQAB"}"#).unwrap();
let jwk1: Jwk = serde_json::from_str(r#"{"kid":"2","kty":"RSA","alg":"RS256","n":"xxxx","e":"AQAB"}"#).unwrap();
let ks = KeyStore {
load_time: None,
fail_time: None,
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![jwk0, jwk1] },
};
assert!(ks.find_kid("1").is_some());
assert!(ks.find_kid("2").is_some());
assert!(ks.find_kid("3").is_none());
}
#[test]
fn find_alg() {
let jwk0: Jwk = serde_json::from_str(r#"{"kty": "RSA", "alg": "RS256", "n": "xxx","e": "yyy"}"#).unwrap();
let ks = KeyStore {
load_time: None,
fail_time: None,
jwks: jsonwebtoken::jwk::JwkSet { keys: vec![jwk0] },
};
assert!(ks.find_alg(&Algorithm::RS256).is_some());
assert!(ks.find_alg(&Algorithm::EdDSA).is_none());
}
async fn mock_jwks_response_once(mock_server: &MockServer, jwk: &str) {
let jwk0: Jwk = serde_json::from_str(jwk).unwrap();
let jwks = jsonwebtoken::jwk::JwkSet { keys: vec![jwk0] };
Mock::given(method("GET"))
.and(path("/"))
.respond_with(ResponseTemplate::new(200).set_body_json(&jwks))
.expect(1)
.mount(&mock_server)
.await;
}
fn build_header(kid: &str, alg: Algorithm) -> Header {
let mut header = Header::new(alg);
header.kid = Some(kid.to_owned());
header
}
#[tokio::test]
async fn keystore_manager_find_key_with_refresh_interval() {
let mock_server = MockServer::start().await;
mock_jwks_response_once(
&mock_server,
r#"{
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
"kid": "key-ed",
"alg": "EdDSA"
}"#,
)
.await;
let ksm = KeyStoreManager::with_refresh_interval(&mock_server.uri(), Duration::from_secs(3000));
let r = ksm.get_key(&Header::new(Algorithm::EdDSA)).await;
assert!(r.is_ok());
mock_server.verify().await;
}
#[tokio::test]
async fn keystore_manager_find_key_with_refresh() {
let mock_server = MockServer::start().await;
mock_jwks_response_once(
&mock_server,
r#"{
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
"kid": "key-ed",
"alg": "EdDSA"
}"#,
)
.await;
let mut ksm = KeyStoreManager::with_refresh(&mock_server.uri());
// STEP 1: initial (lazy) reloading
let r = ksm.get_key(&build_header("key-ed", Algorithm::EdDSA)).await;
assert!(r.is_ok());
mock_server.verify().await;
// STEP2: new kid -> reloading ksm
mock_server.reset().await;
mock_jwks_response_once(
&mock_server,
r#"{
"kty": "OKP",
"use": "sig",
"crv": "Ed25519",
"x": "uWtSkE-I9aTMYTTvuTE1rtu0rNdxp3DU33cJ_ksL1Gk",
"kid": "key-ed02",
"alg": "EdDSA"
}"#,
)
.await;
let h = build_header("key-ed02", Algorithm::EdDSA);
assert!(ksm.get_key(&h).await.is_err());
ksm.minimal_refresh_interval = Duration::from_millis(100);
tokio::time::sleep(Duration::from_millis(101)).await;
assert!(ksm.get_key(&h).await.is_ok());
mock_server.verify().await;
// STEP3: new algorithm -> try to reload
mock_server.reset().await;
mock_jwks_response_once(
&mock_server,
r#"{
"kty": "EC",
"crv": "P-256",
"x": "w7JAoU_gJbZJvV-zCOvU9yFJq0FNC_edCMRM78P8eQQ",
"y": "wQg1EytcsEmGrM70Gb53oluoDbVhCZ3Uq3hHMslHVb4",
"kid": "ec01",
"alg": "ES256",
"use": "sig"
}"#,
)
.await;
let h = Header::new(Algorithm::ES256);
assert!(ksm.get_key(&h).await.is_err());
tokio::time::sleep(Duration::from_millis(101)).await;
assert!(ksm.get_key(&h).await.is_ok());
mock_server.verify().await;
}
}

View file

@ -0,0 +1,24 @@
use jsonwebtoken::{DecodingKey, Header};
use crate::error::AuthError;
use self::key_store_manager::KeyStoreManager;
pub mod key_store_manager;
#[derive(Clone)]
pub enum KeySource {
KeyStoreSource(KeyStoreManager),
DecodingKeySource(DecodingKey),
}
impl KeySource {
pub async fn get_key(&self, header: Header) -> Result<DecodingKey, AuthError> {
match self {
KeySource::KeyStoreSource(kstore) => kstore.get_key(&header).await,
KeySource::DecodingKeySource(key) => {
Ok(key.clone()) // TODO: clone -> &
}
}
}
}

286
jwt-authorizer/src/layer.rs Normal file
View file

@ -0,0 +1,286 @@
use axum::http::Request;
use axum::response::IntoResponse;
use axum::{body::Body, response::Response};
use futures_core::ready;
use futures_util::future::BoxFuture;
use headers::authorization::Bearer;
use headers::{Authorization, HeaderMapExt};
use http::StatusCode;
use pin_project::pin_project;
use serde::de::DeserializeOwned;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use tower_layer::Layer;
use tower_service::Service;
use crate::authorizer::{Authorizer, FnClaimsChecker};
/// Authorizer Layer builder
///
/// - initialisation of the Authorizer from jwks, rsa, ed, ec or secret
/// - can define a checker (jwt claims check)
pub struct JwtAuthorizer<C>
where
C: Clone + DeserializeOwned,
{
url: Option<&'static str>,
claims_checker: Option<FnClaimsChecker<C>>,
}
/// layer builder
impl<C> JwtAuthorizer<C>
where
C: Clone + DeserializeOwned + Send + Sync,
{
pub fn new() -> Self {
JwtAuthorizer {
url: None,
claims_checker: None,
}
}
pub fn from_jwks_url(mut self, url: &'static str) -> JwtAuthorizer<C> {
self.url = Some(url);
self
}
pub fn from_rsa_pem(mut self, path: &'static str) -> JwtAuthorizer<C> {
// TODO
self
}
pub fn from_ec_der(mut self, path: &'static str) -> JwtAuthorizer<C> {
// TODO
self
}
pub fn from_ed_der(mut self, path: &'static str) -> JwtAuthorizer<C> {
// TODO
self
}
pub fn from_secret(mut self, path: &'static str) -> JwtAuthorizer<C> {
// TODO
self
}
/// layer that checks token validity and claim constraints (custom function)
pub fn with_check(mut self, checker_fn: fn(&C) -> bool) -> JwtAuthorizer<C> {
self.claims_checker = Some(FnClaimsChecker { checker_fn });
self
}
/// build axum layer
pub fn layer(&self) -> AsyncAuthorizationLayer<C> {
// TODO: replace unwrap
let auth = Arc::new(Authorizer::from_jwks_url(self.url.unwrap(), self.claims_checker.clone()).unwrap());
AsyncAuthorizationLayer::new(auth)
}
}
/// Trait for authorizing requests.
pub trait AsyncAuthorizer<B> {
type RequestBody;
type ResponseBody;
type Future: Future<Output = Result<Request<Self::RequestBody>, Response<Self::ResponseBody>>>;
/// Authorize the request.
///
/// If the future resolves to `Ok(request)` then the request is allowed through, otherwise not.
fn authorize(&self, request: Request<B>) -> Self::Future;
}
impl<B, S, C> AsyncAuthorizer<B> for AsyncAuthorizationService<S, C>
where
B: Send + Sync + 'static,
C: Clone + DeserializeOwned + Send + Sync + 'static,
{
type RequestBody = B;
type ResponseBody = Body;
type Future = BoxFuture<'static, Result<Request<B>, Response<Self::ResponseBody>>>;
fn authorize(&self, mut request: Request<B>) -> Self::Future {
let authorizer = self.auth.clone();
let h = request.headers();
let bearer: Authorization<Bearer> = h.typed_get().unwrap();
Box::pin(async move {
if let Ok(token_data) = authorizer.check_auth(bearer.token()).await {
// Set `token_data` as a request extension so it can be accessed by other
// services down the stack.
request.extensions_mut().insert(token_data);
Ok(request)
} else {
let unauthorized_response = Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(Body::empty())
.unwrap();
Err(unauthorized_response)
}
})
}
}
// -------------- Layer -----------------
#[derive(Clone)]
pub struct AsyncAuthorizationLayer<C>
where
C: Clone + DeserializeOwned + Send,
{
auth: Arc<Authorizer<C>>,
}
impl<C> AsyncAuthorizationLayer<C>
where
C: Clone + DeserializeOwned + Send,
{
pub fn new(auth: Arc<Authorizer<C>>) -> AsyncAuthorizationLayer<C> {
Self { auth }
}
}
impl<S, C> Layer<S> for AsyncAuthorizationLayer<C>
where
C: Clone + DeserializeOwned + Send + Sync,
{
type Service = AsyncAuthorizationService<S, C>;
fn layer(&self, inner: S) -> Self::Service {
AsyncAuthorizationService::new(inner, self.auth.clone())
}
}
// ---------- AsyncAuthorizationService --------
#[derive(Clone)]
pub struct AsyncAuthorizationService<S, C>
where
C: Clone + DeserializeOwned + Send + Sync,
{
pub inner: S,
pub auth: Arc<Authorizer<C>>,
}
impl<S, C> AsyncAuthorizationService<S, C>
where
C: Clone + DeserializeOwned + Send + Sync,
{
pub fn get_ref(&self) -> &S {
&self.inner
}
/// Gets a mutable reference to the underlying service.
pub fn get_mut(&mut self) -> &mut S {
&mut self.inner
}
/// Consumes `self`, returning the underlying service.
pub fn into_inner(self) -> S {
self.inner
}
}
impl<S, C> AsyncAuthorizationService<S, C>
where
C: Clone + DeserializeOwned + Send + Sync,
{
/// Authorize requests using a custom scheme.
///
/// The `Authorization` header is required to have the value provided.
pub fn new(inner: S, auth: Arc<Authorizer<C>>) -> AsyncAuthorizationService<S, C> {
Self { inner, auth }
}
}
impl<ReqBody, S, C> Service<Request<ReqBody>> for AsyncAuthorizationService<S, C>
where
ReqBody: Send + Sync + 'static,
S: Service<Request<ReqBody>, Response = Response> + Clone,
C: Clone + DeserializeOwned + Send + Sync + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = ResponseFuture<S, ReqBody, C>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let inner = self.inner.clone();
let auth_fut = self.authorize(req);
ResponseFuture {
state: State::Authorize { auth_fut },
service: inner,
}
}
}
#[pin_project]
/// Response future for [`AsyncAuthorizationService`].
pub struct ResponseFuture<S, ReqBody, C>
where
S: Service<Request<ReqBody>, Response = Response>,
ReqBody: Send + Sync + 'static,
C: Clone + DeserializeOwned + Send + Sync + 'static,
{
#[pin]
state: State<<AsyncAuthorizationService<S, C> as AsyncAuthorizer<ReqBody>>::Future, S::Future>,
service: S,
}
#[pin_project(project = StateProj)]
enum State<A, SFut> {
Authorize {
#[pin]
auth_fut: A,
},
Authorized {
#[pin]
svc_fut: SFut,
},
}
impl<S, ReqBody, C> Future for ResponseFuture<S, ReqBody, C>
where
S: Service<Request<ReqBody>, Response = Response>,
ReqBody: Send + Sync + 'static,
C: Clone + DeserializeOwned + Send + Sync,
{
type Output = Result<S::Response, S::Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.project();
loop {
match this.state.as_mut().project() {
StateProj::Authorize { auth_fut } => {
let auth = ready!(auth_fut.poll(cx));
match auth {
Ok(req) => {
let svc_fut = this.service.call(req);
this.state.set(State::Authorized { svc_fut })
}
Err(res) => {
tracing::info!("err: {:?}", res);
let r = (StatusCode::FORBIDDEN, format!("Unauthorized : {:?}", res)).into_response();
// TODO: replace r by res (type problems: res should be already a 403 error response)
return Poll::Ready(Ok(r));
}
};
}
StateProj::Authorized { svc_fut } => {
return svc_fut.poll(cx);
}
}
}
}
}

34
jwt-authorizer/src/lib.rs Normal file
View file

@ -0,0 +1,34 @@
#![doc = include_str!("../docs/README.md")]
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use jsonwebtoken::TokenData;
use serde::de::DeserializeOwned;
pub use self::error::AuthError;
pub use layer::JwtAuthorizer;
pub mod authorizer;
pub mod error;
pub mod jwks;
pub mod layer;
/// Claims serialized using T
#[derive(Debug, Clone, Copy, Default)]
pub struct JwtClaims<T>(pub T);
#[async_trait]
impl<T, S> FromRequestParts<S> for JwtClaims<T>
where
T: DeserializeOwned + Send + Sync + Clone + 'static,
S: Send + Sync,
{
type Rejection = error::AuthError;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
let claims = parts.extensions.get::<TokenData<T>>().unwrap(); // TODO: unwrap -> err
Ok(JwtClaims(claims.claims.clone())) // TODO: unwrap -> err
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1 @@
// TODO: tests

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
max_width=120