Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/tests/a34_oidc_bearer_never_falls_through.rs

//! ★ **A34-1, at holger's own door** (the identity review, 2026-10-26): a
//! JWT that holger cannot verify is REFUSED. Before nornir-auth-oidc 0.2 a
//! token whose key fetch failed went to a guessed `{issuer}/userinfo`, which
//! checks neither issuer nor audience, so a token minted for ANY app of the
//! tenant authenticated to holger.
//!
//! This drives the real dispatch holger uses (`nornir_auth_policy::validate_request`
//! over the RON `auth` block) against an in-process IdP that answers
//! `/userinfo` for anything, with a `jwks_uri` that yields no keys.
#![cfg(all(feature = "auth-oidc", feature = "online"))]

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

use nornir_auth_oidc::test_support;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

/// An IdP whose `/userinfo` says `sub=alice` to ANY bearer, counting hits.
async fn permissive_userinfo(hits: Arc<AtomicUsize>) -> String {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    tokio::spawn(async move {
        while let Ok((mut sock, _)) = listener.accept().await {
            hits.fetch_add(1, Ordering::SeqCst);
            let mut buf = [0u8; 2048];
            let _ = sock.read(&mut buf).await;
            let body = br#"{"sub":"alice"}"#;
            let head = format!(
                "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
                body.len()
            );
            let _ = sock.write_all(head.as_bytes()).await;
            let _ = sock.write_all(body).await;
        }
    });
    format!("http://{addr}")
}

fn auth_block(issuer: &str) -> nornir_auth_policy::AuthConfig {
    let ron = format!(
        r#"(
        methods: [ Oidc(
            issuer_url: "{issuer}",
            audience: Some("holger"),
            jwks_uri: Some("http://127.0.0.1:1/jwks"),
        ) ],
        require_auth_for_reads: true,
    )"#
    );
    ron::from_str(&ron).expect("the auth block parses")
}

#[tokio::test]
async fn a34_1_a_token_for_another_app_never_authenticates_through_userinfo() {
    let hits = Arc::new(AtomicUsize::new(0));
    let issuer = permissive_userinfo(hits.clone()).await;
    let cfg = auth_block(&issuer);
    for (what, token) in [
        (
            "another audience",
            test_support::token_for(&issuer, "another-app", "alice", 3600),
        ),
        (
            "another issuer",
            test_support::token_for("https://evil.example", "holger", "alice", 3600),
        ),
        (
            "cold key cache",
            test_support::token_for(&issuer, "holger", "alice", 3600),
        ),
        ("opaque", "opaque-token-a34".to_owned()),
    ] {
        let got = nornir_auth_policy::validate_request(&cfg, Some(&token), None).await;
        assert!(
            !matches!(got, Ok(Some(_))),
            "{what}: holger authenticated a token it could not verify: {:?}",
            got.map(|o| o.map(|i| i.subject))
        );
    }
    assert_eq!(
        hits.load(Ordering::SeqCst),
        0,
        "the guessed /userinfo was called"
    );
}