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, 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, 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, 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, PartialEq, Eq)]
47pub struct User {
48    pub id: u64,
49    pub name: String,
50    #[serde(rename = "displayName")]
51    pub display_name: String,
52}
53
54#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
55pub struct TwoStepVerificationInfo {
56    #[serde(rename = "mediaType")]
57    pub media_type: MediaType,
58    pub ticket: String,
59}
60
61#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
62pub struct LoginResponse {
63    pub user: User,
64    #[serde(rename = "twoStepVerificationData")]
65    pub two_step_verification_info: TwoStepVerificationInfo,
66    #[serde(rename = "identityVerificationLoginTicket")]
67    pub verification_ticket: String,
68    #[serde(rename = "isBanned")]
69    pub is_banned: bool,
70    #[serde(rename = "shouldUpdateEmail")]
71    pub should_update_email: bool,
72    #[serde(rename = "recoveryEmail")]
73    pub recovery_email: String,
74    #[serde(rename = "accountBlob")]
75    pub account_blob: String,
76}
77
78#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
79struct AuthenticationIntent {
80    #[serde(rename = "clientPublicKey")]
81    public_key: String,
82    #[serde(rename = "clientEpochTimestamp")]
83    epoch_timestamp: u64,
84    #[serde(rename = "saiSignature")]
85    signature: String,
86    #[serde(rename = "serverNonce")]
87    nonce: String,
88}
89
90async fn authentication_intent(client: &mut Client) -> Result<AuthenticationIntent, Error> {
91    let nonce = hba_service::v1::server_nonce(client).await?;
92
93    let unix = SystemTime::now()
94        .duration_since(SystemTime::UNIX_EPOCH)
95        .unwrap()
96        .as_secs();
97
98    let key = SigningKey::random(&mut OsRng);
99    let public_key = BASE64_STANDARD.encode(key.verifying_key().to_sec1_bytes());
100
101    let binding = format!("{}:{}:{}", public_key, unix, nonce);
102    let hash = Sha256::digest(binding);
103
104    let signature: Signature = key.sign(&hash[..]);
105    let signature = String::from_utf8_lossy(&signature.to_bytes()).to_string();
106
107    Ok(AuthenticationIntent {
108        public_key,
109        epoch_timestamp: unix,
110        signature,
111        nonce,
112    })
113}
114
115pub async fn login(
116    client: &mut Client,
117    login: &str,
118    key: &str,
119    login_type: LoginType,
120) -> Result<LoginResponse, Error> {
121    #[derive(Serialize)]
122    struct Request<'a> {
123        #[serde(rename = "ctype")]
124        login_type: LoginType,
125        #[serde(rename = "cvalue")]
126        login: &'a str,
127        #[serde(rename = "password")]
128        key: &'a str,
129
130        #[serde(rename = "secureAuthenticationIntent")]
131        authentication_intent: AuthenticationIntent,
132    }
133
134    let authentication_intent = authentication_intent(client).await?;
135    let result = client
136        .requestor
137        .client
138        .post(format!("{URL}/login"))
139        .headers(client.requestor.default_headers.clone())
140        .json(&Request {
141            login_type,
142            login,
143            key,
144            authentication_intent,
145        })
146        .send()
147        .await;
148
149    let response = client.validate_response(result).await?;
150    client.requestor.parse_json::<LoginResponse>(response).await
151}
152
153pub async fn recommended_usernames_from_display_name(
154    client: &mut Client,
155    display_name: &str,
156    birthday: DateTime,
157) -> Result<RecommendedUsernamesFromDisplayName, Error> {
158    #[derive(Serialize)]
159    struct Request<'a> {
160        #[serde(rename = "displayName")]
161        display_name: &'a str,
162        birthday: &'a str,
163    }
164
165    let result = client
166        .requestor
167        .client
168        .post(format!(
169            "{URL}/validators/recommendedUsernameFromDisplayName"
170        ))
171        .headers(client.requestor.default_headers.clone())
172        .json(&Request {
173            display_name,
174            birthday: birthday.to_string().as_str(),
175        })
176        .send()
177        .await;
178
179    let response = client.validate_response(result).await?;
180    client
181        .requestor
182        .parse_json::<RecommendedUsernamesFromDisplayName>(response)
183        .await
184}