//! Dual-mode JWT authenticator — the public-facing entry point. //! //! Composes `SigningKeyStore` (local cache) + `jwt_decoder` (HS256 logic) //! + `TokenServiceClient` (RPC fallback) into a single `Authenticator`. use std::sync::Arc; use tonic::transport::Channel; use crate::pb::core::VerifyTokenRequest; use crate::pb::core::token_service_client::TokenServiceClient; use crate::{ImksError, ImksResult}; use super::claims::TokenClaims; use super::jwt_decoder; use super::key_store::SigningKeyStore; /// Dual-mode JWT authenticator. /// /// - **Local mode** (`verify_local`): HS256 verification against cached /// signing keys. Zero network latency. Suitable for high-frequency /// operations like message send/receive. /// /// - **RPC mode** (`verify_rpc`): forwards the token to appks /// `VerifyToken()`. Real-time revocation awareness. Use for /// sensitive operations like kick/ban/permission changes. #[derive(Clone)] pub struct Authenticator { key_store: Arc, token_client: TokenServiceClient, } impl Authenticator { /// Create a new authenticator. Initializes the signing key cache from appks. pub async fn new(token_client: TokenServiceClient) -> ImksResult { let key_store = SigningKeyStore::init(token_client.clone()).await?; Ok(Self { key_store: Arc::new(key_store), token_client, }) } /// Fast-path verification using locally cached signing keys. /// /// Extracts `kid` from the JWT header, looks up the key, and verifies /// the HS256 signature. Cannot detect token revocation within the /// current key rotation window (~3 hours). pub fn verify_local(&self, token: &str) -> ImksResult { let kid = jwt_decoder::extract_kid(token)?; let key = self .key_store .get_key(&kid) .ok_or_else(|| ImksError::Auth(format!("Unknown signing key kid: {kid}")))?; jwt_decoder::verify_and_decode(token, &key) } /// Authoritative verification via appks `VerifyToken` RPC. /// /// Detects token revocation in real-time. Adds one RPC round-trip. pub async fn verify_rpc(&self, token: &str) -> ImksResult { let mut client = self.token_client.clone(); let resp = client .verify_token(VerifyTokenRequest { token: token.to_string(), }) .await?; let inner = resp.into_inner(); if !inner.valid { return Err(ImksError::Auth(inner.reason)); } let proto_claims = inner.claims.ok_or_else(|| { ImksError::Auth("VerifyToken returned valid=true but no claims".into()) })?; Ok(TokenClaims::from_proto(proto_claims)) } /// Extract the Bearer token value from an `Authorization` header. /// /// Expects format: `"Bearer "`. Returns the token part. pub fn extract_bearer(auth_header: &str) -> ImksResult<&str> { auth_header .strip_prefix("Bearer ") .ok_or_else(|| ImksError::Auth("Missing or malformed Authorization header".into())) } /// Shut down the background key refresh task. pub async fn shutdown(self) { // Unwrap the Arc — if there are other clones, the store lives on. if let Ok(store) = Arc::try_unwrap(self.key_store) { store.shutdown().await; } } }