nautilus_deribit/websocket/auth.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Authentication state and token refresh for Deribit WebSocket connections.
17
18use std::time::Duration;
19
20use nautilus_common::live::get_runtime;
21use nautilus_core::{UUID4, time::get_atomic_clock_realtime};
22
23use super::{
24 handler::HandlerCommand,
25 messages::{DeribitAuthParams, DeribitAuthResult, DeribitRefreshTokenParams},
26};
27use crate::common::credential::Credential;
28
29/// Session name for Deribit WebSocket data client authentication.
30pub const DERIBIT_DATA_SESSION_NAME: &str = "nautilus-data";
31
32/// Session name for Deribit WebSocket execution client authentication.
33pub const DERIBIT_EXECUTION_SESSION_NAME: &str = "nautilus-execution";
34
35/// Authentication state storing OAuth tokens.
36#[derive(Debug, Clone)]
37pub struct AuthState {
38 /// Access token for API requests.
39 pub access_token: String,
40 /// Refresh token for obtaining new access tokens.
41 pub refresh_token: String,
42 /// Token expiration time in seconds from authentication.
43 pub expires_in: u64,
44 /// Timestamp when tokens were obtained (Unix milliseconds).
45 pub obtained_at: u64,
46 /// Scope used for authentication.
47 pub scope: String,
48}
49
50impl AuthState {
51 /// Creates a new [`AuthState`] from an authentication result.
52 #[must_use]
53 pub fn from_auth_result(result: &DeribitAuthResult, obtained_at: u64) -> Self {
54 Self {
55 access_token: result.access_token.clone(),
56 refresh_token: result.refresh_token.clone(),
57 expires_in: result.expires_in,
58 obtained_at,
59 scope: result.scope.clone(),
60 }
61 }
62
63 /// Returns the expiration timestamp in Unix milliseconds.
64 #[must_use]
65 pub fn expires_at_ms(&self) -> u64 {
66 self.obtained_at + (self.expires_in * 1000)
67 }
68
69 /// Returns whether the token is expired or near expiry (within 60 seconds).
70 #[must_use]
71 pub fn is_expired(&self, current_time_ms: u64) -> bool {
72 // Consider expired if within 60 seconds of expiry
73 current_time_ms + 60_000 >= self.expires_at_ms()
74 }
75
76 /// Returns whether this is a session-scoped authentication.
77 #[must_use]
78 pub fn is_session_scoped(&self) -> bool {
79 self.scope.starts_with("session:")
80 }
81}
82
83/// Sends an authentication request using client_signature grant type.
84///
85/// This is a helper function used by both initial authentication and re-authentication
86/// after reconnection. It generates the signature and sends auth params via the command channel.
87/// The handler is responsible for generating the request ID.
88///
89/// # Arguments
90///
91/// * `credential` - API credentials for signing the request
92/// * `scope` - Optional scope (e.g., "session:nautilus" for session-based auth)
93/// * `cmd_tx` - Command channel to send the authentication request
94pub fn send_auth_request(
95 credential: &Credential,
96 scope: Option<String>,
97 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
98) {
99 let timestamp = get_atomic_clock_realtime().get_time_ms();
100 let nonce = UUID4::new().to_string();
101 let signature = credential.sign_ws_auth(timestamp, &nonce, "");
102
103 let auth_params = DeribitAuthParams {
104 grant_type: "client_signature".to_string(),
105 client_id: credential.api_key.to_string(),
106 timestamp,
107 signature,
108 nonce,
109 data: String::new(),
110 scope,
111 };
112
113 if let Ok(auth_params_value) = serde_json::to_value(&auth_params) {
114 let _ = cmd_tx.send(HandlerCommand::Authenticate {
115 auth_params: auth_params_value,
116 });
117 }
118}
119
120/// Spawns a background task to refresh the authentication token before it expires.
121///
122/// The task sleeps until 80% of the token lifetime has passed, then sends a refresh request.
123/// When the refresh succeeds, a new `Authenticated` message will be received, which triggers
124/// another refresh task - creating a continuous refresh cycle.
125pub fn spawn_token_refresh_task(
126 expires_in: u64,
127 refresh_token: String,
128 cmd_tx: tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
129) {
130 // Refresh at 80% of token lifetime to ensure we never expire
131 let refresh_delay_secs = (expires_in as f64 * 0.8) as u64;
132
133 get_runtime().spawn(async move {
134 log::debug!(
135 "Token refresh scheduled in {refresh_delay_secs}s (token expires in {expires_in}s)"
136 );
137 tokio::time::sleep(Duration::from_secs(refresh_delay_secs)).await;
138
139 log::debug!("Refreshing authentication token...");
140 let refresh_params = DeribitRefreshTokenParams {
141 grant_type: "refresh_token".to_string(),
142 refresh_token,
143 };
144
145 if let Ok(auth_params_value) = serde_json::to_value(&refresh_params) {
146 let _ = cmd_tx.send(HandlerCommand::Authenticate {
147 auth_params: auth_params_value,
148 });
149 }
150 });
151}