nautilus_deribit/websocket/
auth.rs1use std::time::Duration;
19
20use nautilus_core::{UUID4, string::secret::SecretString, time::get_atomic_clock_realtime};
21use tokio_util::sync::CancellationToken;
22use zeroize::{Zeroize, ZeroizeOnDrop};
23
24use super::{
25 handler::HandlerCommand,
26 messages::{DeribitAuthParams, DeribitAuthResult, DeribitRefreshTokenParams},
27};
28use crate::common::credential::Credential;
29
30pub const DERIBIT_DATA_SESSION_NAME: &str = "nautilus-data";
32
33pub const DERIBIT_EXECUTION_SESSION_NAME: &str = "nautilus-execution";
35
36#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)]
38pub struct AuthState {
39 pub access_token: SecretString,
41 pub refresh_token: SecretString,
43 pub expires_in: u64,
45 pub obtained_at: u64,
47 pub scope: String,
49}
50
51impl AuthState {
52 #[must_use]
54 pub fn from_auth_result(result: &DeribitAuthResult, obtained_at: u64) -> Self {
55 Self {
56 access_token: result.access_token.clone(),
57 refresh_token: result.refresh_token.clone(),
58 expires_in: result.expires_in,
59 obtained_at,
60 scope: result.scope.clone(),
61 }
62 }
63
64 #[must_use]
66 pub fn expires_at_ms(&self) -> u64 {
67 self.obtained_at + (self.expires_in * 1000)
68 }
69
70 #[must_use]
72 pub fn is_expired(&self, current_time_ms: u64) -> bool {
73 current_time_ms + 60_000 >= self.expires_at_ms()
75 }
76
77 #[must_use]
79 pub fn is_session_scoped(&self) -> bool {
80 self.scope.starts_with("session:")
81 }
82}
83
84pub fn send_auth_request(
96 credential: &Credential,
97 scope: Option<String>,
98 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
99) {
100 let timestamp = get_atomic_clock_realtime().get_time_ms();
101 let nonce = UUID4::new().to_string();
102 let signature = credential.sign_ws_auth(timestamp, &nonce, "");
103
104 let mut auth_params = DeribitAuthParams {
105 grant_type: "client_signature".to_string(),
106 client_id: SecretString::from(credential.api_key()),
107 timestamp,
108 signature: SecretString::from(signature),
109 nonce,
110 data: SecretString::default(),
111 scope,
112 };
113
114 let serialized = serde_json::to_string(&auth_params).map(SecretString::from);
115 auth_params.zeroize();
116
117 match serialized {
118 Ok(auth_params) => {
119 if let Err(e) = cmd_tx.send(HandlerCommand::Authenticate { auth_params }) {
120 log::error!("Failed to send auth command: {e}");
121 }
122 }
123 Err(e) => {
124 log::error!("Failed to serialize auth params: {e}");
125 }
126 }
127}
128
129pub async fn refresh_token_after_delay(
137 expires_in: u64,
138 refresh_token: SecretString,
139 cmd_tx: tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
140 cancel_token: CancellationToken,
141) {
142 let refresh_delay_secs = (expires_in as f64 * 0.8) as u64;
143
144 log::debug!(
145 "Token refresh scheduled in {refresh_delay_secs}s (token expires in {expires_in}s)"
146 );
147
148 tokio::select! {
149 () = tokio::time::sleep(Duration::from_secs(refresh_delay_secs)) => {}
150 () = cancel_token.cancelled() => {
151 log::debug!("Token refresh cancelled");
152 return;
153 }
154 }
155
156 log::debug!("Refreshing authentication token...");
157 let mut refresh_params = DeribitRefreshTokenParams {
158 grant_type: "refresh_token".to_string(),
159 refresh_token,
160 };
161
162 let serialized = serde_json::to_string(&refresh_params).map(SecretString::from);
163 refresh_params.zeroize();
164
165 if let Ok(auth_params) = serialized {
166 let _ = cmd_tx.send(HandlerCommand::Authenticate { auth_params });
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use rstest::rstest;
173
174 use super::*;
175
176 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
177
178 #[rstest]
179 fn test_auth_state_zeroizes_on_drop() {
180 assert_zeroize_on_drop::<AuthState>();
181 }
182
183 #[rstest]
184 #[tokio::test]
185 async fn test_refresh_token_serialization_is_unchanged() {
186 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel();
187 let refresh_token = ["refresh-", "token-value"].concat();
188
189 refresh_token_after_delay(
190 0,
191 SecretString::from(refresh_token.clone()),
192 cmd_tx,
193 CancellationToken::new(),
194 )
195 .await;
196
197 let command = cmd_rx.try_recv().expect("refresh command");
198 let HandlerCommand::Authenticate { auth_params } = command else {
199 panic!("expected authenticate command");
200 };
201 let auth_params: serde_json::Value =
202 serde_json::from_str(auth_params.expose_secret()).expect("valid auth params");
203 assert_eq!(
204 auth_params,
205 serde_json::json!({
206 "grant_type": "refresh_token",
207 "refresh_token": refresh_token,
208 })
209 );
210 }
211}