Skip to content

Commit

Permalink
refactor: add identity_provider to IntrospectRequest
Browse files Browse the repository at this point in the history
* add more tests
* change error message for KeyNotInJWKS

Co-authored-by: tronghn <[email protected]>
Co-authored-by: kimtore <[email protected]>
  • Loading branch information
3 people committed Nov 14, 2024
1 parent 7ca8f76 commit 2d362bb
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 73 deletions.
2 changes: 1 addition & 1 deletion hack/roundtrip-azure-cc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
response=$(curl -s -X POST http://localhost:3000/api/v1/token -H "content-type: application/json" -d '{"target": "client-id", "identity_provider": "azuread"}')
token=$(echo ${response} | jq -r .access_token)

validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\"}")
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\", \"identity_provider\": \"azuread\"}")

echo
echo "JWT:"
Expand Down
2 changes: 1 addition & 1 deletion hack/roundtrip-azure-obo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ user_token=$(echo ${user_token_response} | jq -r .access_token)
response=$(curl -s -X POST http://localhost:3000/api/v1/token/exchange -H "content-type: application/json" -d '{"target": "client-id", "identity_provider": "azuread", "user_token": "'${user_token}'"}')
token=$(echo ${response} | jq -r .access_token)

validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\"}")
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\", \"identity_provider\": \"azuread\"}")

echo
echo "User token:"
Expand Down
2 changes: 1 addition & 1 deletion hack/roundtrip-idporten.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
user_token_response=$(curl -s -X POST http://localhost:8080/idporten/token -d "grant_type=authorization_code&code=yolo&client_id=yolo&client_secret=bolo")
user_token=$(echo ${user_token_response} | jq -r .access_token)

validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${user_token}\"}")
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${user_token}\", \"identity_provider\": \"idporten\"}")

echo
echo "User token:"
Expand Down
2 changes: 1 addition & 1 deletion hack/roundtrip-maskinporten.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/bash -e
response=$(curl -s -X POST http://localhost:3000/api/v1/token -H "content-type: application/json" -d '{"target": "my-target", "identity_provider": "maskinporten"}')
token=$(echo ${response} | jq -r .access_token)
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\"}")
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\", \"identity_provider\": \"maskinporten\"}")

echo
echo "JWT:"
Expand Down
2 changes: 1 addition & 1 deletion hack/roundtrip-tokenx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ user_token=$(echo ${user_token_response} | jq -r .access_token)
response=$(curl -s -X POST http://localhost:3000/api/v1/token/exchange -H "content-type: application/json" -d '{"target": "client-id", "identity_provider": "tokenx", "user_token": "'${user_token}'"}')
token=$(echo ${response} | jq -r .access_token)

validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\"}")
validation=$(curl -s -X POST http://localhost:3000/api/v1/introspect -H "content-type: application/json" -d "{\"token\": \"${token}\", \"identity_provider\": \"tokenx\"}")

echo
echo "User token:"
Expand Down
111 changes: 89 additions & 22 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use std::time::Duration;
use axum::body::Bytes;
use axum::extract::MatchedPath;
use axum::http::{HeaderMap, Request};
use axum::response::Response;
use crate::config::Config;
use crate::handlers::__path_introspect;
use crate::handlers::__path_token;
use crate::handlers::__path_token_exchange;
use crate::handlers::{introspect, token, token_exchange, HandlerState};
use axum::body::Bytes;
use axum::extract::MatchedPath;
use axum::http::{HeaderMap, Request};
use axum::response::Response;
use axum::Router;
use log::info;
use opentelemetry::propagation::TextMapPropagator;
use opentelemetry_http::HeaderExtractor;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use std::time::Duration;
use tokio::net::TcpListener;
use tower_http::classify::ServerErrorsFailureClass;
use tower_http::trace::TraceLayer;
Expand Down Expand Up @@ -207,6 +207,7 @@ mod tests {
test_introspect_token_is_not_a_jwt(&address).await;
test_introspect_token_missing_issuer(&address).await;
test_introspect_token_unrecognized_issuer(&address).await;
test_introspect_token_issuer_mismatch(&address, &identity_provider_address).await;
test_introspect_token_missing_kid(&address, &identity_provider_address).await;
test_introspect_token_missing_key_in_jwks(&address, &identity_provider_address).await;
test_introspect_token_is_expired(&address, &identity_provider_address).await;
Expand Down Expand Up @@ -246,22 +247,62 @@ mod tests {
}

async fn test_introspect_token_unrecognized_issuer(address: &str) {
let token = Token::sign(TokenClaims::from([("iss".into(), Value::String("snafu".into()))]));
let token = Token::sign_with_kid(
TokenClaims::from([
("iss".into(), Value::String("snafu".into())),
("nbf".into(), (epoch_now_secs()).into()),
("iat".into(), (epoch_now_secs()).into()),
("exp".into(), (epoch_now_secs() + 120).into()),
]),
&IdentityProvider::Maskinporten.to_string(),
);
test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectResponse::new_invalid("unrecognized issuer: 'snafu'"),
IntrospectRequest {
token,
identity_provider: IdentityProvider::Maskinporten,
},
IntrospectResponse::new_invalid("invalid token: InvalidIssuer"),
StatusCode::OK,
)
.await;
}

async fn test_introspect_token_issuer_mismatch(address: &str, identity_provider_address: &str) {
let iss = format!("http://{}/maskinporten", identity_provider_address);
let token = Token::sign_with_kid(TokenClaims::from([
("iss".into(), Value::String(iss)),
("nbf".into(), (epoch_now_secs()).into()),
("iat".into(), (epoch_now_secs()).into()),
("exp".into(), (epoch_now_secs() + 120).into()),
]), &IdentityProvider::Maskinporten.to_string());

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest {
token,
identity_provider: IdentityProvider::AzureAD,
},
IntrospectResponse::new_invalid("token can not be validated with this identity provider"),
StatusCode::OK,
)
.await;
}

