Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication and authorization

The camel-auth crate validates bearer tokens, maps claims into a Principal, and evaluates authorization decisions for route-level security_policy. It is provider-neutral. OIDC presets for specific providers live in component crates such as camel-component-keycloak.

Architecture

The auth pipeline has three layers:

  1. TokenAuthenticator validates a bearer or API token and returns a Principal. Implementations include IntrospectionAuthenticator (RFC 7662), StaticTokenAuthenticator, and LocalJwtValidator.
  2. ClaimsMapper maps token or introspection claims into Principal fields: subject, roles, scopes, issuer, audience. JsonPointerClaimsMapper resolves JSON Pointer paths, so any OIDC provider works without code.
  3. PermissionEvaluator evaluates resource, action, and scope requests and returns a PermissionDecision. Route-level security_policy.permission calls it.

The enforcement boundary is SecurityPolicyLayer in camel-core. It evaluates BEFORE route steps run. A granted decision stores Principal properties on the Exchange. A denied decision returns Unauthorized into route error handling.

See ADR-0010 for the pre-pipeline authorization decision.

Native auth

The native auth pipeline reproduces a Keycloak-style flow without external dependencies. It validates static credentials against a local store and applies role-based policies. The same pipeline works with a real Keycloak through camel-component-keycloak.

Register static credentials in Camel.toml. Each [[security.native.credentials]] entry binds a subject to a credential, supplied inline (secret) or by environment-variable reference (secret_env). Roles and scopes are optional:

[security.native]
subject = "native-user"
issuer = "native"

[[security.native.credentials]]
subject = "svc-orders"
secret_env = "ORDERS_SECRET"
roles = ["service"]
scopes = ["read:orders", "write:orders"]

The CLI builds a NativeCredentialStore from these entries and wraps it in a StaticTokenAuthenticator. Each entry maps its credential to a Principal with the entry's roles and scopes.

The example defines the bearer values it presents to the authenticator:

    let alice_token = "alice-token";
    let bob_token = "bob-token";

Validate a bearer value with StaticTokenAuthenticator:

    println!("--- Validation ---");

    let alice_principal = authenticator.authenticate_bearer(alice_token).await;
    match &alice_principal {
        Ok(p) => println!("Alice OK subject={} roles={:?}", p.subject, p.roles), // allow-secret
        Err(e) => println!("Alice invalid ({e})"),                               // allow-secret
    }

    let bob_principal = authenticator.authenticate_bearer(bob_token).await;
    match &bob_principal {
        Ok(p) => println!("Bob: VALID  (subject={}, roles={:?})", p.subject, p.roles),
        Err(e) => println!("Bob: INVALID ({e})"),
    }

Apply a RolePolicy that checks for required roles:

    println!("--- Role-Based Security Policy ---");
    let admin_policy: Arc<dyn SecurityPolicy> =
        Arc::new(RolePolicy::new(vec!["admin".to_string()], true));

    let mut alice_exchange = Exchange::default();
    alice_exchange.input.headers.insert(
        "authorization".to_string(),
        Value::String(format!("Bearer {}", alice_token)), // allow-secret
    );

    let mut bob_exchange = Exchange::default();
    bob_exchange.input.headers.insert(
        "authorization".to_string(),
        Value::String(format!("Bearer {}", bob_token)), // allow-secret
    );

    let alice_principal = authenticator.authenticate_bearer(alice_token).await?;
    let alice_typed = ExamplePrincipal(alice_principal);
    let alice_auth = AuthContext {
        principal: &alice_typed,
        transport: TransportId::Http,
    };

    let bob_principal = authenticator.authenticate_bearer(bob_token).await?;
    let bob_typed = ExamplePrincipal(bob_principal);
    let bob_auth = AuthContext {
        principal: &bob_typed,
        transport: TransportId::Http,
    };

    let alice_decision = admin_policy
        .evaluate(&mut alice_exchange, &alice_auth)
        .await;
    let bob_decision = admin_policy.evaluate(&mut bob_exchange, &bob_auth).await;

    match alice_decision {
        Ok(AuthorizationDecision::Granted { principal }) => {
            println!(
                "Alice vs RolePolicy[admin]: GRANTED (subject={})",
                principal.subject
            );
        }
        Ok(AuthorizationDecision::Denied { reason, .. }) => {
            println!("Alice vs RolePolicy[admin]: DENIED ({reason})");
        }
        Err(e) => println!("Alice vs RolePolicy[admin]: ERROR ({e})"),
        _ => println!("Alice vs RolePolicy[admin]: UNKNOWN decision"),
    }

    match bob_decision {
        Ok(AuthorizationDecision::Granted { principal }) => {
            println!(
                "Bob vs RolePolicy[admin]:   GRANTED (subject={})",
                principal.subject
            );
        }
        Ok(AuthorizationDecision::Denied { reason, .. }) => {
            println!("Bob vs RolePolicy[admin]:   DENIED ({reason})");
        }
        Err(e) => println!("Bob vs RolePolicy[admin]:   ERROR ({e})"),
        _ => println!("Bob vs RolePolicy[admin]:   UNKNOWN decision"),
    }

    println!();
    println!("--- Route with Security Policy ---");

    let mut ctx = CamelContext::builder().build().await.unwrap(); // allow-unwrap
    ctx.register_component(TimerComponent::new());
    ctx.register_component(LogComponent::new());

    let role_policy = RolePolicy::new(vec!["admin".to_string()], true);
    let wrapped = BearerInjectingPolicy::new(alice_token.to_string(), role_policy);

    let secured_route = RouteBuilder::from("timer:tick?period=2000&repeatCount=2")
        .route_id("admin-only-route")
        .security_policy(SecurityPolicyConfig::new(wrapped))
        .to("log:info?showHeaders=true")
        .build()?;

    ctx.add_route_definition(secured_route).await?;
