Api Key Exposure in Axum with Openid Connect

Api Key Exposure in Axum with Openid Connect — how this specific combination creates or exposes the vulnerability

When integrating OpenID Connect (OIDC) into an Axum service, developers often manage external service credentials such as API keys alongside OIDC tokens. If an API key is stored in environment variables or configuration and then inadvertently exposed through logs, error messages, or HTTP responses, the combination with OIDC identity context can amplify the impact. For example, an endpoint that authenticates a user via OIDC and then calls a downstream service using a static API key may leak the key in structured error outputs when authorization fails or upstream timeouts occur.

Consider an Axum handler that retrieves an API key from the environment and uses it to authorize requests to a third-party API after successful OIDC authentication. If the handler does not carefully control what information is included in responses or logs, the API key might be reflected in HTTP bodies or logged alongside user identity claims (sub, iss). This becomes especially risky when debugging endpoints echo partial context back to the client or when verbose errors include headers and metadata. Because OIDC tokens identify the user, exposure of an API key in this context can allow an attacker who sees or intercepts the leak to act with the permissions granted to that service account, potentially bypassing intended isolation between user contexts and service-to-service calls.

Insecure configurations can unintentionally widen the exposure surface. For instance, enabling very broad CORS rules while also forwarding API keys in client-side JavaScript or server responses increases the risk that the key is readable by unintended origins or visible to JavaScript running in the user’s browser. Similarly, if an Axum application serializes request or response data for logging and includes headers that carry authorization tokens or API keys, the key may be persisted in logs or monitoring systems accessible to more users than necessary. The presence of OIDC does not cause the leak by itself, but it creates a scenario where a leaked key can be tied to an authenticated identity, making post-incident analysis more complex and increasing the potential for misuse.

Attack patterns specific to this combination include error-based leakage where stack traces or validation messages include configuration values, and insecure logging where sensitive headers are captured at the framework level. An illustrative but simplified Axum handler that demonstrates the risk might read an API key via std::env and use it in outbound requests after confirming the user’s OIDC identity. If any panic or validation path returns the key or related metadata in the response, the key can be extracted by an authenticated user who triggers the faulty path. This is a data exposure finding that maps to common OWASP API Top 10 items around sensitive data exposure and can appear in scans as insecure handling of credentials in runtime outputs.

Tools like middleBrick detect such exposure by analyzing the unauthenticated attack surface and correlating findings with known patterns, including checks for sensitive data exposure and insecure consumption practices. The scanner does not inspect source code but observes runtime behavior, looking for places where API keys or tokens might be reflected in responses or logs when combined with authenticated contexts. For this reason, teams should treat scans that flag data exposure in authenticated flows as high priority and remediate by ensuring keys are never returned to clients and are masked in logs regardless of OIDC state.

Openid Connect-Specific Remediation in Axum — concrete code fixes

To reduce the risk of API key exposure in Axum with OIDC, apply strict separation between identity tokens and service credentials, and ensure keys are handled in server-only contexts. Use Axum extractors and middleware to control what reaches handlers, and avoid passing API keys through responses or logs. Below are focused remediation steps and code examples that demonstrate secure patterns.

1. Keep API keys server-side and do not echo them

Never include API keys in HTTP responses or error payloads. Store keys in environment variables and access them only in controlled server code. Use typed configuration structs and avoid serializing sensitive fields.

use axum::{routing::get, Router};
use std::net::SocketAddr;
use std::env;

#[tokio::main]
async fn main() {
    // Load API key from environment at startup; keep it out of handlers and responses.
    let api_key = env::var("SERVICE_API_KEY").expect("SERVICE_API_KEY must be set");
    let app = Router::new()
        .route("/health", get(health))
        // Pass key into application state if needed, but do not expose it via extractors.
        .with_state(AppState { api_key });

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

struct AppState {
    api_key: String,
}

async fn health() -> &'static str {
    "ok"
}

2. Mask sensitive values in logs and errors

Ensure that logging formats and error handlers redact API keys. Avoid passing key-containing structs directly into log macros. Instead, log only necessary metadata and use placeholders.

use tracing::{info, error};

fn log_request_user(sub: &str) {
    // Log only the identity, not the service credential.
    info!(user_sub = %sub, "authenticated request");
}

fn handle_error() -> &'static str {
    // Return a generic message to avoid leaking keys in responses.
    error!(key_masked = "****", "failed to call downstream");
    "Internal error"
}

3. Use OIDC claims for authorization, not key propagation

When calling downstream services, prefer short-lived tokens obtained via OIDC flows instead of long-lived API keys when possible. If a key must be used, keep it in server memory and attach only necessary claims to outbound requests without including the key itself.

use axum::{{
    extract::State,
    http::StatusCode,
    response::IntoResponse,
}};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};

async fn call_downstream(
    State(state): State,
    // Assume `claims` extracted from validated OIDC token.
) -> Result {
    // Validate identity claims before proceeding.
    // Do not include API key in the request sent to client-facing responses.
    let client = reqwest::Client::new();
    let res = client
        .get("https://downstream.example.com/api")
        .bearer_auth(&state.api_key) // Use key only on server side.
        .send()
        .await
        .map_err(|e| (StatusCode::BAD_GATEWAY, e.to_string()))?;

    if res.status().is_success() {
        Ok(res.text().await.unwrap_or_default())
    } else {
        Err((StatusCode::INTERNAL_SERVER_ERROR, "downstream failure".into()))
    }
}

4. Harden CORS and middleware

Configure CORS strictly and avoid reflecting sensitive headers to origins that should not see them. Use Axum middleware to strip or mask headers before logging or error handling.