async fn test_introspect_token_missing_issuer(address: &str) {
let token = Token::sign(TokenClaims::new());
let token = Token::sign_with_kid(TokenClaims::from([
("nbf".into(), (epoch_now_secs()).into()),
("iat".into(), (epoch_now_secs()).into()),
("exp".into(), (epoch_now_secs() + 120).into()),
]), &IdentityProvider::Maskinporten.to_string());

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectResponse::new_invalid("token is invalid"),
IntrospectRequest {
token,
identity_provider: IdentityProvider::Maskinporten,
},
IntrospectResponse::new_invalid("invalid token: Missing required claim: iss"),
StatusCode::OK,
)
.await;
Expand All @@ -271,9 +312,10 @@ mod tests {
test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest {
token: "this is not a token".to_string(),
token: "not a jwt".to_string(),
identity_provider: IdentityProvider::AzureAD,
},
IntrospectResponse::new_invalid("token is invalid"),
IntrospectResponse::new_invalid("invalid token header: InvalidToken"),
StatusCode::OK,
)
.await;
Expand All @@ -283,7 +325,10 @@ mod tests {
let token = Token::sign(TokenClaims::from([("iss".into(), format!("http://{}/maskinporten", identity_provider_address).into())]));
test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectRequest {
token,
identity_provider: IdentityProvider::AzureAD,
},
IntrospectResponse::new_invalid("missing key id from token header"),
StatusCode::OK,
)
Expand All @@ -295,8 +340,11 @@ mod tests {

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectResponse::new_invalid("signing key with missing-key not in json web key set"),
IntrospectRequest {
token,
identity_provider: IdentityProvider::Maskinporten,
},
IntrospectResponse::new_invalid("token can not be validated with this identity provider"),
StatusCode::OK,
)
.await;
Expand All @@ -316,7 +364,10 @@ mod tests {

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectRequest {
token,
identity_provider: IdentityProvider::Maskinporten,
},
IntrospectResponse::new_invalid("invalid token: ExpiredSignature"),
StatusCode::OK,
)
Expand All @@ -336,7 +387,10 @@ mod tests {

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectRequest {
token,
identity_provider: IdentityProvider::Maskinporten,
},
IntrospectResponse::new_invalid("invalid token: ImmatureSignature"),
StatusCode::OK,
)
Expand All @@ -356,7 +410,10 @@ mod tests {

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token },
IntrospectRequest {
token,
identity_provider: IdentityProvider::Maskinporten,
},
IntrospectResponse::new_invalid("invalid token: ImmatureSignature"),
StatusCode::OK,
)
Expand All @@ -383,7 +440,10 @@ mod tests {

test_well_formed_json_request(
&format!("http://{}/api/v1/introspect", address),
IntrospectRequest { token: body.access_token.clone() },
IntrospectRequest {
token: body.access_token.clone(),
identity_provider: IdentityProvider::AzureAD,
},
IntrospectResponse::new_invalid("invalid token: InvalidAudience"),
StatusCode::OK,
)
Expand Down Expand Up @@ -470,7 +530,10 @@ mod tests {

let response = post_request(
format!("http://{}/api/v1/introspect", address.clone().to_string()),
IntrospectRequest { token: body.access_token.clone() },
IntrospectRequest {
token: body.access_token.clone(),
identity_provider,
},
request_format,
)
.await
Expand Down Expand Up @@ -532,7 +595,10 @@ mod tests {

let response = post_request(
format!("http://{}/api/v1/introspect", address.clone().to_string()),
IntrospectRequest { token: body.access_token.clone() },
IntrospectRequest {
token: body.access_token.clone(),
identity_provider,
},
request_format,
)
.await
Expand Down Expand Up @@ -575,6 +641,7 @@ mod tests {
format!("http://{}/api/v1/introspect", address.clone().to_string()),
IntrospectRequest {
token: user_token.access_token.clone(),
identity_provider: IdentityProvider::IDPorten,
},
request_format,
)
Expand Down
4 changes: 1 addition & 3 deletions src/claims.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ impl Assertion for ClientAssertion {
}

impl Assertion for () {
fn new(_token_endpoint: String, _client_id: String, _target: String) -> Self {
()
}
fn new(_token_endpoint: String, _client_id: String, _target: String) -> Self {}
}

pub fn epoch_now_secs() -> u64 {
Expand Down
10 changes: 5 additions & 5 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ pub async fn token_exchange(State(state): State<HandlerState>, JsonOrForm(reques
description = "Introspect a token. This means to validate the token and returns its claims. The `active` is not part of the claims, but indicates whether the token is valid.",
examples(
("Token introspection" = (value = json!(IntrospectRequest{
token: "eyJraWQiOiJ0b2tlbngiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJlMDE1NTQyYy0wZjgxLTQwZjUtYmJkOS03YzNkOTM2NjI5OGYiLCJhdWQiOiJteS10YXJnZXQiLCJuYmYiOjE3MzA5NzcyOTMsImF6cCI6InlvbG8iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdG9rZW54IiwiZXhwIjoxNzMwOTgwODkzLCJpYXQiOjE3MzA5NzcyOTMsImp0aSI6ImU3Y2JhZGMzLTZiZGEtNDljMC1hMTk2LWM0NzMyOGRhODgwZSIsInRpZCI6InRva2VueCJ9.SIme9o5YE6pZXT9IMAx5upV3V4ww_TnDlqZG203pkySPBd_VqNGBXzOKHeOasIDpXEMlf8Yc-1nKgySjGOT3c46PIHEUrhQFXF6s9OpJAYAwy7L2n2DIFfEOLt8EpwSpM5hWDwnGpSdvebWlmoaA3ImFEB5dtnxLrVG-7dYEEzZjMfBOKFWrPp03FTO4qKOJUqCZR0tmZRmcPzymPWFIMjP2FTj6iz9zai93dhQmdvNVMGL9HBXF6ewKf_CTlUIx9XpwI2M-dhlyH2PIxyhix7Amuff_mHuEHTuCAFqMfjon-F438uyZmgicyrvhoUGxV8W1PfZEiLIv0RBeWRJ9gw".to_string()
token: "eyJraWQiOiJ0b2tlbngiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJlMDE1NTQyYy0wZjgxLTQwZjUtYmJkOS03YzNkOTM2NjI5OGYiLCJhdWQiOiJteS10YXJnZXQiLCJuYmYiOjE3MzA5NzcyOTMsImF6cCI6InlvbG8iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdG9rZW54IiwiZXhwIjoxNzMwOTgwODkzLCJpYXQiOjE3MzA5NzcyOTMsImp0aSI6ImU3Y2JhZGMzLTZiZGEtNDljMC1hMTk2LWM0NzMyOGRhODgwZSIsInRpZCI6InRva2VueCJ9.SIme9o5YE6pZXT9IMAx5upV3V4ww_TnDlqZG203pkySPBd_VqNGBXzOKHeOasIDpXEMlf8Yc-1nKgySjGOT3c46PIHEUrhQFXF6s9OpJAYAwy7L2n2DIFfEOLt8EpwSpM5hWDwnGpSdvebWlmoaA3ImFEB5dtnxLrVG-7dYEEzZjMfBOKFWrPp03FTO4qKOJUqCZR0tmZRmcPzymPWFIMjP2FTj6iz9zai93dhQmdvNVMGL9HBXF6ewKf_CTlUIx9XpwI2M-dhlyH2PIxyhix7Amuff_mHuEHTuCAFqMfjon-F438uyZmgicyrvhoUGxV8W1PfZEiLIv0RBeWRJ9gw".to_string(),
identity_provider: IdentityProvider::TokenX,
})))
),
),
Expand Down Expand Up @@ -194,13 +195,12 @@ where
Ok(Arc::new(RwLock::new(Box::new(
Provider::<R, A>::new(
kind,
provider_cfg.issuer.clone(),
provider_cfg.client_id.clone(),
provider_cfg.token_endpoint.clone(),
provider_cfg.client_jwk.clone(),
jwks::Jwks::new(&provider_cfg.issuer.clone(), &provider_cfg.jwks_uri.clone(), audience).await?,
)
.ok_or(InitError::Jwk)?,
.ok_or(InitError::Jwk)?,
))))
}

Expand Down Expand Up @@ -288,8 +288,8 @@ impl<S, T> FromRequest<S> for JsonOrForm<T>
where
S: Send + Sync,
T: 'static,
Json<T>: FromRequest<S, Rejection = JsonRejection>,
Form<T>: FromRequest<S, Rejection = FormRejection>,
Json<T>: FromRequest<S, Rejection=JsonRejection>,
Form<T>: FromRequest<S, Rejection=FormRejection>,
{
type Rejection = ApiError;

Expand Down
Loading

0 comments on commit 2d362bb

Please sign in to comment.