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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
# Runs each test in its own process, so a hang names the test that hung
# instead of timing the whole job out. Third-party action: pinned to a
# commit SHA (dependabot keeps it fresh).
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2
- uses: taiki-e/install-action@68a389c788b2cd04016ce8c80ee23b80d2ab3d68 # v2
with:
tool: cargo-nextest

Expand Down Expand Up @@ -165,7 +165,7 @@ jobs:
with:
key: ${{ matrix.target }}
# Third-party action: pinned to a commit SHA (dependabot keeps it fresh).
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2
- uses: taiki-e/install-action@68a389c788b2cd04016ce8c80ee23b80d2ab3d68 # v2
with:
tool: cargo-nextest

Expand Down Expand Up @@ -204,7 +204,7 @@ jobs:
# deny.toml. The released binary rather than the Docker action, whose
# image build pulls from Docker Hub and hits its anonymous rate limit
# on shared runners.
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2
- uses: taiki-e/install-action@68a389c788b2cd04016ce8c80ee23b80d2ab3d68 # v2
with:
tool: cargo-deny
- name: Check advisories
Expand Down
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ tonic services.
capabilities you ask for ([Building the proxy in code](#building-the-proxy-in-code))
- Requests the proxy does not serve go to your own fallback service
- Hooks for auth decisions, token verification, OIDC and extra routes, with no
HTTP framework in your code
HTTP framework in your code. The auth hooks are for applications that leave
authentication to the proxy and an external identity provider
([Applications with their own authentication](#applications-with-their-own-authentication))

## Non-goals

Expand Down Expand Up @@ -973,6 +975,22 @@ returns `application/jwk-set+json` (RFC 7517 §8.5).
command-line dependencies come with the `cli` feature. The library runs on
your tokio runtime and logs through `tracing` to the subscriber you set up.

### Applications with their own authentication

The proxy's auth (JWT verification, route policies and roles, claim→header
forwarding, the forward-auth endpoint, ext_authz and the `with_auth`,
`with_token_verifier` and `with_auth_decider` hooks) is for an application
that leaves authentication and authorization to the proxy and an external
identity provider. An application that verifies tokens and authorizes requests
itself needs none of it for the requests that reach its service: no `auth:`
section, and the proxy installs no `jsonwebtoken` provider in its process.

Those checks cover only what reaches the service. The proxy's own routes
(health, metrics, OpenAPI, OIDC, extra routes) never do, so each needs a guard
of its own or must stay off: `with_auth_decider` with a
`with_auth_decider_scope` that takes in `endpoints` is the proxy-side option
(see [Guards and scopes](#guards-and-scopes)).

```rust,no_run
use std::path::Path;
use structured_proxy::ProxyServer;
Expand Down Expand Up @@ -1343,12 +1361,15 @@ from `auth.jwt` (an Ed25519 PEM file or a JWKS endpoint), verified with
| `aws_lc_rs` | aws-lc | Constant-time / FIPS-capable, advisory-free, links aws-lc through C FFI. |

Both can be linked at once, as with any pair of Cargo features. `jsonwebtoken`
cannot infer a provider then, so `ProxyServer::from_config` installs `aws_lc_rs`
for the process: it is constant-time and carries no advisory. A build that wants
RustCrypto regardless drops the `aws_lc_rs` feature.
cannot infer a provider then, so building the built-in verifier installs
`aws_lc_rs` for the process, unless the process installed a provider first: it
is constant-time and carries no advisory. A build that wants RustCrypto
regardless drops the `aws_lc_rs` feature. A proxy without `auth.mode: "jwt"`, or
with an injected verifier, installs no `jsonwebtoken` provider: that choice
stays with your application.

If another crate in your process uses `jsonwebtoken` too, it may reach it before
any proxy server is built, and would hit the same ambiguity. It can also turn on
the built-in verifier is built, and would hit the same ambiguity. It can also turn on
the other backend for `jsonwebtoken` directly, which leaves this crate looking
single-backend while `jsonwebtoken` sees two. Settle it once at the top of
`main`:
Expand Down
10 changes: 5 additions & 5 deletions src/auth/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ pub(crate) fn preferred_provider() -> &'static jsonwebtoken::crypto::CryptoProvi
/// JWT. It is idempotent, and installs the backend this crate was built with,
/// so `jsonwebtoken` never has to infer one.
///
/// A plain [`ProxyServer`](crate::ProxyServer) deployment needs no call: the
/// server does this while it is being built. It is public for the case the
/// server cannot cover, which is also how the ambiguity arises in the first
/// place: another crate in the graph uses `jsonwebtoken` too and may reach it
/// first. Call this at the top of `main` and every consumer is covered,
/// A plain [`ProxyServer`](crate::ProxyServer) deployment needs no call: building
/// the built-in verifier does this, and a proxy without one installs nothing. It
/// is public for the case the proxy cannot cover, which is also how the ambiguity
/// arises in the first place: another crate in the graph uses `jsonwebtoken` too
/// and may reach it first. Call this at the top of `main` and every consumer is covered,
/// whichever runs first.
///
/// With both backends linked the choice is `aws_lc_rs`: constant-time, and free
Expand Down
34 changes: 34 additions & 0 deletions src/auth/crypto/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,37 @@ fn installing_twice_is_harmless() {
super::install_default_crypto_provider();
super::install_default_crypto_provider();
}

/// A proxy without `auth:` uses no JWT and leaves the process-wide jsonwebtoken
/// provider to the host: an application with its own token checks must keep
/// its own choice. The probe install succeeds only if nothing installed one
/// first (each test runs in its own process).
#[test]
fn a_proxy_without_auth_installs_no_jwt_provider() {
let service = crate::ProxyServer::from_yaml_str("service:\n name: demo\n")
.unwrap()
.service(tonic::service::Routes::default())
.unwrap();
drop(service);
assert!(
super::preferred_provider().install_default().is_ok(),
"building a proxy without auth must not install a jsonwebtoken provider"
);
}

/// The built-in verifier, once built, has the provider it verifies with.
#[test]
fn building_the_builtin_verifier_installs_the_provider() {
let jwt = crate::config::JwtConfig {
jwks_uri: Some("https://issuer.example/jwks".into()),
jwks_max_age_secs: 300,
issuer: None,
audience: None,
public_key_pem_file: None,
claims_headers: Default::default(),
roles_claim: "roles".into(),
cache: Default::default(),
};
super::super::verifier::ConfigVerifier::build(&jwt).unwrap();
assert!(super::preferred_provider().install_default().is_err());
}
5 changes: 2 additions & 3 deletions src/auth/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ impl ConfigVerifier {
/// max age is below the refresh interval, the PEM file cannot be read, or
/// it is not a valid Ed25519 public key.
pub(crate) fn build(jwt: &JwtConfig) -> Result<Self, String> {
// Normally settled in `ProxyServer::from_config` already; repeated here
// because a verifier can also be built without going through the
// server, and the call costs one atomic once the provider is in place.
// The only place the proxy settles the jsonwebtoken provider: a proxy
// that builds no built-in verifier leaves that choice to its host.
super::crypto::install_default_crypto_provider();

let keys = if let Some(uri) = &jwt.jwks_uri {
Expand Down
6 changes: 6 additions & 0 deletions src/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
//! `axum` type), so `cargo tree -i axum` in an embedder shows axum only under
//! `structured-proxy`.
//!
//! [`AuthDecider`] and [`TokenVerifier`] are for traffic whose authentication
//! and authorization are left to the proxy. An application with its own
//! authn/authz needs neither for requests that reach its service; its checks
//! never see the proxy's own routes, which take an [`AuthDecider`] scoped to
//! them or stay off.
//!
//! Stateful concerns (BFF sessions, OIDC `authorize`/`token`) are deliberately
//! absent: the default build is a stateless data plane (see the crate README
//! Non-goals). They are planned behind an opt-in `bff` feature.
Expand Down
16 changes: 5 additions & 11 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
//! compiled in at once — Cargo features are additive, so a dependency graph
//! with two dependents asking for different backends unifies into exactly that
//! build. `aws_lc_rs` then wins: it is constant-time and free of the `rsa`
//! advisory `rust_crypto` carries. [`ProxyServer::from_config`] settles that
//! choice for the process; a process where another crate may reach
//! `jsonwebtoken` before any server exists calls
//! [`install_default_crypto_provider`] from `main` instead.
//! advisory `rust_crypto` carries. Building this verifier settles that choice
//! for the process; a process where another crate may reach `jsonwebtoken`
//! first calls [`install_default_crypto_provider`] from `main` instead. A
//! proxy with no built-in verifier installs nothing: that choice stays with
//! its host.
//! - an **injected** one, supplied by the embedder through
//! [`ProxyServer::with_token_verifier`]. Since Cargo unifies features across a
//! whole dependency graph, a backend feature cannot be chosen per binary —
Expand Down Expand Up @@ -163,13 +164,6 @@ impl Default for ProxyServer {
impl ProxyServer {
/// Create from YAML config file.
pub fn from_config(config: ProxyConfig) -> Self {
// Earliest point this crate owns: settle the JWT crypto provider here,
// long before the first token arrives. A process whose other crates
// reach jsonwebtoken before any server exists calls
// `install_default_crypto_provider` from `main` instead.
#[cfg(feature = "builtin_jwt")]
auth::crypto::install_default_crypto_provider();

Self {
config,
descriptor_pool: None,
Expand Down
Loading