ppoppo-token 0.3.0

JWT (RFC 9068, EdDSA) issuance + verification engine for the Ppoppo ecosystem. Single deep module with a small interface (issue, verify) hiding RFC 8725 mitigations M01-M45, JWKS handling, and substrate ports (epoch, session, replay).
Documentation
//! M66 — id_token nonce binding (OIDC Core 1.0 §3.1.3.7 step 11).
//!
//! The RP mints a per-session nonce, sends it in the Authentication
//! Request, stores a copy bound to its session state. On id_token
//! receipt, the engine compares the payload's `nonce` claim against
//! the RP-stored copy. Mismatch / missing → reject — the canonical
//! replay-defense gate for OIDC.
//!
//! ── Why a separate engine submodule ─────────────────────────────────────
//!
//! Nonce binding is structurally similar to the access_token's M35
//! replay-defense (both check "is this token uniquely bound to this
//! session?") but the substrate is RP-managed, not server-cached. The
//! engine provides the comparison primitive; storage of the expected
//! value is `id_token::VerifyConfig::expected_nonce` (set at auth-
//! request boundary, threaded through to verify call).
//!
//! ── Reusability across profiles ─────────────────────────────────────────
//!
//! Currently only id_token consumes `check_nonce`. Future DPoP work
//! (RFC 9449) carries a server-issued nonce on the access path; this
//! function stays `pub(crate)` and profile-agnostic so it can be reused
//! without splitting between modules. Per `feedback_audit_grilled_decisions`:
//! make it reusable by default (no profile-specific assumption in the
//! signature), but don't pre-wire DPoP until that row lands.

use crate::id_token::{AuthError, Nonce};

/// Compare the token's payload `nonce` against the RP-stored expected
/// value. Plain `==` — nonce is a public correlator with no secrecy
/// contract (see `id_token::nonce` doc-comment).
pub(crate) fn run(payload: &serde_json::Value, expected: &Nonce) -> Result<(), AuthError> {
    let token_nonce = payload
        .get("nonce")
        .and_then(|v| v.as_str())
        .ok_or(AuthError::NonceMissing)?;
    if token_nonce != expected.as_str() {
        return Err(AuthError::NonceMismatch);
    }
    Ok(())
}