Skip to main content

roblox_api/api/auth/
v1.rs

1use std::time::SystemTime;
2
3use base64::{Engine, prelude::BASE64_STANDARD};
4use p256::{
5    ecdsa::{Signature, SigningKey, signature::Signer},
6    elliptic_curve::rand_core::OsRng,
7};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use crate::{DateTime, Error, api::hba_service, client::Client, endpoint};
12
13pub const URL: &str = "https://auth.roblox.com/v1";
14
15#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
16pub enum LoginType {
17    Email,
18    #[default]
19    Username,
20    PhoneNumber,
21    EmailOtpSessionToken,
22    AuthToken,
23    Passkey,
24}
25
26#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
27pub enum MediaType {
28    Email,
29    SMS,
30    Authenticator,
31    RecoveryCode,
32    SecurityKey,
33    CrossDevice,
34    Password,
35    Passkey,
36}
37
38#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
39pub struct RecommendedUsernamesFromDisplayName {
40    #[serde(rename = "didGenerateNewUsername")]
41    pub new_name_generated: bool,
42    #[serde(rename = "suggestedUsernames")]
43    pub suggested_names: Vec<String>,
44}
45
46#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
47#[serde(rename_all = "camelCase")]
48pub struct User {
49    pub id: u64,
50    pub name: String,
51    pub display_name: String,
52}
53
54#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
55#[serde(rename_all = "camelCase")]
56pub struct TwoStepVerificationInfo {
57    pub media_type: MediaType,
58    pub ticket: String,
59}
60
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
62#[serde(rename_all = "camelCase")]
63pub struct LoginResponse {
64    pub user: User,
65    #[serde(rename = "twoStepVerificationData")]
66    pub two_step_verification_info: TwoStepVerificationInfo,
67    #[serde(rename = "identityVerificationLoginTicket")]
68    pub verification_ticket: String,
69    pub is_banned: bool,
70    pub should_update_email: bool,
71    pub recovery_email: String,
72    pub account_blob: String,
73}
74
75#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
76struct AuthenticationIntent {
77    #[serde(rename = "clientPublicKey")]
78    public_key: String,
79    #[serde(rename = "clientEpochTimestamp")]
80    epoch_timestamp: u64,
81    #[serde(rename = "saiSignature")]
82    signature: String,
83    #[serde(rename = "serverNonce")]
84    nonce: String,
85}
86
87async fn authentication_intent(client: &mut Client) -> Result<AuthenticationIntent, Error> {
88    let nonce = hba_service::v1::server_nonce(client).await?;
89
90    let unix = SystemTime::now()
91        .duration_since(SystemTime::UNIX_EPOCH)
92        .unwrap()
93        .as_secs();
94
95    let key = SigningKey::random(&mut OsRng);
96    let public_key = BASE64_STANDARD.encode(key.verifying_key().to_sec1_bytes());
97
98    let binding = format!("{}:{}:{}", public_key, unix, nonce);
99    let hash = Sha256::digest(binding);
100
101    let signature: Signature = key.sign(&hash[..]);
102    let signature = String::from_utf8_lossy(&signature.to_bytes()).to_string();
103
104    Ok(AuthenticationIntent {
105        public_key,
106        epoch_timestamp: unix,
107        signature,
108        nonce,
109    })
110}
111
112endpoint! {
113    /// Logs in with the given credentials and returns a session token.
114    login(client, login: &str, key: &str, login_type: LoginType) -> LoginResponse {
115        POST "{URL}/login";
116        types {
117            Request<'a> {
118                login_type("ctype"): LoginType,
119                login("cvalue"): &'a str,
120                key("password"): &'a str,
121                authentication_intent("secureAuthenticationIntent"): AuthenticationIntent,
122            }
123        }
124        prelude {
125            let intent = authentication_intent(client).await?;
126        }
127        body_serialize {
128            &Request { login_type, login, key, authentication_intent: intent }
129        }
130    }
131
132    recommended_usernames_from_display_name(
133        display_name: &str, birthday: DateTime
134    ) -> RecommendedUsernamesFromDisplayName {
135        POST "{URL}/validators/recommendedUsernameFromDisplayName";
136        types {
137            Request<'a> {
138                display_name("displayName"): &'a str,
139                birthday: String,
140            }
141        }
142        body_serialize {
143            &Request {
144                display_name,
145                birthday: birthday.to_string(),
146            }
147        }
148    }
149}