Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/jwtlet-server/src/exchange/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ use thiserror::Error;
pub enum ExchangeApiError {
#[error("Unsupported grant_type: {0}")]
UnsupportedGrantType(String),
#[error("Unsupported subject_token_type: {0}")]
UnsupportedTokenType(String),
#[error(transparent)]
Exchange(#[from] ExchangeError),
}
Expand All @@ -36,6 +38,7 @@ impl IntoResponse for ExchangeApiError {
fn into_response(self) -> Response {
let (status, error) = match self {
ExchangeApiError::UnsupportedGrantType(_) => (StatusCode::BAD_REQUEST, "unsupported_grant_type"),
ExchangeApiError::UnsupportedTokenType(_) => (StatusCode::BAD_REQUEST, "invalid_request"),
ExchangeApiError::Exchange(ExchangeError::TokenVerification(_)) => {
(StatusCode::BAD_REQUEST, "invalid_grant")
}
Expand Down
6 changes: 6 additions & 0 deletions crates/jwtlet-server/src/exchange/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ const ISSUED_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:jwt";
pub struct TokenExchangeForm {
grant_type: String,
subject_token: String,
/// RFC 8693 required parameter identifying the type of `subject_token`.
subject_token_type: String,
/// Identifies the participant context the caller is requesting a token for.
resource: String,
#[serde(default)]
Expand Down Expand Up @@ -67,6 +69,10 @@ pub async fn token_exchange(
return Err(ExchangeApiError::UnsupportedGrantType(form.grant_type));
}

if form.subject_token_type != ISSUED_TOKEN_TYPE {
return Err(ExchangeApiError::UnsupportedTokenType(form.subject_token_type));
}

let scopes: Vec<String> = form
.scope
.as_deref()
Expand Down
24 changes: 21 additions & 3 deletions crates/jwtlet-server/src/exchange/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

#![allow(clippy::unwrap_used)]

use crate::exchange::{TOKEN_EXCHANGE_GRANT_TYPE, TokenExchangeForm, token_exchange};
use crate::exchange::{ISSUED_TOKEN_TYPE, TOKEN_EXCHANGE_GRANT_TYPE, TokenExchangeForm, token_exchange};
use async_trait::async_trait;
use axum::body::to_bytes;
use axum::extract::{Form, State};
Expand Down Expand Up @@ -156,6 +156,17 @@ async fn exchange_token_passes_empty_scopes_when_scope_absent() {
assert_eq!(response.status(), StatusCode::OK);
}

#[tokio::test]
async fn exchange_token_returns_400_for_unsupported_subject_token_type() {
let service = make_service(ok_verifier(), ok_generator(), empty_store());
let mut f = form(None);
f.subject_token_type = "urn:ietf:params:oauth:token-type:access_token".to_string();
let response = token_exchange(State(Arc::new(service)), Form(f)).await.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
let body = json_body(response).await;
assert_eq!(body["error"], "invalid_request");
}

async fn json_body(response: axum::response::Response) -> serde_json::Value {
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
serde_json::from_slice(&bytes).unwrap()
Expand All @@ -169,6 +180,7 @@ fn form_with_audience(scope: Option<&str>, audience: Option<&str>) -> TokenExcha
TokenExchangeForm {
grant_type: TOKEN_EXCHANGE_GRANT_TYPE.to_string(),
subject_token: "input.jwt.token".to_string(),
subject_token_type: ISSUED_TOKEN_TYPE.to_string(),
resource: PARTICIPANT_CONTEXT.to_string(),
scope: scope.map(str::to_string),
audience: audience.map(str::to_string),
Expand Down Expand Up @@ -349,9 +361,15 @@ async fn exchange_token_returns_no_error_description_on_token_verification_failu
#[tokio::test]
async fn exchange_token_returns_400_for_scope_claim_conflict() {
let mut read_claims = Map::new();
read_claims.insert("role".to_string(), json!({"readOnly": true, "description": "reader description"}));
read_claims.insert(
"role".to_string(),
json!({"readOnly": true, "description": "reader description"}),
);
let mut write_claims = Map::new();
write_claims.insert("role".to_string(), json!({"readOnly": false, "description": "writer description"}));
write_claims.insert(
"role".to_string(),
json!({"readOnly": false, "description": "writer description"}),
);
let mut scope_mappings = HashMap::new();
scope_mappings.insert(
"read".to_string(),
Expand Down
6 changes: 5 additions & 1 deletion crates/jwtlet-server/src/management/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,11 @@ async fn create_scope_mapping_array_is_atomic_on_invalid_entry() {
let resp = get_scopes(&router).await;
let bytes = to_bytes(resp.into_body(), usize::MAX).await.unwrap();
let stored: Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(stored, json!([]), "no scope mapping should be persisted when the batch fails");
assert_eq!(
stored,
json!([]),
"no scope mapping should be persisted when the batch fails"
);
}

#[traced_test]
Expand Down
9 changes: 4 additions & 5 deletions crates/jwtlet-server/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ use dsdk_facet_core::jwt::{JwkSetProvider, JwtVerifier};
use jwtlet_core::resource::ResourceService;
use jwtlet_core::saccount::ServiceAccountAuthorizer;
use jwtlet_core::token::TokenExchangeService;
use opentelemetry::global;
use opentelemetry_http::HeaderExtractor;
use std::net::IpAddr;
use std::sync::Arc;
use thiserror::Error;
use tokio::net::TcpListener;
use tokio::select;
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;
use opentelemetry::global;
use opentelemetry_http::HeaderExtractor;
use tower_http::trace::TraceLayer;
use tracing::{error, info};
use tracing_opentelemetry::OpenTelemetrySpanExt;
Expand Down Expand Up @@ -168,9 +168,8 @@ async fn health() -> &'static str {
/// (`traceparent`/`tracestate`) from the inbound request headers, so jwtlet's spans become children
/// of the calling service's span rather than starting a new trace.
fn make_http_span(request: &axum::extract::Request) -> tracing::Span {
let parent_cx = global::get_text_map_propagator(|propagator| {
propagator.extract(&HeaderExtractor(request.headers()))
});
let parent_cx =
global::get_text_map_propagator(|propagator| propagator.extract(&HeaderExtractor(request.headers())));
let span = tracing::info_span!(
"http_request",
otel.kind = "server",
Expand Down
5 changes: 5 additions & 0 deletions e2e/src/tests/token_exchange.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ async fn test_token_exchange() -> anyhow::Result<()> {
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
("subject_token", sa_token.as_str()),
("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"),
("resource", PARTICIPANT_CONTEXT),
("scope", "read"),
("audience", TOKEN_AUDIENCE),
Expand Down Expand Up @@ -190,6 +191,7 @@ async fn test_token_exchange_with_scope_mapping() -> anyhow::Result<()> {
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
("subject_token", sa_token.as_str()),
("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"),
("resource", PARTICIPANT_CONTEXT),
("scope", "read write"),
("audience", TOKEN_AUDIENCE),
Expand Down Expand Up @@ -244,6 +246,7 @@ async fn test_token_exchange_audience_not_in_allowlist() -> anyhow::Result<()> {
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
("subject_token", sa_token.as_str()),
("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"),
("resource", PARTICIPANT_CONTEXT),
("scope", "read"),
("audience", "https://not-in-allowlist.example.com"),
Expand Down Expand Up @@ -271,6 +274,7 @@ async fn test_token_exchange_unauthorized() -> anyhow::Result<()> {
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
("subject_token", sa_token.as_str()),
("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"),
("resource", "no-such-context"),
("scope", "read"),
];
Expand Down Expand Up @@ -320,6 +324,7 @@ async fn test_token_jwks_verification() -> anyhow::Result<()> {
let params = [
("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange"),
("subject_token", sa_token.as_str()),
("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"),
("resource", PARTICIPANT_CONTEXT),
("scope", "read"),
];
Expand Down
Loading