steam_user/services/
tokens.rs1use base64::Engine;
2use hmac::{Hmac, Mac};
3use prost::Message;
4use sha2::Sha256;
5use steam_protos::{CAuthenticationRefreshTokenEnumerateResponse, CAuthenticationRefreshTokenRevokeRequest, EAuthTokenRevokeAction};
6use steam_totp::Secret;
7
8use crate::{client::SteamUser, endpoint::steam_endpoint, error::SteamUserError};
9
10impl SteamUser {
11 #[steam_endpoint(POST, host = Api, path = "/IAuthenticationService/EnumerateTokens/v1", kind = Auth)]
16 pub async fn enumerate_tokens(&self) -> Result<CAuthenticationRefreshTokenEnumerateResponse, SteamUserError> {
17 let access_token = self.session.access_token.as_ref().or(self.session.mobile_access_token.as_ref()).ok_or_else(|| SteamUserError::Other("Access token is required for enumerate_tokens".into()))?;
18
19 let request = steam_protos::messages::auth::CAuthenticationRefreshTokenEnumerateRequest { include_revoked: Some(false) };
21
22 let mut body = Vec::new();
23 request.encode(&mut body).map_err(|e| SteamUserError::Other(e.to_string()))?;
24
25 tracing::debug!(token_len = access_token.len(), "using access_token");
26
27 let response = self.post_path("/IAuthenticationService/EnumerateTokens/v1").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).query(&[("origin", "https://store.steampowered.com")]).form(&[("input_protobuf_encoded", base64::engine::general_purpose::STANDARD.encode(body))]).send().await?;
28
29 let status = response.status();
30 let bytes = response.bytes().await?;
31
32 if !status.is_success() {
33 tracing::error!("Failed to get tokens: HTTP {}", status);
34 return Err(SteamUserError::Other(format!("HTTP error {}", status)));
35 }
36
37 let result = CAuthenticationRefreshTokenEnumerateResponse::decode(bytes).map_err(|e| SteamUserError::Other(format!("Failed to decode protobuf: {}", e)))?;
39
40 Ok(result)
41 }
42
43 #[tracing::instrument(skip(self))]
47 pub async fn check_token_exists(&self, token_id: &str) -> Result<bool, SteamUserError> {
48 let tokens = self.enumerate_tokens().await?;
49 let token_id_u64 = token_id.parse::<u64>().map_err(|_| SteamUserError::Other(format!("Invalid token ID format: {}", token_id)))?;
50
51 Ok(tokens.refresh_tokens.iter().any(|t| t.token_id == Some(token_id_u64)))
52 }
53
54 #[steam_endpoint(POST, host = Api, path = "/IAuthenticationService/RevokeRefreshToken/v1", kind = Auth)]
68 pub async fn revoke_tokens(&self, token_ids: &[&str], shared_secret: Option<&str>) -> Result<RevokeTokensResult, SteamUserError> {
69 let access_token = self.session.mobile_access_token.as_ref().or(self.session.access_token.as_ref()).ok_or_else(|| SteamUserError::Other("Mobile access token is required for revoke_tokens".into()))?;
70 let steam_id = self.session.steam_id.ok_or(SteamUserError::NotLoggedIn)?;
71 let base_mac = match shared_secret {
72 Some(s) => {
73 let secret = Secret::from_string(s).map_err(|e| SteamUserError::Other(e.to_string()))?;
74 let mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).map_err(|e| SteamUserError::Other(e.to_string()))?;
75 Some(mac)
76 }
77 None => None,
78 };
79
80 let parsed: Vec<(&str, u64)> = token_ids
82 .iter()
83 .map(|id| {
84 let n = id.parse::<u64>().map_err(|_| SteamUserError::Other(format!("Invalid token ID format: {}", id)))?;
85 Ok((*id, n))
86 })
87 .collect::<Result<Vec<_>, SteamUserError>>()?;
88
89 let initial_tokens = self.enumerate_tokens().await?;
91 let active_ids: std::collections::HashSet<u64> = initial_tokens.refresh_tokens.iter().filter_map(|t| t.token_id).collect();
92
93 let mut already_gone = Vec::new();
94 let mut to_revoke = Vec::new();
95
96 for &(id_str, id_u64) in &parsed {
97 if active_ids.contains(&id_u64) {
98 to_revoke.push((id_str, id_u64));
99 } else {
100 already_gone.push(id_str.to_string());
101 }
102 }
103
104 if to_revoke.is_empty() {
105 return Ok(RevokeTokensResult { success: vec![], failed: vec![], already_gone, response: initial_tokens });
106 }
107
108 for &(id_str, id_u64) in &to_revoke {
110 let signature = match &base_mac {
111 Some(mac) => {
112 let mut m = mac.clone();
113 let token_id_ascii = id_str.as_bytes();
115 let len = token_id_ascii.len().min(20);
116 m.update(&token_id_ascii[..len]);
117 Some(m.finalize().into_bytes().to_vec())
118 }
119 None => None,
120 };
121
122 let has_signature = signature.is_some();
123 let request = CAuthenticationRefreshTokenRevokeRequest {
124 token_id: Some(id_u64),
125 steamid: Some(steam_id.steam_id64()),
126 revoke_action: Some(EAuthTokenRevokeAction::Permanent.into()),
127 signature,
128 };
129
130 let mut body = Vec::new();
131 request.encode(&mut body).map_err(|e| SteamUserError::Other(e.to_string()))?;
132
133 let response = match self.post_path("/IAuthenticationService/RevokeRefreshToken/v1").header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token)).query(&[("origin", "https://store.steampowered.com")]).form(&[("input_protobuf_encoded", base64::engine::general_purpose::STANDARD.encode(&body))]).send().await {
134 Ok(resp) => resp,
135 Err(e) => {
136 tracing::error!("[RevokeTokens] HTTP request failed for token {}: {}", id_str, e);
137 continue;
138 }
139 };
140
141 let mut eresult = 1; if let Some(er_val) = response.headers().get("x-eresult") {
144 if let Ok(er_str) = er_val.to_str() {
145 if let Ok(er_num) = er_str.parse::<i32>() {
146 eresult = er_num;
147 }
148 }
149 }
150
151 if !response.status().is_success() || eresult != 1 {
152 let status = response.status();
153 tracing::error!("[RevokeTokens] Steam API rejected revocation for token {}: HTTP {} (EResult: {})", id_str, status, eresult);
154 let _ = response.bytes().await;
156
157 if eresult == 5 && !has_signature {
158 tracing::warn!("[RevokeTokens] Hint: Token {} might require a `shared_secret` to be revoked, but none was provided.", id_str);
159 } else if eresult == 15 && has_signature {
160 tracing::warn!("[RevokeTokens] Hint: Token {} — EResult 15 (BadSignature). Signature was sent but Steam rejected it. The `shared_secret` stored in the DB is likely wrong or stale for this account.", id_str);
161 }
162
163 continue;
164 } else {
165 tracing::debug!("[RevokeTokens] Steam API accepted revocation request for token {}", id_str);
166 let _ = response.bytes().await;
168 }
169 }
170
171 let final_tokens = self.enumerate_tokens().await?;
173 let remaining_ids: std::collections::HashSet<u64> = final_tokens.refresh_tokens.iter().filter_map(|t| t.token_id).collect();
174
175 let mut success = Vec::new();
176 let mut failed = Vec::new();
177
178 for &(id_str, id_u64) in &to_revoke {
179 if remaining_ids.contains(&id_u64) {
180 failed.push(id_str.to_string());
181 } else {
182 success.push(id_str.to_string());
183 }
184 }
185
186 Ok(RevokeTokensResult { success, failed, already_gone, response: final_tokens })
187 }
188}
189
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
192pub struct RevokeTokensResult {
193 pub success: Vec<String>,
195 pub failed: Vec<String>,
197 pub already_gone: Vec<String>,
199 #[serde(skip)]
201 pub response: steam_protos::CAuthenticationRefreshTokenEnumerateResponse,
202}