nonce_auth/
lib.rs

1//! A lightweight, secure nonce-based authentication library for Rust.
2//!
3//! This library provides a simple, robust solution for preventing replay attacks
4//! in APIs and other network services. It uses a combination of nonces, timestamps,
5//! and HMAC signatures to ensure that each request is unique and authentic.
6//!
7//! For a quick start, see the [README.md](https://github.com/kookyleo/nonce-auth/blob/main/README.md).
8//! For configuration options, see [CONFIGURATION.md](https://github.com/kookyleo/nonce-auth/blob/main/CONFIGURATION.md).
9
10use hmac::Hmac;
11use serde::{Deserialize, Serialize};
12use sha2::Sha256;
13
14pub mod nonce;
15pub mod storage {
16    //! Pluggable storage backends for nonce persistence.
17    pub use crate::nonce::storage::*;
18}
19
20// Re-export key types for easy access.
21pub use nonce::{NonceClient, NonceConfig, NonceError, NonceServer};
22
23/// Internal type alias for HMAC-SHA256 operations.
24type HmacSha256 = Hmac<Sha256>;
25
26/// A self-contained cryptographic credential used to authenticate a request.
27///
28/// This structure is generated by a `NonceClient` and verified by a `NonceServer`.
29/// It is designed to be serialized and sent alongside your application's request data.
30///
31/// # Fields
32///
33/// - `timestamp`: Unix timestamp indicating when the credential was created.
34/// - `nonce`: A unique, single-use value to prevent replay attacks.
35/// - `signature`: An HMAC-SHA256 signature covering the timestamp, nonce, and user-defined payload.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct NonceCredential {
38    pub timestamp: u64,
39    pub nonce: String,
40    pub signature: String,
41}
42
43#[cfg(test)]
44mod tests {
45    use crate::nonce::{NonceClient, NonceError, NonceServer};
46
47    #[tokio::test]
48    async fn test_client_server_separation() {
49        let client = NonceClient::new(b"test_secret");
50        let server = NonceServer::builder().build_and_init().await.unwrap();
51        let payload = b"test_payload";
52
53        // Client creates a credential
54        let credential = client.credential_builder().sign(payload).unwrap();
55
56        // Server verifies the credential using the standard method
57        let result = server
58            .credential_verifier(&credential)
59            .with_secret(b"test_secret")
60            .verify(payload)
61            .await;
62
63        assert!(result.is_ok());
64
65        // Same nonce should be rejected
66        let result = server
67            .credential_verifier(&credential)
68            .with_secret(b"test_secret")
69            .verify(payload)
70            .await;
71
72        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
73    }
74
75    #[tokio::test]
76    async fn test_context_isolation() {
77        let client = NonceClient::new(b"test_secret");
78        let server = NonceServer::builder().build_and_init().await.unwrap();
79        let payload = b"test_payload";
80
81        let credential = client.credential_builder().sign(payload).unwrap();
82
83        // Same nonce should work in different contexts
84        server
85            .credential_verifier(&credential)
86            .with_secret(b"test_secret")
87            .with_context(Some("context1"))
88            .verify(payload)
89            .await
90            .unwrap();
91
92        server
93            .credential_verifier(&credential)
94            .with_secret(b"test_secret")
95            .with_context(Some("context2"))
96            .verify(payload)
97            .await
98            .unwrap();
99
100        // But should fail if used twice in same context
101        let result = server
102            .credential_verifier(&credential)
103            .with_secret(b"test_secret")
104            .with_context(Some("context1"))
105            .verify(payload)
106            .await;
107
108        assert!(matches!(result, Err(NonceError::DuplicateNonce)));
109    }
110
111    #[tokio::test]
112    async fn test_timestamp_validation() {
113        let client = NonceClient::new(b"test_secret");
114        let server = NonceServer::builder()
115            .with_time_window(std::time::Duration::from_secs(1))
116            .build_and_init()
117            .await
118            .unwrap();
119        let payload = b"test_payload";
120
121        // Create credential with old timestamp
122        let mut credential = client.credential_builder().sign(payload).unwrap();
123
124        // Simulate old timestamp (2 seconds ago, but time window is 1 second)
125        credential.timestamp = std::time::SystemTime::now()
126            .duration_since(std::time::UNIX_EPOCH)
127            .unwrap()
128            .as_secs()
129            - 2;
130
131        let result = server
132            .credential_verifier(&credential)
133            .with_secret(b"test_secret")
134            .verify(payload)
135            .await;
136
137        assert!(matches!(result, Err(NonceError::TimestampOutOfWindow)));
138    }
139
140    #[tokio::test]
141    async fn test_signature_verification() {
142        let client = NonceClient::new(b"test_secret");
143        let server = NonceServer::builder().build_and_init().await.unwrap();
144        let payload = b"test_payload";
145
146        let credential = client.credential_builder().sign(payload).unwrap();
147
148        let result = server
149            .credential_verifier(&credential)
150            .with_secret(b"different_secret") // Different secret from client
151            .verify(payload)
152            .await;
153
154        assert!(matches!(result, Err(NonceError::InvalidSignature)));
155    }
156
157    #[tokio::test]
158    async fn test_serialization() {
159        let client = NonceClient::new(b"test_secret");
160        let credential = client.credential_builder().sign(b"test_payload").unwrap();
161
162        // Test JSON serialization
163        let json = serde_json::to_string(&credential).unwrap();
164        let deserialized: super::NonceCredential = serde_json::from_str(&json).unwrap();
165
166        assert_eq!(credential.timestamp, deserialized.timestamp);
167        assert_eq!(credential.nonce, deserialized.nonce);
168        assert_eq!(credential.signature, deserialized.signature);
169    }
170}