Skip to content

Commit

Permalink
tests: add roundtrip tests
Browse files Browse the repository at this point in the history
Co-authored-by: tronghn <trong.huu.nguyen@nav.no>
Co-authored-by: kimtore <kim.tore.jensen@nav.no>
  • Loading branch information
3 people committed Nov 5, 2024
1 parent c3f7043 commit d128bac
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 30 deletions.
4 changes: 2 additions & 2 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -45,7 +45,7 @@ pub async fn token_exchange(

pub async fn introspect(
State(state): State<HandlerState>,
Json(request): Json<IntrospectRequest>,
JsonOrForm(request): JsonOrForm<IntrospectRequest>,
) -> Result<impl IntoResponse, ApiError> {

// Need to decode the token to get the issuer before we actually validate it.
Expand Down
48 changes: 23 additions & 25 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -93,14 +92,27 @@ 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<MaskinportenTokenRequest, JWTBearerAssertion> = Provider::new(
cfg.maskinporten_issuer.clone(),
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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
}
}
158 changes: 158 additions & 0 deletions src/router.rs
Original file line number Diff line number Diff line change
@@ -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<String, Value> = 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<String, Value> = 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(&params)
.send()
.await
.unwrap()
}
}
4 changes: 1 addition & 3 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ pub enum TokenType {
pub struct TokenRequest {
pub target: String, // typically <cluster>:<namespace>:<app>
pub identity_provider: IdentityProvider,
#[serde(skip_serializing_if = "Option::is_none")]
pub force: Option<bool>,
}

/// This is a token exchange request that comes from the application we are serving.
Expand All @@ -49,7 +47,7 @@ pub struct TokenExchangeRequest {
pub user_token: String,
}

#[derive(Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct IntrospectRequest {
pub token: String,
}
Expand Down

0 comments on commit d128bac

Please sign in to comment.