From d128bac61eb3f00613bd9a05c7e553b1bfba82c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tommy=20Tr=C3=B8en?= Date: Tue, 5 Nov 2024 12:02:13 +0100 Subject: [PATCH] tests: add roundtrip tests Co-authored-by: tronghn Co-authored-by: kimtore --- src/handlers.rs | 4 +- src/main.rs | 48 +++++++-------- src/router.rs | 158 ++++++++++++++++++++++++++++++++++++++++++++++++ src/types.rs | 4 +- 4 files changed, 184 insertions(+), 30 deletions(-) create mode 100644 src/router.rs diff --git a/src/handlers.rs b/src/handlers.rs index 18e911b..0c98310 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -13,7 +13,7 @@ use axum::Json; use jsonwebtoken as jwt; use jsonwebtoken::Algorithm::RS512; use jsonwebtoken::DecodingKey; -use log::error; +use log::{error}; use std::sync::Arc; use thiserror::Error; use tokio::sync::RwLock; @@ -45,7 +45,7 @@ pub async fn token_exchange( pub async fn introspect( State(state): State, - Json(request): Json, + JsonOrForm(request): JsonOrForm, ) -> Result { // Need to decode the token to get the issuer before we actually validate it. diff --git a/src/main.rs b/src/main.rs index c48f0f5..6943ac2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,18 +3,17 @@ pub mod identity_provider; pub mod jwks; pub mod types; mod claims; +mod router; +use crate::claims::{ClientAssertion, JWTBearerAssertion}; use crate::config::Config; -use axum::routing::post; -use axum::Router; +use crate::identity_provider::{AzureADClientCredentialsTokenRequest, AzureADOnBehalfOfTokenRequest, MaskinportenTokenRequest, TokenXTokenRequest}; use clap::Parser; use dotenv::dotenv; +use identity_provider::Provider; use log::{info, LevelFilter}; use std::sync::Arc; use tokio::sync::RwLock; -use identity_provider::Provider; -use crate::claims::{ClientAssertion, JWTBearerAssertion}; -use crate::identity_provider::{AzureADClientCredentialsTokenRequest, AzureADOnBehalfOfTokenRequest, MaskinportenTokenRequest, TokenXTokenRequest}; pub mod config { use clap::Parser; @@ -93,6 +92,19 @@ async fn main() { let cfg = Config::parse(); + let state = setup_state(cfg.clone()).await; + let app = router::new(state); + + let listener = tokio::net::TcpListener::bind(cfg.bind_address) + .await + .unwrap(); + + info!("Serving on {:?}", listener.local_addr().unwrap()); + + axum::serve(listener, app).await.unwrap(); +} + +async fn setup_state(cfg: Config) -> handlers::HandlerState { // TODO: we should be able to conditionally enable certain providers based on the configuration info!("Fetch JWKS for Maskinporten..."); let maskinporten: Provider = Provider::new( @@ -100,7 +112,7 @@ async fn main() { cfg.maskinporten_client_id.clone(), cfg.maskinporten_token_endpoint.clone(), cfg.maskinporten_client_jwk.clone(), - jwks::Jwks::new(&cfg.maskinporten_issuer, &cfg.maskinporten_jwks_uri) + jwks::Jwks::new(&cfg.maskinporten_issuer.clone(), &cfg.maskinporten_jwks_uri.clone()) .await .unwrap(), ).unwrap(); @@ -112,7 +124,7 @@ async fn main() { cfg.azure_ad_client_id.clone(), cfg.azure_ad_token_endpoint.clone(), cfg.azure_ad_client_jwk.clone(), - jwks::Jwks::new(&cfg.azure_ad_issuer, &cfg.azure_ad_jwks_uri) + jwks::Jwks::new(&cfg.azure_ad_issuer.clone(), &cfg.azure_ad_jwks_uri.clone()) .await .unwrap(), ).unwrap(); @@ -123,7 +135,7 @@ async fn main() { cfg.azure_ad_client_id.clone(), cfg.azure_ad_token_endpoint.clone(), cfg.azure_ad_client_jwk.clone(), - jwks::Jwks::new(&cfg.azure_ad_issuer, &cfg.azure_ad_jwks_uri) + jwks::Jwks::new(&cfg.azure_ad_issuer.clone(), &cfg.azure_ad_jwks_uri.clone()) .await .unwrap(), ).unwrap(); @@ -134,30 +146,16 @@ async fn main() { cfg.token_x_client_id.clone(), cfg.token_x_token_endpoint.clone(), cfg.token_x_client_jwk.clone(), - jwks::Jwks::new(&cfg.token_x_issuer, &cfg.token_x_jwks_uri) + jwks::Jwks::new(&cfg.token_x_issuer.clone(), &cfg.token_x_jwks_uri.clone()) .await .unwrap(), ).unwrap(); - let state = handlers::HandlerState { + handlers::HandlerState { cfg: cfg.clone(), maskinporten: Arc::new(RwLock::new(maskinporten)), azure_ad_obo: Arc::new(RwLock::new(azure_ad_obo)), azure_ad_cc: Arc::new(RwLock::new(azure_ad_cc)), token_x: Arc::new(RwLock::new(token_x)), - }; - - let app = Router::new() - .route("/token", post(handlers::token)) - .route("/token_exchange", post(handlers::token_exchange)) - .route("/introspect", post(handlers::introspect)) - .with_state(state.clone()); - - let listener = tokio::net::TcpListener::bind(cfg.bind_address) - .await - .unwrap(); - - info!("Serving on {:?}", listener.local_addr().unwrap()); - - axum::serve(listener, app).await.unwrap(); + } } diff --git a/src/router.rs b/src/router.rs new file mode 100644 index 0000000..a82f844 --- /dev/null +++ b/src/router.rs @@ -0,0 +1,158 @@ +use crate::handlers; +use crate::handlers::{introspect, token, token_exchange}; +use axum::routing::post; +use axum::Router; + +pub fn new(state: handlers::HandlerState) -> Router { + Router::new() + .route("/token", post(token)) + .route("/token/exchange", post(token_exchange)) + .route("/introspect", post(introspect)) + .with_state(state) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use crate::config::Config; + use crate::types::{IdentityProvider, IntrospectRequest, TokenExchangeRequest, TokenRequest, TokenResponse}; + use crate::setup_state; + use log::info; + use reqwest::Response; + use serde::Serialize; + use serde_json::Value; + + // TODO: add some error case tests + + #[tokio::test] + async fn test_roundtrip() { + let cfg = setup_config(); + let listener = tokio::net::TcpListener::bind(cfg.bind_address.clone()) + .await + .unwrap(); + let state = setup_state(cfg.clone()).await; + let app = super::new(state); + let address = listener.local_addr().unwrap(); + info!("Serving on {:?}", address.clone()); + let join_handler = tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + machine_to_machine_token(cfg.maskinporten_issuer.clone(),address.to_string(), IdentityProvider::Maskinporten).await; + machine_to_machine_token(cfg.azure_ad_issuer.clone(), address.to_string(), IdentityProvider::AzureAD).await; + + token_exchange_token(cfg.azure_ad_issuer.clone(), address.to_string(), IdentityProvider::AzureAD).await; + token_exchange_token(cfg.token_x_issuer.clone(), address.to_string(), IdentityProvider::TokenX).await; + + join_handler.abort(); + } + + async fn machine_to_machine_token(expected_issuer: String, address: String, identity_provider: IdentityProvider) { + let params = TokenRequest { + target: "mytarget".to_string(), + identity_provider, + }; + + let response = do_post(format!("http://{}/token", address.clone().to_string()), params).await; + + assert_eq!(response.status(), 200); + + let body: TokenResponse = response.json().await.unwrap(); + assert!(body.expires_in_seconds > 0); + assert!(!body.access_token.is_empty()); + + let params = IntrospectRequest { + token: body.access_token.clone(), + }; + + let response = do_post(format!("http://{}/introspect", address.clone().to_string()), params).await; + + assert_eq!(response.status(), 200); + let body: HashMap = response.json().await.unwrap(); + assert_eq!(body["active"], Value::Bool(true)); + assert_eq!(body["iss"], Value::String(expected_issuer.to_string())); + } + + async fn token_exchange_token(expected_issuer: String, address: String, identity_provider: IdentityProvider) { + #[derive(Serialize)] + struct AuthorizeRequest { + grant_type: String, + code: String, + client_id: String, + client_secret: String, + } + + let params = AuthorizeRequest { + grant_type: "authorization_code".to_string(), + code: "mycode".to_string(), + client_id: "myclientid".to_string(), + client_secret: "myclientsecret".to_string(), + }; + + let user_token_response = do_post("http://localhost:8080/token".to_string(), params ).await; + assert_eq!(user_token_response.status(), 200); + let user_token: TokenResponse = user_token_response.json().await.unwrap(); + + let params = TokenExchangeRequest { + target: "mytarget".to_string(), + identity_provider, + user_token: user_token.access_token, + }; + + let response = do_post(format!("http://{}/token/exchange", address.clone().to_string()), params).await; + + if response.status() != 200 { + let body = response.text().await.unwrap(); + panic!("Failed to exchange token: {:?}", body); + } + assert_eq!(response.status(), 200); + + let body: TokenResponse = response.json().await.unwrap(); + assert!(body.expires_in_seconds > 0); + assert!(!body.access_token.is_empty()); + + let params = IntrospectRequest { + token: body.access_token.clone(), + }; + + let response = do_post(format!("http://{}/introspect", address.clone().to_string()), params).await; + + assert_eq!(response.status(), 200); + let body: HashMap = response.json().await.unwrap(); + assert_eq!(body["active"], Value::Bool(true)); + assert_eq!(body["iss"], Value::String(expected_issuer.to_string())); + assert!(!body["sub"].to_string().is_empty()); + } + + fn setup_config() -> Config { + Config { + bind_address: "127.0.0.1:0".to_string(), + maskinporten_client_id: "client-id".to_string(), + maskinporten_client_jwk: r#"{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW047jOYFEGvIdMkTRLGOBig6fLWzgd62lnLainzV35J6K6zr4jQfTldLondlkldMR6nQrp1KfnNUuRbKvzpNKkhl12-f1l91l0tCx3s4blztvWgdzN2xBfvWV68","kty":"RSA","q":"9MIWsbIA3WjiR_Ful5FM8NCgb6JdS2D6ySHVepoNI-iAPilcltF_J2orjfLqAxeztTskPi45wtF_-eV4GIYSzvMo-gFiXLMrvEa7WaWizMi_7Bu9tEk3m_f3IDLN9lwULYoebkDbiXx6GOiuj0VkuKz8ckYFNKLCMP9QRLFff-0","d":"J6UX848X8tNz-09PFvcFDUVqak32GXzoPjnuDjBsxNUvG7LxenLmM_i8tvYl0EW9Ztn4AiCqJUoHw5cX3jz_mSqGl7ciaDedpKm_AetcZwHiEuT1EpSKRPMmOMQSqcJqXrdbbWB8gdUrnTKZIlJCfj7yqgT16ypC43TnwjA0UwxhG5pHaYjKI3pPdoHg2BzA-iubHjVn15Sz7-pnjBmeGDbEFa7ADY-1yPHCmqqvPKTNhoCNW6RpG34Id9hXslPa3X-7pAhJrDBd0_NPlktSA2rUkifYiZURhHR5ijhe0v3uw6kYP8f_foVm_C8O1ExkxXh9Dg8KDZ89dbsSOtBc0Q","e":"AQAB","use":"sig","kid":"l7C_WJgbZ_6e59vPrFETAehX7Dsp7fIyvSV4XhotsGs","qi":"cQFN5q5WhYkzgd1RS0rGqvpX1AkmZMrLv2MW04gSfu0dDwpbsSAu8EUCQW9oA4pr6V7R9CBSu9kdN2iY5SR-hZvEad5nDKPV1F3TMQYv5KpRiS_0XhfV5PcolUJVO_4p3h8d-mo2hh1Sw2fairAKOzvnwJCQ6DFkiY7H1cqwA54","dp":"YTql9AGtvyy158gh7jeXcgmySEbHQzvDFulDr-IXIg8kjHGEbp0rTIs0Z50RA95aC5RFkRjpaBKBfvaySjDm5WIi6GLzntpp6B8l7H6qG1jVO_la4Df2kzjx8LVvY8fhOrKz_hDdHodUeKdCF3RdvWMr00ruLnJhBPJHqoW7cwE","alg":"RS256","dq":"IZA4AngRbEtEtG7kJn6zWVaSmZxfRMXwvgIYvy4-3Qy2AVA0tS3XTPVfMaD8_B2U9CY_CxPVseR-sysHc_12uNBZbycfcOzU84WTjXCMSZ7BysPnGMDtkkLHra-p1L29upz1HVNhh5H9QEswHM98R2LZX2ZAsn4bORLZ1AGqweU","n":"8ZqUp5Cs90XpNn8tJBdUUxdGH4bjqKjFj8lyB3x50RpTuECuwzX1NpVqyFENDiEtMja5fdmJl6SErjnhj6kbhcmfmFibANuG-0WlV5yMysdSbocd75C1JQbiPdpHdXrijmVFMfDnoZTQ-ErNsqqngTNkn5SXBcPenli6Cf9MTSchZuh_qFj_B7Fp3CWKehTiyBcLlNOIjYsXX8WQjZkWKGpQ23AWjZulngWRektLcRWuEKTWaRBtbAr3XAfSmcqTICrebaD3IMWKHDtvzHAt_pt4wnZ06clgeO2Wbc980usnpsF7g8k9p81RcbS4JEZmuuA9NCmOmbyADXwgA9_-Aw"}"#.to_string(), + maskinporten_jwks_uri: "http://localhost:8080/maskinporten/jwks".to_string(), + maskinporten_issuer: "http://localhost:8080/maskinporten".to_string(), + maskinporten_token_endpoint: "http://localhost:8080/maskinporten/token".to_string(), + azure_ad_client_id: "client-id".to_string(), + azure_ad_client_jwk: r#"{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW047jOYFEGvIdMkTRLGOBig6fLWzgd62lnLainzV35J6K6zr4jQfTldLondlkldMR6nQrp1KfnNUuRbKvzpNKkhl12-f1l91l0tCx3s4blztvWgdzN2xBfvWV68","kty":"RSA","q":"9MIWsbIA3WjiR_Ful5FM8NCgb6JdS2D6ySHVepoNI-iAPilcltF_J2orjfLqAxeztTskPi45wtF_-eV4GIYSzvMo-gFiXLMrvEa7WaWizMi_7Bu9tEk3m_f3IDLN9lwULYoebkDbiXx6GOiuj0VkuKz8ckYFNKLCMP9QRLFff-0","d":"J6UX848X8tNz-09PFvcFDUVqak32GXzoPjnuDjBsxNUvG7LxenLmM_i8tvYl0EW9Ztn4AiCqJUoHw5cX3jz_mSqGl7ciaDedpKm_AetcZwHiEuT1EpSKRPMmOMQSqcJqXrdbbWB8gdUrnTKZIlJCfj7yqgT16ypC43TnwjA0UwxhG5pHaYjKI3pPdoHg2BzA-iubHjVn15Sz7-pnjBmeGDbEFa7ADY-1yPHCmqqvPKTNhoCNW6RpG34Id9hXslPa3X-7pAhJrDBd0_NPlktSA2rUkifYiZURhHR5ijhe0v3uw6kYP8f_foVm_C8O1ExkxXh9Dg8KDZ89dbsSOtBc0Q","e":"AQAB","use":"sig","kid":"l7C_WJgbZ_6e59vPrFETAehX7Dsp7fIyvSV4XhotsGs","qi":"cQFN5q5WhYkzgd1RS0rGqvpX1AkmZMrLv2MW04gSfu0dDwpbsSAu8EUCQW9oA4pr6V7R9CBSu9kdN2iY5SR-hZvEad5nDKPV1F3TMQYv5KpRiS_0XhfV5PcolUJVO_4p3h8d-mo2hh1Sw2fairAKOzvnwJCQ6DFkiY7H1cqwA54","dp":"YTql9AGtvyy158gh7jeXcgmySEbHQzvDFulDr-IXIg8kjHGEbp0rTIs0Z50RA95aC5RFkRjpaBKBfvaySjDm5WIi6GLzntpp6B8l7H6qG1jVO_la4Df2kzjx8LVvY8fhOrKz_hDdHodUeKdCF3RdvWMr00ruLnJhBPJHqoW7cwE","alg":"RS256","dq":"IZA4AngRbEtEtG7kJn6zWVaSmZxfRMXwvgIYvy4-3Qy2AVA0tS3XTPVfMaD8_B2U9CY_CxPVseR-sysHc_12uNBZbycfcOzU84WTjXCMSZ7BysPnGMDtkkLHra-p1L29upz1HVNhh5H9QEswHM98R2LZX2ZAsn4bORLZ1AGqweU","n":"8ZqUp5Cs90XpNn8tJBdUUxdGH4bjqKjFj8lyB3x50RpTuECuwzX1NpVqyFENDiEtMja5fdmJl6SErjnhj6kbhcmfmFibANuG-0WlV5yMysdSbocd75C1JQbiPdpHdXrijmVFMfDnoZTQ-ErNsqqngTNkn5SXBcPenli6Cf9MTSchZuh_qFj_B7Fp3CWKehTiyBcLlNOIjYsXX8WQjZkWKGpQ23AWjZulngWRektLcRWuEKTWaRBtbAr3XAfSmcqTICrebaD3IMWKHDtvzHAt_pt4wnZ06clgeO2Wbc980usnpsF7g8k9p81RcbS4JEZmuuA9NCmOmbyADXwgA9_-Aw"}"#.to_string(), + azure_ad_jwks_uri: "http://localhost:8080/azuread/jwks".to_string(), + azure_ad_issuer: "http://localhost:8080/azuread".to_string(), + azure_ad_token_endpoint: "http://localhost:8080/azuread/token".to_string(), + token_x_client_id: "client-id".to_string(), + token_x_client_jwk: r#"{"p":"_LNnIjBshCrFuxtjUC2KKzg_NTVv26UZh5j12_9r5mYTxb8yW047jOYFEGvIdMkTRLGOBig6fLWzgd62lnLainzV35J6K6zr4jQfTldLondlkldMR6nQrp1KfnNUuRbKvzpNKkhl12-f1l91l0tCx3s4blztvWgdzN2xBfvWV68","kty":"RSA","q":"9MIWsbIA3WjiR_Ful5FM8NCgb6JdS2D6ySHVepoNI-iAPilcltF_J2orjfLqAxeztTskPi45wtF_-eV4GIYSzvMo-gFiXLMrvEa7WaWizMi_7Bu9tEk3m_f3IDLN9lwULYoebkDbiXx6GOiuj0VkuKz8ckYFNKLCMP9QRLFff-0","d":"J6UX848X8tNz-09PFvcFDUVqak32GXzoPjnuDjBsxNUvG7LxenLmM_i8tvYl0EW9Ztn4AiCqJUoHw5cX3jz_mSqGl7ciaDedpKm_AetcZwHiEuT1EpSKRPMmOMQSqcJqXrdbbWB8gdUrnTKZIlJCfj7yqgT16ypC43TnwjA0UwxhG5pHaYjKI3pPdoHg2BzA-iubHjVn15Sz7-pnjBmeGDbEFa7ADY-1yPHCmqqvPKTNhoCNW6RpG34Id9hXslPa3X-7pAhJrDBd0_NPlktSA2rUkifYiZURhHR5ijhe0v3uw6kYP8f_foVm_C8O1ExkxXh9Dg8KDZ89dbsSOtBc0Q","e":"AQAB","use":"sig","kid":"l7C_WJgbZ_6e59vPrFETAehX7Dsp7fIyvSV4XhotsGs","qi":"cQFN5q5WhYkzgd1RS0rGqvpX1AkmZMrLv2MW04gSfu0dDwpbsSAu8EUCQW9oA4pr6V7R9CBSu9kdN2iY5SR-hZvEad5nDKPV1F3TMQYv5KpRiS_0XhfV5PcolUJVO_4p3h8d-mo2hh1Sw2fairAKOzvnwJCQ6DFkiY7H1cqwA54","dp":"YTql9AGtvyy158gh7jeXcgmySEbHQzvDFulDr-IXIg8kjHGEbp0rTIs0Z50RA95aC5RFkRjpaBKBfvaySjDm5WIi6GLzntpp6B8l7H6qG1jVO_la4Df2kzjx8LVvY8fhOrKz_hDdHodUeKdCF3RdvWMr00ruLnJhBPJHqoW7cwE","alg":"RS256","dq":"IZA4AngRbEtEtG7kJn6zWVaSmZxfRMXwvgIYvy4-3Qy2AVA0tS3XTPVfMaD8_B2U9CY_CxPVseR-sysHc_12uNBZbycfcOzU84WTjXCMSZ7BysPnGMDtkkLHra-p1L29upz1HVNhh5H9QEswHM98R2LZX2ZAsn4bORLZ1AGqweU","n":"8ZqUp5Cs90XpNn8tJBdUUxdGH4bjqKjFj8lyB3x50RpTuECuwzX1NpVqyFENDiEtMja5fdmJl6SErjnhj6kbhcmfmFibANuG-0WlV5yMysdSbocd75C1JQbiPdpHdXrijmVFMfDnoZTQ-ErNsqqngTNkn5SXBcPenli6Cf9MTSchZuh_qFj_B7Fp3CWKehTiyBcLlNOIjYsXX8WQjZkWKGpQ23AWjZulngWRektLcRWuEKTWaRBtbAr3XAfSmcqTICrebaD3IMWKHDtvzHAt_pt4wnZ06clgeO2Wbc980usnpsF7g8k9p81RcbS4JEZmuuA9NCmOmbyADXwgA9_-Aw"}"#.to_string(), + token_x_jwks_uri: "http://localhost:8080/tokenx/jwks".to_string(), + token_x_issuer: "http://localhost:8080/tokenx".to_string(), + token_x_token_endpoint: "http://localhost:8080/tokenx/token".to_string(), + } + } + + async fn do_post(url: String, params: impl Serialize) -> Response { + let client = reqwest::Client::new(); + client + .post(url) + .header("accept", "application/json") + .form(¶ms) + .send() + .await + .unwrap() + } +} diff --git a/src/types.rs b/src/types.rs index c8d0a3a..53ff1a6 100644 --- a/src/types.rs +++ b/src/types.rs @@ -37,8 +37,6 @@ pub enum TokenType { pub struct TokenRequest { pub target: String, // typically :: pub identity_provider: IdentityProvider, - #[serde(skip_serializing_if = "Option::is_none")] - pub force: Option, } /// This is a token exchange request that comes from the application we are serving. @@ -49,7 +47,7 @@ pub struct TokenExchangeRequest { pub user_token: String, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] pub struct IntrospectRequest { pub token: String, }