From 6ff5d88ae9ecf7437abfb4501e2785ef168194b4 Mon Sep 17 00:00:00 2001 From: cduvray Date: Mon, 30 Jan 2023 23:44:25 +0100 Subject: [PATCH] refactor: demo server (clean, refactor, docs) --- demo-server/request.http | 17 ++- demo-server/src/main.rs | 34 +++--- demo-server/src/oidc_provider/mod.rs | 171 +++++---------------------- jwt-authorizer/src/error.rs | 2 +- jwt-authorizer/src/oidc.rs | 2 +- 5 files changed, 56 insertions(+), 170 deletions(-) diff --git a/demo-server/request.http b/demo-server/request.http index 0deb062..59d3e9f 100644 --- a/demo-server/request.http +++ b/demo-server/request.http @@ -1,8 +1,5 @@ -### -POST http://localhost:3000/oidc/authorize -Content-Type: application/json - -{"client_id":"foo","client_secret":"bar"} +### Public URL +GET http://localhost:3000/public ### Protected RSA GET http://localhost:3000/api/protected @@ -19,7 +16,11 @@ GET http://localhost:3000/api/protected Content-Type: application/json Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImtleS1lZCJ9.eyJzdWIiOiJiQGIuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.XAx9msioheXEH1XUEIWMHGBg25JOpBHqcgL_ou_S3fwVht2TbKRiDZ4G6ZyEtn57hCbOy250zTD_g0EbaMGwAg -### +### 401 (no token) +GET http://localhost:3000/api/protected +Content-Type: application/json + +### 401 (invalid_token) GET http://localhost:3000/api/protected Content-Type: application/json Authorization: Bearer blablabla.xxxx.xxxx @@ -28,12 +29,10 @@ Authorization: Bearer blablabla.xxxx.xxxx GET http://localhost:3001/.well-known/openid-configuration Content-Type: application/json - ### jwks GET http://localhost:3001/jwks Content-Type: application/json - -### +### Test 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 3f8318b..3f1497d 100644 --- a/demo-server/src/main.rs +++ b/demo-server/src/main.rs @@ -1,13 +1,20 @@ 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 std::net::SocketAddr; use tower_http::trace::TraceLayer; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; mod oidc_provider; +/// Object representing claims +/// (a subset of deserialized claims) +#[derive(Debug, Deserialize, Clone)] +struct User { + sub: String, +} + #[tokio::main] async fn main() -> Result<(), InitError> { tracing_subscriber::registry() @@ -17,6 +24,7 @@ async fn main() -> Result<(), InitError> { .with(tracing_subscriber::fmt::layer()) .init(); + // claims checker function fn claim_checker(u: &User) -> bool { info!("checking claims: {} -> {}", u.sub, u.sub.contains('@')); @@ -24,12 +32,12 @@ async fn main() -> Result<(), InitError> { } // starting oidc provider (discovery is needed by from_oidc()) - oidc_provider::run_server(); + let issuer_uri = oidc_provider::run_server(); - // First let's create an authorizer builder from a JWKS Endpoint + // First let's create an authorizer builder from a Oidc Discovery // User is a struct deserializable from JWT claims representing the authorized user // let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("https://accounts.google.com/") - let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc("http://localhost:3001") + let jwt_auth: JwtAuthorizer = JwtAuthorizer::from_oidc(issuer_uri) // .no_refresh() .refresh(Refresh { strategy: RefreshStrategy::Interval, @@ -42,10 +50,11 @@ async fn main() -> Result<(), InitError> { .route("/protected", get(protected)) // adding the authorizer layer .layer(jwt_auth.layer().await?); - // .layer(jwt_auth.check_claims(|_: User| true)); let app = Router::new() - // actual protected apis + // public endpoint + .route("/public", get(public_handler)) + // protected APIs .nest("/api", api) .layer(TraceLayer::new_for_http()); @@ -57,18 +66,13 @@ async fn main() -> Result<(), InitError> { Ok(()) } +/// handler with injected claims object async fn protected(JwtClaims(user): JwtClaims) -> Result { // 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) - } +// public url handler +async fn public_handler() -> &'static str { + "Public URL!" } diff --git a/demo-server/src/oidc_provider/mod.rs b/demo-server/src/oidc_provider/mod.rs index 297f587..a2ff480 100644 --- a/demo-server/src/oidc_provider/mod.rs +++ b/demo-server/src/oidc_provider/mod.rs @@ -1,59 +1,27 @@ -use axum::{ - async_trait, - extract::{FromRequestParts, TypedHeader}, - headers::{authorization::Bearer, Authorization}, - http::{request::Parts, StatusCode}, - response::{IntoResponse, Response}, - routing::{get, post}, - Json, Router, -}; +use axum::{routing::get, Json, Router}; 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 jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -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"); - // Keys::new("xxxxx".as_bytes()) - Keys::load_rsa() -}); +use std::{net::SocketAddr, thread, time::Duration}; const ISSUER_URI: &str = "http://localhost:3001"; -pub struct Keys { - pub alg: Algorithm, - pub encoding: EncodingKey, - pub decoding: DecodingKey, -} - -impl Keys { - 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(), - } - } -} - /// OpenId Connect discovery (simplified for test purposes) #[derive(Serialize, Clone)] -pub struct OidcDiscovery { +struct OidcDiscovery { issuer: String, jwks_uri: String, - authorization_endpoint: String, } -pub async fn discovery() -> Json { +/// discovery url handler +async fn discovery() -> Json { let d = OidcDiscovery { issuer: ISSUER_URI.to_owned(), - jwks_uri: format!("{}/jwks", ISSUER_URI), - authorization_endpoint: format!("{}/authorize", ISSUER_URI), + jwks_uri: format!("{ISSUER_URI}/jwks"), }; Json(json!(d)) } @@ -63,9 +31,8 @@ struct JwkSet { keys: Vec, } -pub async fn jwks() -> Json { - // let mut ksmap = serde_json::Map::new(); - +/// jwk set endpoint handler +async fn jwks() -> Json { let mut kset = JwkSet { keys: Vec::::new() }; let keypair = RsaKeyPair::from_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(); @@ -113,6 +80,7 @@ pub async fn jwks() -> Json { Json(json!(kset)) } +/// build a minimal JWT header fn build_header(alg: Algorithm, kid: &str) -> Header { Header { typ: Some("JWT".to_string()), @@ -128,11 +96,22 @@ fn build_header(alg: Algorithm, kid: &str) -> Header { } } -/// issues test tokens (this is not a standard endpoint) +/// token claims +#[derive(Debug, Serialize, Deserialize)] +struct Claims { + iss: &'static str, + sub: &'static str, + exp: usize, + nbf: usize, +} + +/// handler issuing test tokens (this is not a standard endpoint) pub async fn tokens() -> Json { let claims = Claims { - sub: "b@b.com".to_owned(), + iss: ISSUER_URI, + sub: "b@b.com", exp: 2000000000, // May 2033 + nbf: 1516239022, // Jan 2018 }; let rsa_key = EncodingKey::from_rsa_pem(include_bytes!("../../../config/jwtRS256.key")).unwrap(); @@ -150,33 +129,10 @@ pub async fn tokens() -> Json { })) } -pub async fn authorize(Json(payload): Json) -> Result, 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))) -} - -/// exposes oidc "like" endpoints (this is for test purposes) -pub fn run_server() { - // oidc "like" endpoints for test purposes +/// exposes some oidc "like" endpoints for test purposes +pub fn run_server() -> &'static str { let app = Router::new() .route("/.well-known/openid-configuration", get(discovery)) - .route("/authorize", post(authorize)) .route("/jwks", get(jwks)) .route("/tokens", get(tokens)); @@ -187,79 +143,6 @@ pub fn run_server() { }); thread::sleep(Duration::from_millis(200)); // waiting oidc to start -} - -#[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 FromRequestParts for Claims -where - S: Send + Sync, -{ - type Rejection = AuthError; - - async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - // Extract the token from the authorization header - let TypedHeader(Authorization(bearer)) = TypedHeader::>::from_request_parts(parts, state) - .await - .map_err(|_| AuthError::InvalidToken)?; - let token_data = - decode::(bearer.token(), &KEYS.decoding, &Validation::default()).map_err(|_| AuthError::InvalidToken)?; - - Ok(token_data.claims) - } + + ISSUER_URI } diff --git a/jwt-authorizer/src/error.rs b/jwt-authorizer/src/error.rs index 9596019..75538ac 100644 --- a/jwt-authorizer/src/error.rs +++ b/jwt-authorizer/src/error.rs @@ -57,7 +57,7 @@ fn response_wwwauth(status: StatusCode, bearer: &str) -> Response { let h = if bearer.is_empty() { "Bearer".to_owned() } else { - format!("Bearer {}", bearer) + format!("Bearer {bearer}") }; res.headers_mut().insert(header::WWW_AUTHENTICATE, h.parse().unwrap()); diff --git a/jwt-authorizer/src/oidc.rs b/jwt-authorizer/src/oidc.rs index bcd49cb..015fe66 100644 --- a/jwt-authorizer/src/oidc.rs +++ b/jwt-authorizer/src/oidc.rs @@ -9,7 +9,7 @@ pub struct OidcDiscovery { } pub async fn discover_jwks(issuer: &str) -> Result { - let discovery_url = format!("{}/.well-known/openid-configuration", issuer); + let discovery_url = format!("{issuer}/.well-known/openid-configuration"); reqwest::Client::new() .get(discovery_url) .send()