YAML equivalent for the secured route

The Rust example wraps RolePolicy in BearerInjectingPolicy to inject a static demo token. In a YAML route, the bearer token arrives in the transport Authorization header and the policy is declarative.

routes:
  - id: admin-only-route
    from: timer:tick?period=2000&repeatCount=2
    security_policy:
      roles: [admin]
      all_required: true
    steps:
      - to: log:info?showHeaders=true

YAML security_policy accepts one of roles, scopes, ref, wasm, or permission as its policy form. An optional credential_sources list declares where the credential comes from (see below). Routes with security_policy do not support the canonical hot-reload path.

Credential sources

By default, a route reads its credential from the Authorization header (ADR-0033). A browser cannot set that header on an <img src> request. Map tiles served to Leaflet, MapLibre, or OpenLayers need another transport. The credential_sources key names the extraction sources:

routes:
  - id: tile-route
    from: "http://0.0.0.0:8080/tiles"
    security_policy:
      roles: [tile-user]
      credential_sources:
        - cookie: { name: session }
        - authorization_header
    steps:
      - to: "log:info"

Each entry names one source:

FormMeaning
authorization_headerBearer token in the Authorization header
query_param: { param: <name> }Token in a query parameter
cookie: { name: <name> }Token in a cookie
header: { name: <name> }API key in a named custom header

Extraction runs in the declared order. The first source that supplies a credential wins; later sources are fallbacks. When no source supplies a credential, the request fails with 401 before policy evaluation. Store lookups run in constant time.

http:// and ws:// consumers support the key. On a ws:// route, a roles or scopes policy authenticates the token extracted from the declared sources; the removed trust_upstream_principal flag no longer exists, and exchange-property principal evidence never authorizes.

Load-time validation rejects malformed declarations: an unknown source form, an empty cookie name, or a header name that is not a valid RFC 9110 token.

Diagnostic records never render a declared credential value. The 401 reply body carries a generic reason only (ADR-0051). The operator sets SameSite=Lax (or stricter) and HttpOnly on session cookies where the cookie is issued. Cookie auth on state-changing verbs still requires CSRF defense.

See ADR-0059 for the extraction architecture.

WASM authorization policy

A WASM plugin can serve as an authorization policy. The plugin reads camel.auth.* properties from the Exchange and returns a grant or denial.

    let fixtures_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures");
    let wasm_path = fixtures_dir.join("role-check.wasm");
    let registry = Arc::new(std::sync::Mutex::new(camel_core::Registry::new()));

    // This example uses the programmatic WasmSecurityPolicy::new() API.
    // For production routes, prefer Camel.toml registration via
    // [security.policies.wasm.<name>] + YAML `security_policy: wasm: <name>`.
    // See crates/components/camel-component-wasm/README.md for details.
    // Context is constructed after policy load; observability args pass
    // None/false until then.
    let wasm_policy = WasmSecurityPolicy::new(
        &wasm_path,
        WasmConfig::default(),
        Arc::new(camel_core::RegistryComponentContext::new(
            registry, None, false,
        )),
        HashMap::new(),
    )
    .await?;

    let policy = AuthenticatedWasmPolicy::new(authenticator, alice_token.to_string(), wasm_policy);

Note: Service registration is Rust API only. YAML routes compile to the same RouteDefinition. The service wiring stays in application code.

YAML equivalent for a production route

Register the WASM policy in Camel.toml under [security.policies.wasm.<name>], then reference it by name in the route.

routes:
  - id: wasm-secured-route
    from: timer:tick?period=1000&repeatCount=5
    security_policy:
      wasm: role-check
    steps:
      - to: log:info?showHeaders=true

For production routes, prefer Camel.toml registration through [security.policies.wasm.<name>] with YAML security_policy: wasm: <name>.

See ADR-0050 for the WASM sandbox capability posture.

Security defaults

The startup-validation phase enforces fail-closed security defaults. Routes refuse to start when required configuration is missing. See ADR-0033 for the full policy.

Reference: camel-auth crate