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};
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
112pub async fn login(
113    client: &mut Client,
114    login: &str,
115    key: &str,
116    login_type: LoginType,
117) -> Result<LoginResponse, Error> {
118    #[derive(Serialize)]
119    struct Request<'a> {
120        #[serde(rename = "ctype")]
121        login_type: LoginType,
122        #[serde(rename = "cvalue")]
123        login: &'a str,
124        #[serde(rename = "password")]
125        key: &'a str,
126
127        #[serde(rename = "secureAuthenticationIntent")]
128        authentication_intent: AuthenticationIntent,
129    }
130
131    let authentication_intent = authentication_intent(client).await?;
132    let result = client
133        .requestor
134        .client
135        .post(format!("{URL}/login"))
136        .headers(client.requestor.default_headers.clone())
137        .json(&Request {
138            login_type,
139            login,
140            key,
141            authentication_intent,
142        })
143        .send()
144        .await;
145
146    let response = client.validate_response(result).await?;
147    client.requestor.parse_json::<LoginResponse>(response).await
148}
149
150pub async fn recommended_usernames_from_display_name(
151    client: &mut Client,
152    display_name: &str,
153    birthday: DateTime,
154) -> Result<RecommendedUsernamesFromDisplayName, Error> {
155    #[derive(Serialize)]
156    #[serde(rename_all = "camelCase")]
157    struct Request<'a> {
158        display_name: &'a str,
159        birthday: &'a str,
160    }
161
162    let result = client
163        .requestor
164        .client
165        .post(format!(
166            "{URL}/validators/recommendedUsernameFromDisplayName"
167        ))
168        .headers(client.requestor.default_headers.clone())
169        .json(&Request {
170            display_name,
171            birthday: birthday.to_string().as_str(),
172        })
173        .send()
174        .await;
175
176    let response = client.validate_response(result).await?;
177    client
178        .requestor
179        .parse_json::<RecommendedUsernamesFromDisplayName>(response)
180        .await
181}