From 240094c5e17000068f17580e85f236c975512f19 Mon Sep 17 00:00:00 2001 From: Darren B <68653294+Devd0@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:07:13 -0400 Subject: [PATCH 1/5] init req guard for rocket --- Cargo.toml | 2 ++ src/validators/mod.rs | 2 ++ src/validators/rocket.rs | 60 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/validators/rocket.rs diff --git a/Cargo.toml b/Cargo.toml index f1757d5..627c678 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ futures-util = "0.3.28" actix-rt = { version = "2.10.0", optional = true } actix-web = { version = "4.9.0", optional = true } axum = { version = "0.7.5", optional = true } +rocket = { version = "0.5.1" } axum-extra = { version = "0.9.3", features = ["cookie"], optional = true } tower = { version = "0.5.0", optional = true } async-trait = "0.1.81" @@ -67,3 +68,4 @@ native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] actix = ["dep:actix-rt", "dep:actix-web"] axum = ["dep:axum", "dep:axum-extra", "dep:tower"] +# rocket = ["dep:rocket"] diff --git a/src/validators/mod.rs b/src/validators/mod.rs index e51edce..ff55a58 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -7,3 +7,5 @@ pub mod jwks; pub mod actix; #[cfg(feature = "axum")] pub mod axum; +// #[cfg(feature = "rocket")] +pub mod rocket; diff --git a/src/validators/rocket.rs b/src/validators/rocket.rs new file mode 100644 index 0000000..1523647 --- /dev/null +++ b/src/validators/rocket.rs @@ -0,0 +1,60 @@ +use crate::validators::{ + authorizer::{ClerkAuthorizer, ClerkError, ClerkRequest}, + jwks::JwksProvider, +}; +use rocket::{ + http::Status, + request::{FromRequest, Outcome}, + Request, +}; + +// Implement ClerkRequest for Rocket's Request +impl<'r> ClerkRequest for &'r Request<'_> { + fn get_header(&self, key: &str) -> Option { + self.headers().get_one(key).map(|s| s.to_string()) + } + + fn get_cookie(&self, key: &str) -> Option { + self.cookies().get(key).map(|cookie| cookie.value().to_string()) + } +} + +pub struct ClerkGuardConfig { + pub authorizer: ClerkAuthorizer, + pub routes: Option>, +} + +impl ClerkGuardConfig { + pub fn new(jwks_provider: J, routes: Option>, validate_session_cookie: bool) -> Self { + let authorizer = ClerkAuthorizer::new(jwks_provider, validate_session_cookie); + Self { authorizer, routes } + } +} + +pub struct ClerkGuard { + _unused: std::marker::PhantomData, +} + +// Implement request guard for ClerkGuard +#[rocket::async_trait] +impl<'r, J: JwksProvider + Send + Sync + 'static> FromRequest<'r> for ClerkGuard { + type Error = ClerkError; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + // Retrieve the ClerkAuthorizer from managed state + let authorizer = request + .rocket() + .state::>() + .expect("ClerkAuthorizer not found in managed state"); + + match authorizer.authorizer.authorize(&request).await { + Ok(_) => Outcome::Success(ClerkGuard { + _unused: std::marker::PhantomData, + }), + Err(error) => match error { + ClerkError::Unauthorized(msg) => Outcome::Error((Status::Unauthorized, ClerkError::Unauthorized(msg))), + ClerkError::InternalServerError(msg) => Outcome::Error((Status::InternalServerError, ClerkError::InternalServerError(msg))), + }, + } + } +} From be209a02ecfaf6f08d0da6b346c4ecb835c0c8ba Mon Sep 17 00:00:00 2001 From: Darren B <68653294+Devd0@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:15:05 -0400 Subject: [PATCH 2/5] support optional auth based on route paths in rocket req guard --- src/validators/rocket.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/validators/rocket.rs b/src/validators/rocket.rs index 1523647..b43ba93 100644 --- a/src/validators/rocket.rs +++ b/src/validators/rocket.rs @@ -42,12 +42,30 @@ impl<'r, J: JwksProvider + Send + Sync + 'static> FromRequest<'r> for ClerkGuard async fn from_request(request: &'r Request<'_>) -> Outcome { // Retrieve the ClerkAuthorizer from managed state - let authorizer = request + let config = request .rocket() .state::>() .expect("ClerkAuthorizer not found in managed state"); - match authorizer.authorizer.authorize(&request).await { + match &config.routes { + Some(route_matches) => { + // If the user only wants to apply authentication to a select amount of routes, we handle that logic here + let path = request.uri().path(); + // Check if the path was NOT contained inside of the routes specified by the user... + let path_not_in_specified_routes = route_matches.iter().find(|&route| route == &path.to_string()).is_none(); + + if path_not_in_specified_routes { + // Since the path was not inside of the listed routes we want to trigger an early exit + return Outcome::Success(ClerkGuard { + _unused: std::marker::PhantomData, + }); + } + } + // Since we did find a matching route we can simply do nothing here and start the actual auth logic... + None => {} + } + + match config.authorizer.authorize(&request).await { Ok(_) => Outcome::Success(ClerkGuard { _unused: std::marker::PhantomData, }), From 13c7496b9c29f96ee007e8b3012e814a6987cc81 Mon Sep 17 00:00:00 2001 From: Darren B <68653294+Devd0@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:21:41 -0400 Subject: [PATCH 3/5] feature flag rocket dep --- Cargo.toml | 4 ++-- src/validators/mod.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 627c678..ec10eb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ futures-util = "0.3.28" actix-rt = { version = "2.10.0", optional = true } actix-web = { version = "4.9.0", optional = true } axum = { version = "0.7.5", optional = true } -rocket = { version = "0.5.1" } +rocket = { version = "0.5.0", optional = true } axum-extra = { version = "0.9.3", features = ["cookie"], optional = true } tower = { version = "0.5.0", optional = true } async-trait = "0.1.81" @@ -68,4 +68,4 @@ native-tls = ["reqwest/native-tls"] rustls-tls = ["reqwest/rustls-tls"] actix = ["dep:actix-rt", "dep:actix-web"] axum = ["dep:axum", "dep:axum-extra", "dep:tower"] -# rocket = ["dep:rocket"] +rocket = ["dep:rocket"] diff --git a/src/validators/mod.rs b/src/validators/mod.rs index ff55a58..96f42f0 100644 --- a/src/validators/mod.rs +++ b/src/validators/mod.rs @@ -7,5 +7,5 @@ pub mod jwks; pub mod actix; #[cfg(feature = "axum")] pub mod axum; -// #[cfg(feature = "rocket")] +#[cfg(feature = "rocket")] pub mod rocket; From 17418950eee6f290f1151b14ab28ad45ac3e722e Mon Sep 17 00:00:00 2001 From: Darren B <68653294+Devd0@users.noreply.github.com> Date: Sun, 13 Oct 2024 18:24:54 -0400 Subject: [PATCH 4/5] change error message struct name --- src/validators/rocket.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validators/rocket.rs b/src/validators/rocket.rs index b43ba93..c76a931 100644 --- a/src/validators/rocket.rs +++ b/src/validators/rocket.rs @@ -45,7 +45,7 @@ impl<'r, J: JwksProvider + Send + Sync + 'static> FromRequest<'r> for ClerkGuard let config = request .rocket() .state::>() - .expect("ClerkAuthorizer not found in managed state"); + .expect("ClerkGuardConfig not found in managed state"); match &config.routes { Some(route_matches) => { From 2dd9e2ed289c5fc22c2cb2d1bb4968aa8e4d351e Mon Sep 17 00:00:00 2001 From: Darren B <68653294+Devd0@users.noreply.github.com> Date: Sat, 19 Oct 2024 19:13:03 -0400 Subject: [PATCH 5/5] examples --- Cargo.toml | 5 +++++ README.md | 43 ++++++++++++++++++++++++++++++++++++++++ examples/rocket.rs | 35 ++++++++++++++++++++++++++++++++ src/validators/jwks.rs | 12 +++++------ src/validators/rocket.rs | 18 ++++++++++++----- 5 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 examples/rocket.rs diff --git a/Cargo.toml b/Cargo.toml index ec10eb3..081696c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,11 @@ name = "axum" path = "examples/axum.rs" required-features = ["axum"] +[[example]] +name = "rocket" +path = "examples/rocket.rs" +required-features = ["rocket"] + [lib] doctest = false diff --git a/README.md b/README.md index c355bd0..bea27ee 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,49 @@ async fn main() -> std::io::Result<()> { } ``` +### Protecting a rocket endpoint with Clerk.dev: + +With the `rocket` feature enabled: + +```rust +use clerk_rs::{ + clerk::Clerk, + validators::{ + jwks::MemoryCacheJwksProvider, + rocket::{ClerkGuard, ClerkGuardConfig}, + }, + ClerkConfiguration, +}; +use rocket::{ + get, launch, routes, + serde::{Deserialize, Serialize}, +}; + +#[derive(Serialize, Deserialize)] +struct Message { + content: String, +} + +#[get("/")] +fn index(jwt: ClerkGuard) -> &'static str { + "Hello world!" +} + +#[launch] +fn rocket() -> _ { + let config = ClerkConfiguration::new(None, None, Some("sk_test_F9HM5l3WMTDMdBB0ygcMMAiL37QA6BvXYV1v18Noit".to_string()), None); + let clerk = Clerk::new(config); + let clerk_config = ClerkGuardConfig::new( + MemoryCacheJwksProvider::new(clerk), + None, + true, // validate_session_cookie + ); + + rocket::build().mount("/", routes![index]).manage(clerk_config) +} + +``` + ## Roadmap - [ ] Support other http clients along with the default reqwest client (like hyper) diff --git a/examples/rocket.rs b/examples/rocket.rs new file mode 100644 index 0000000..a2a8eff --- /dev/null +++ b/examples/rocket.rs @@ -0,0 +1,35 @@ +use clerk_rs::{ + clerk::Clerk, + validators::{ + jwks::MemoryCacheJwksProvider, + rocket::{ClerkGuard, ClerkGuardConfig}, + }, + ClerkConfiguration, +}; +use rocket::{ + get, launch, routes, + serde::{Deserialize, Serialize}, +}; + +#[derive(Serialize, Deserialize)] +struct Message { + content: String, +} + +#[get("/")] +fn index(jwt: ClerkGuard) -> &'static str { + "Hello world!" +} + +#[launch] +fn rocket() -> _ { + let config = ClerkConfiguration::new(None, None, Some("sk_test_F9HM5l3WMTDMdBB0ygcMMAiL37QA6BvXYV1v18Noit".to_string()), None); + let clerk = Clerk::new(config); + let clerk_config = ClerkGuardConfig::new( + MemoryCacheJwksProvider::new(clerk), + None, + true, // validate_session_cookie + ); + + rocket::build().mount("/", routes![index]).manage(clerk_config) +} diff --git a/src/validators/jwks.rs b/src/validators/jwks.rs index 901e045..491751c 100644 --- a/src/validators/jwks.rs +++ b/src/validators/jwks.rs @@ -41,18 +41,18 @@ impl From for ClerkError { /// A [`JwksProvider`] implementation that doesn't do any caching. /// /// The JWKS is fetched from the Clerk API on every request. -pub struct SimpleJwksProvider { +pub struct JwksProviderNoCache { clerk_client: Clerk, } -impl SimpleJwksProvider { +impl JwksProviderNoCache { pub fn new(clerk_client: Clerk) -> Self { Self { clerk_client } } } #[async_trait] -impl JwksProvider for SimpleJwksProvider { +impl JwksProvider for JwksProviderNoCache { type Error = JwksProviderError; async fn get_key(&self, kid: &str) -> Result { @@ -273,7 +273,7 @@ pub(crate) mod tests { }; let clerk = Clerk::new(config); - let jwks = SimpleJwksProvider::new(clerk); + let jwks = JwksProviderNoCache::new(clerk); let res = jwks.get_key(MOCK_KID).await.expect("should retrieve key"); assert_eq!(res.kid, MOCK_KID); @@ -293,7 +293,7 @@ pub(crate) mod tests { }; let clerk = Clerk::new(config); - let jwks = SimpleJwksProvider::new(clerk); + let jwks = JwksProviderNoCache::new(clerk); jwks.get_key(MOCK_KID).await.expect("should retrieve key"); jwks.get_key(MOCK_KID).await.expect("should retrieve key"); @@ -314,7 +314,7 @@ pub(crate) mod tests { }; let clerk = Clerk::new(config); - let jwks = SimpleJwksProvider::new(clerk); + let jwks = JwksProviderNoCache::new(clerk); // try to get a key that doesn't exist let res = jwks.get_key("unknown key").await.expect_err("should fail"); diff --git a/src/validators/rocket.rs b/src/validators/rocket.rs index c76a931..d081a1e 100644 --- a/src/validators/rocket.rs +++ b/src/validators/rocket.rs @@ -8,6 +8,8 @@ use rocket::{ Request, }; +use super::authorizer::ClerkJwt; + // Implement ClerkRequest for Rocket's Request impl<'r> ClerkRequest for &'r Request<'_> { fn get_header(&self, key: &str) -> Option { @@ -32,7 +34,8 @@ impl ClerkGuardConfig { } pub struct ClerkGuard { - _unused: std::marker::PhantomData, + pub jwt: Option, + _marker: std::marker::PhantomData, } // Implement request guard for ClerkGuard @@ -57,7 +60,8 @@ impl<'r, J: JwksProvider + Send + Sync + 'static> FromRequest<'r> for ClerkGuard if path_not_in_specified_routes { // Since the path was not inside of the listed routes we want to trigger an early exit return Outcome::Success(ClerkGuard { - _unused: std::marker::PhantomData, + jwt: None, + _marker: std::marker::PhantomData, }); } } @@ -66,9 +70,13 @@ impl<'r, J: JwksProvider + Send + Sync + 'static> FromRequest<'r> for ClerkGuard } match config.authorizer.authorize(&request).await { - Ok(_) => Outcome::Success(ClerkGuard { - _unused: std::marker::PhantomData, - }), + Ok(jwt) => { + request.local_cache(|| jwt.clone()); + return Outcome::Success(ClerkGuard { + jwt: Some(jwt), + _marker: std::marker::PhantomData, + }); + } Err(error) => match error { ClerkError::Unauthorized(msg) => Outcome::Error((Status::Unauthorized, ClerkError::Unauthorized(msg))), ClerkError::InternalServerError(msg) => Outcome::Error((Status::InternalServerError, ClerkError::InternalServerError(msg))),