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:
- TokenAuthenticator validates a bearer or API token and returns a
Principal. Implementations includeIntrospectionAuthenticator(RFC 7662),StaticTokenAuthenticator, andLocalJwtValidator. - ClaimsMapper maps token or introspection claims into
Principalfields: subject, roles, scopes, issuer, audience.JsonPointerClaimsMapperresolves JSON Pointer paths, so any OIDC provider works without code. - PermissionEvaluator evaluates resource, action, and scope requests and returns a
PermissionDecision. Route-levelsecurity_policy.permissioncalls 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:
| Form | Meaning |
|---|---|
authorization_header | Bearer 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