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 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}