Skip to main content

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};
22use tokio_util::sync::CancellationToken;
23
24use super::{
25    handler::HandlerCommand,
26    messages::{DeribitAuthParams, DeribitAuthResult, DeribitRefreshTokenParams},
27};
28use crate::common::credential::Credential;
29
30/// Session name for Deribit WebSocket data client authentication.
31pub const DERIBIT_DATA_SESSION_NAME: &str = "nautilus-data";
32
33/// Session name for Deribit WebSocket execution client authentication.
34pub const DERIBIT_EXECUTION_SESSION_NAME: &str = "nautilus-execution";
35
36/// Authentication state storing OAuth tokens.
37#[derive(Debug, Clone)]
38pub struct AuthState {
39    /// Access token for API requests.
40    pub access_token: String,
41    /// Refresh token for obtaining new access tokens.
42    pub refresh_token: String,
43    /// Token expiration time in seconds from authentication.
44    pub expires_in: u64,
45    /// Timestamp when tokens were obtained (Unix milliseconds).
46    pub obtained_at: u64,
47    /// Scope used for authentication.
48    pub scope: String,
49}
50
51impl AuthState {
52    /// Creates a new [`AuthState`] from an authentication result.
53    #[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    /// Returns the expiration timestamp in Unix milliseconds.
65    #[must_use]
66    pub fn expires_at_ms(&self) -> u64 {
67        self.obtained_at + (self.expires_in * 1000)
68    }
69
70    /// Returns whether the token is expired or near expiry (within 60 seconds).
71    #[must_use]
72    pub fn is_expired(&self, current_time_ms: u64) -> bool {
73        // Consider expired if within 60 seconds of expiry
74        current_time_ms + 60_000 >= self.expires_at_ms()
75    }
76
77    /// Returns whether this is a session-scoped authentication.
78    #[must_use]
79    pub fn is_session_scoped(&self) -> bool {
80        self.scope.starts_with("session:")
81    }
82}
83
84/// Sends an authentication request using client_signature grant type.
85///
86/// This is a helper function used by both initial authentication and re-authentication
87/// after reconnection. It generates the signature and sends auth params via the command channel.
88/// The handler is responsible for generating the request ID.
89///
90/// # Arguments
91///
92/// * `credential` - API credentials for signing the request
93/// * `scope` - Optional scope (e.g., "session:nautilus" for session-based auth)
94/// * `cmd_tx` - Command channel to send the authentication request
95pub 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 auth_params = DeribitAuthParams {
105        grant_type: "client_signature".to_string(),
106        client_id: credential.api_key().to_string(),
107        timestamp,
108        signature,
109        nonce,
110        data: String::new(),
111        scope,
112    };
113
114    match serde_json::to_value(&auth_params) {
115        Ok(auth_params_value) => {
116            if let Err(e) = cmd_tx.send(HandlerCommand::Authenticate {
117                auth_params: auth_params_value,
118            }) {
119                log::error!("Failed to send auth command: {e}");
120            }
121        }
122        Err(e) => {
123            log::error!("Failed to serialize auth params: {e}");
124        }
125    }
126}
127
128/// Spawns a background task to refresh the authentication token before it expires.
129///
130/// The task sleeps until 80% of the token lifetime has passed, then sends a refresh request.
131/// When the refresh succeeds, a new `Authenticated` message will be received, which triggers
132/// another refresh task - creating a continuous refresh cycle.
133///
134/// The `cancel_token` allows the caller to cancel a stale refresh task when a new
135/// authentication cycle begins (e.g., after reconnection re-auth).
136pub fn spawn_token_refresh_task(
137    expires_in: u64,
138    refresh_token: String,
139    cmd_tx: tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
140    cancel_token: CancellationToken,
141) {
142    // Refresh at 80% of token lifetime to ensure we never expire
143    let refresh_delay_secs = (expires_in as f64 * 0.8) as u64;
144
145    get_runtime().spawn(async move {
146        log::debug!(
147            "Token refresh scheduled in {refresh_delay_secs}s (token expires in {expires_in}s)"
148        );
149
150        tokio::select! {
151            () = tokio::time::sleep(Duration::from_secs(refresh_delay_secs)) => {}
152            () = cancel_token.cancelled() => {
153                log::debug!("Token refresh task cancelled");
154                return;
155            }
156        }
157
158        log::debug!("Refreshing authentication token...");
159        let refresh_params = DeribitRefreshTokenParams {
160            grant_type: "refresh_token".to_string(),
161            refresh_token,
162        };
163
164        if let Ok(auth_params_value) = serde_json::to_value(&refresh_params) {
165            let _ = cmd_tx.send(HandlerCommand::Authenticate {
166                auth_params: auth_params_value,
167            });
168        }
169    });
170}