From 43f2523ec6e3c7600dbeabba643a1062c8fc99c2 Mon Sep 17 00:00:00 2001 From: cduvray Date: Sat, 28 Jan 2023 21:20:56 +0100 Subject: [PATCH] feat: oidc issuer --- demo-server/request.http | 10 +++++-- demo-server/src/main.rs | 28 +++++++++--------- demo-server/src/oidc_provider/mod.rs | 43 ++++++++++++++++++++++++++-- jwt-authorizer/docs/README.md | 2 +- jwt-authorizer/src/authorizer.rs | 4 ++- jwt-authorizer/src/error.rs | 3 ++ jwt-authorizer/src/layer.rs | 33 +++++++++++++++------ jwt-authorizer/src/lib.rs | 1 + jwt-authorizer/src/oidc.rs | 22 ++++++++++++++ 9 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 jwt-authorizer/src/oidc.rs diff --git a/demo-server/request.http b/demo-server/request.http index 62ec8bb..0deb062 100644 --- a/demo-server/request.http +++ b/demo-server/request.http @@ -24,12 +24,16 @@ GET http://localhost:3000/api/protected Content-Type: application/json Authorization: Bearer blablabla.xxxx.xxxx +### discovery +GET http://localhost:3001/.well-known/openid-configuration +Content-Type: application/json -### -GET http://localhost:3000/oidc/jwks + +### jwks +GET http://localhost:3001/jwks Content-Type: application/json ### -GET http://localhost:3000/oidc/tokens +GET http://localhost:3001/tokens Content-Type: application/json diff --git a/demo-server/src/main.rs b/demo-server/src/main.rs index 1ef275c..3f8318b 100644 --- a/demo-server/src/main.rs +++ b/demo-server/src/main.rs @@ -1,8 +1,5 @@ -use axum::{ - routing::{get, post}, - Router, -}; -use jwt_authorizer::{AuthError, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; +use axum::{routing::get, Router}; +use jwt_authorizer::{error::InitError, AuthError, JwtAuthorizer, JwtClaims, Refresh, RefreshStrategy}; use serde::Deserialize; use std::{fmt::Display, net::SocketAddr}; use tower_http::trace::TraceLayer; @@ -12,7 +9,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod oidc_provider; #[tokio::main] -async fn main() { +async fn main() -> Result<(), InitError> { tracing_subscriber::registry() .with(tracing_subscriber::EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info,axum_poc=debug,tower_http=debug".into()), @@ -26,9 +23,13 @@ async fn main() { u.sub.contains('@') // must be an email } + // starting oidc provider (discovery is needed by from_oidc()) + oidc_provider::run_server(); + // 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 = JwtAuthorizer::from_jwks_url("http://localhost:3000/oidc/jwks") + // let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com/") + let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("http://localhost:3001") // .no_refresh() .refresh(Refresh { strategy: RefreshStrategy::Interval, @@ -36,18 +37,15 @@ async fn main() { }) .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)); - + // actual router demo let api = Router::new() .route("/protected", get(protected)) - .layer(jwt_auth.layer().unwrap()); + // adding the authorizer layer + .layer(jwt_auth.layer().await?); // .layer(jwt_auth.check_claims(|_: User| true)); let app = Router::new() - .nest("/oidc/", oidc) + // actual protected apis .nest("/api", api) .layer(TraceLayer::new_for_http()); @@ -55,6 +53,8 @@ async fn main() { tracing::info!("listening on {}", addr); axum::Server::bind(&addr).serve(app.into_make_service()).await.unwrap(); + + Ok(()) } async fn protected(JwtClaims(user): JwtClaims) -> Result { diff --git a/demo-server/src/oidc_provider/mod.rs b/demo-server/src/oidc_provider/mod.rs index 27c240a..297f587 100644 --- a/demo-server/src/oidc_provider/mod.rs +++ b/demo-server/src/oidc_provider/mod.rs @@ -4,7 +4,8 @@ use axum::{ headers::{authorization::Bearer, Authorization}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, - Json, + routing::{get, post}, + Json, Router, }; use josekit::jwk::{ alg::{ec::EcCurve, ec::EcKeyPair, ed::EdKeyPair, rsa::RsaKeyPair}, @@ -14,7 +15,7 @@ use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use std::fmt::Display; +use std::{fmt::Display, net::SocketAddr, thread, time::Duration}; pub static KEYS: Lazy = Lazy::new(|| { //let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); @@ -22,6 +23,8 @@ pub static KEYS: Lazy = Lazy::new(|| { Keys::load_rsa() }); +const ISSUER_URI: &str = "http://localhost:3001"; + pub struct Keys { pub alg: Algorithm, pub encoding: EncodingKey, @@ -38,6 +41,23 @@ impl Keys { } } +/// OpenId Connect discovery (simplified for test purposes) +#[derive(Serialize, Clone)] +pub struct OidcDiscovery { + issuer: String, + jwks_uri: String, + authorization_endpoint: String, +} + +pub async fn discovery() -> Json { + let d = OidcDiscovery { + issuer: ISSUER_URI.to_owned(), + jwks_uri: format!("{}/jwks", ISSUER_URI), + authorization_endpoint: format!("{}/authorize", ISSUER_URI), + }; + Json(json!(d)) +} + #[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] struct JwkSet { keys: Vec, @@ -108,6 +128,7 @@ fn build_header(alg: Algorithm, kid: &str) -> Header { } } +/// issues test tokens (this is not a standard endpoint) pub async fn tokens() -> Json { let claims = Claims { sub: "b@b.com".to_owned(), @@ -150,6 +171,24 @@ pub async fn authorize(Json(payload): Json) -> Result) -> Result { diff --git a/jwt-authorizer/src/authorizer.rs b/jwt-authorizer/src/authorizer.rs index e179069..875ded0 100644 --- a/jwt-authorizer/src/authorizer.rs +++ b/jwt-authorizer/src/authorizer.rs @@ -51,12 +51,14 @@ pub enum KeySourceType { ED(String), Secret(&'static str), Jwks(String), + Discovery(String), } impl Authorizer where C: DeserializeOwned + Clone + Send + Sync, { + // TODO: expose it in JwtAuthorizer pub fn from_jwks(jwks: &str, claims_checker: Option>) -> Result, AuthError> { let set: JwkSet = serde_json::from_str(jwks)?; let k = DecodingKey::from_jwk(&set.keys[0])?; @@ -76,7 +78,7 @@ where KeySourceType::EC(path) => DecodingKey::from_ec_der(&read_data(path.as_str())?), KeySourceType::ED(path) => DecodingKey::from_ed_der(&read_data(path.as_str())?), KeySourceType::Secret(secret) => DecodingKey::from_secret(secret.as_bytes()), - KeySourceType::Jwks(_) => panic!("bug: use from_jwks_url() to initialise Authorizer"), // should never hapen + _ => panic!("bug: use from_jwks_url() or from_oidc() to initialise Authorizer"), // should never hapen }; Ok(Authorizer { diff --git a/jwt-authorizer/src/error.rs b/jwt-authorizer/src/error.rs index a69b344..9596019 100644 --- a/jwt-authorizer/src/error.rs +++ b/jwt-authorizer/src/error.rs @@ -19,6 +19,9 @@ pub enum InitError { #[error(transparent)] KeyFileDecodingError(#[from] jsonwebtoken::errors::Error), + + #[error("Builder Error {0}")] + DiscoveryError(String), } #[derive(Debug, Error)] diff --git a/jwt-authorizer/src/layer.rs b/jwt-authorizer/src/layer.rs index 5eb1ae7..c24faf1 100644 --- a/jwt-authorizer/src/layer.rs +++ b/jwt-authorizer/src/layer.rs @@ -17,7 +17,7 @@ use tower_service::Service; use crate::authorizer::{Authorizer, FnClaimsChecker, KeySourceType}; use crate::error::InitError; use crate::jwks::key_store_manager::Refresh; -use crate::{AuthError, RefreshStrategy}; +use crate::{oidc, AuthError, RefreshStrategy}; /// Authorizer Layer builder /// @@ -37,7 +37,16 @@ impl JwtAuthorizer where C: Clone + DeserializeOwned + Send + Sync, { - /// Build Authorizer Layer from a JWKS endpoint + /// Builds Authorizer Layer from a OpenId Connect discover metadata + pub fn from_oidc(issuer: &str) -> JwtAuthorizer { + JwtAuthorizer { + key_source_type: Some(KeySourceType::Discovery(issuer.to_string())), + refresh: Default::default(), + claims_checker: None, + } + } + + /// Builds Authorizer Layer from a JWKS endpoint pub fn from_jwks_url(url: &'static str) -> JwtAuthorizer { JwtAuthorizer { key_source_type: Some(KeySourceType::Jwks(url.to_owned())), @@ -46,7 +55,7 @@ where } } - /// Build Authorizer Layer from a RSA PEM file + /// Builds Authorizer Layer from a RSA PEM file pub fn from_rsa_pem(path: &'static str) -> JwtAuthorizer { JwtAuthorizer { key_source_type: Some(KeySourceType::RSA(path.to_owned())), @@ -55,7 +64,7 @@ where } } - /// Build Authorizer Layer from a EC PEM file + /// Builds Authorizer Layer from a EC PEM file pub fn from_ec_pem(path: &'static str) -> JwtAuthorizer { JwtAuthorizer { key_source_type: Some(KeySourceType::EC(path.to_owned())), @@ -64,7 +73,7 @@ where } } - /// Build Authorizer Layer from a EC PEM file + /// Builds Authorizer Layer from a EC PEM file pub fn from_ed_pem(path: &'static str) -> JwtAuthorizer { JwtAuthorizer { key_source_type: Some(KeySourceType::ED(path.to_owned())), @@ -73,7 +82,7 @@ where } } - /// Build Authorizer Layer from a secret phrase + /// Builds Authorizer Layer from a secret phrase pub fn from_secret(secret: &'static str) -> JwtAuthorizer { JwtAuthorizer { key_source_type: Some(KeySourceType::Secret(secret)), @@ -82,7 +91,7 @@ where } } - /// refresh configuration for jwk store + /// Refreshes configuration for jwk store pub fn refresh(mut self, refresh: Refresh) -> JwtAuthorizer { if self.refresh.is_some() { tracing::warn!("More than one refresh configuration found!"); @@ -112,7 +121,7 @@ where } /// Build axum layer - pub fn layer(&self) -> Result, InitError> { + pub async fn layer(&self) -> Result, InitError> { let auth = if let Some(ref key_source_type) = self.key_source_type { match key_source_type { KeySourceType::RSA(_) | KeySourceType::EC(_) | KeySourceType::ED(_) | KeySourceType::Secret(_) => { @@ -123,6 +132,14 @@ where self.claims_checker.clone(), self.refresh.unwrap_or_default(), )?), + KeySourceType::Discovery(issuer_url) => { + let jwks_url = oidc::discover_jwks(issuer_url).await?; + Arc::new(Authorizer::from_jwks_url( + &jwks_url, + self.claims_checker.clone(), + self.refresh.unwrap_or_default(), + )?) + } } } else { return Err(InitError::BuilderError( diff --git a/jwt-authorizer/src/lib.rs b/jwt-authorizer/src/lib.rs index 2dc657b..ec6cfe5 100644 --- a/jwt-authorizer/src/lib.rs +++ b/jwt-authorizer/src/lib.rs @@ -13,6 +13,7 @@ pub mod authorizer; pub mod error; pub mod jwks; pub mod layer; +mod oidc; /// Claims serialized using T #[derive(Debug, Clone, Copy, Default)] diff --git a/jwt-authorizer/src/oidc.rs b/jwt-authorizer/src/oidc.rs new file mode 100644 index 0000000..bcd49cb --- /dev/null +++ b/jwt-authorizer/src/oidc.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; + +use crate::error::InitError; + +/// OpenId Connect discovery (simplified for test purposes) +#[derive(Deserialize, Clone)] +pub struct OidcDiscovery { + pub jwks_uri: String, +} + +pub async fn discover_jwks(issuer: &str) -> Result { + let discovery_url = format!("{}/.well-known/openid-configuration", issuer); + reqwest::Client::new() + .get(discovery_url) + .send() + .await + .map_err(|e| InitError::DiscoveryError(e.to_string()))? + .json::() + .await + .map_err(|e| InitError::DiscoveryError(e.to_string())) + .map(|d| d.jwks_uri) +}