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}