game_manager_lib/utils/oauth/
config.rs1use crate::errors::AppError;
7use crate::utils::http_client::HTTP_CLIENT;
8use crate::utils::oauth::core::PkceChallenge;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::time::{SystemTime, UNIX_EPOCH};
12use url::Url;
13
14#[derive(Debug, Clone, Copy, PartialEq)]
17pub enum TokenRequestMethod {
18 Post,
19 Get,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum TokenAuthMethod {
24 Body,
26 BasicHeader,
28}
29
30#[derive(Debug, Clone)]
34pub struct OAuthProviderConfig {
35 pub provider_id: &'static str,
36 pub client_id: String,
37 pub client_secret: Option<String>,
38 pub authorize_endpoint: String,
39 pub token_endpoint: String,
40 pub redirect_uri: String,
41 pub scopes: Vec<String>,
42 pub uses_pkce: bool,
43 pub extra_params: Vec<(String, String)>,
44 pub token_request_method: TokenRequestMethod,
45 pub token_auth_method: TokenAuthMethod,
46}
47
48#[derive(Debug, Deserialize)]
50pub struct TokenResponse {
51 pub access_token: String,
52 pub refresh_token: Option<String>,
53 #[serde(default)]
54 pub expires_in: Option<u64>, #[serde(default)]
56 pub token_type: Option<String>,
57 #[serde(default, deserialize_with = "deserialize_scope")]
58 pub scope: Option<String>,
59}
60
61#[derive(Debug, Serialize, Deserialize, Clone)]
63pub struct OAuthToken {
64 pub access_token: String,
65 pub refresh_token: Option<String>,
66 pub expires_at: Option<i64>, pub scope: Option<String>,
68 #[serde(default)]
69 pub extra: HashMap<String, String>, }
71
72impl From<TokenResponse> for OAuthToken {
73 fn from(resp: TokenResponse) -> Self {
74 let expires_at = resp.expires_in.map(|secs| now_unix() + secs as i64);
75 Self {
76 access_token: resp.access_token,
77 refresh_token: resp.refresh_token,
78 expires_at,
79 scope: resp.scope,
80 extra: HashMap::new(),
81 }
82 }
83}
84
85impl OAuthToken {
86 pub fn is_expired(&self) -> bool {
88 match self.expires_at {
89 Some(exp) => now_unix() >= exp - 60,
90 None => false,
91 }
92 }
93}
94
95pub(crate) fn now_unix() -> i64 {
98 SystemTime::now()
99 .duration_since(UNIX_EPOCH)
100 .unwrap_or_default()
101 .as_secs() as i64
102}
103
104fn deserialize_scope<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
108where
109 D: serde::Deserializer<'de>,
110{
111 #[derive(Deserialize)]
112 #[serde(untagged)]
113 enum ScopeField {
114 Text(String),
115 List(Vec<String>),
116 }
117
118 let opt = Option::<ScopeField>::deserialize(deserializer)?;
119 Ok(match opt {
120 Some(ScopeField::Text(s)) => Some(s),
121 Some(ScopeField::List(list)) if list.is_empty() => None,
122 Some(ScopeField::List(list)) => Some(list.join(" ")),
123 None => None,
124 })
125}
126
127async fn parse_token_response(
128 response: reqwest::Response,
129 provider_id: &str,
130 stage: &str,
131) -> Result<TokenResponse, AppError> {
132 let status = response.status();
133 let body = response.text().await.unwrap_or_default();
134
135 if !status.is_success() {
136 return Err(AppError::OAuthTokenExchangeError(format!(
137 "[{provider_id}] falha em '{stage}': HTTP {status} - {body}"
138 )));
139 }
140
141 serde_json::from_str::<TokenResponse>(&body).map_err(|e| {
142 AppError::OAuthTokenExchangeError(format!(
143 "[{provider_id}] resposta inesperada em '{stage}': {e}"
144 ))
145 })
146}
147
148fn build_token_request(
151 config: &OAuthProviderConfig,
152 mut params: Vec<(&'static str, String)>,
153) -> reqwest::RequestBuilder {
154 let mut request = match config.token_request_method {
155 TokenRequestMethod::Get => HTTP_CLIENT.get(&config.token_endpoint),
156 TokenRequestMethod::Post => HTTP_CLIENT.post(&config.token_endpoint),
157 };
158
159 match config.token_auth_method {
160 TokenAuthMethod::Body => {
161 params.push(("client_id", config.client_id.clone()));
162 if let Some(secret) = &config.client_secret {
163 params.push(("client_secret", secret.clone()));
164 }
165 }
166 TokenAuthMethod::BasicHeader => {
167 let secret = config.client_secret.clone().unwrap_or_default();
168 request = request.basic_auth(&config.client_id, Some(secret));
169 }
170 }
171
172 match config.token_request_method {
173 TokenRequestMethod::Get => request.query(¶ms),
174 TokenRequestMethod::Post => request.form(¶ms),
175 }
176}
177
178pub fn build_authorize_url(
182 config: &OAuthProviderConfig,
183 pkce: Option<&PkceChallenge>,
184 state: &str,
185) -> Result<Url, AppError> {
186 let mut url = Url::parse(&config.authorize_endpoint)
187 .map_err(|e| AppError::OAuthConfigError(format!("URL de autorização inválida: {e}")))?;
188
189 {
190 let mut query = url.query_pairs_mut();
191 query
192 .append_pair("client_id", &config.client_id)
193 .append_pair("redirect_uri", &config.redirect_uri)
194 .append_pair("response_type", "code")
195 .append_pair("state", state);
196
197 if !config.scopes.is_empty() {
198 query.append_pair("scope", &config.scopes.join(" "));
199 }
200
201 if config.uses_pkce {
202 let pkce = pkce.ok_or_else(|| {
203 AppError::OAuthConfigError(
204 "uses_pkce=true mas nenhum PkceChallenge foi fornecido".into(),
205 )
206 })?;
207 query
208 .append_pair("code_challenge", &pkce.challenge)
209 .append_pair("code_challenge_method", "S256");
210 }
211
212 for (key, value) in &config.extra_params {
213 query.append_pair(key, value);
214 }
215 }
216
217 Ok(url)
218}
219
220pub async fn exchange_code_for_token(
222 config: &OAuthProviderConfig,
223 code: &str,
224 pkce_verifier: Option<&str>,
225) -> Result<TokenResponse, AppError> {
226 let mut params = vec![
227 ("grant_type", "authorization_code".to_string()),
228 ("code", code.to_string()),
229 ("redirect_uri", config.redirect_uri.clone()),
230 ];
231 if let Some(verifier) = pkce_verifier {
232 params.push(("code_verifier", verifier.to_string()));
233 }
234
235 let response = build_token_request(config, params).send().await?;
236 parse_token_response(response, config.provider_id, "exchange").await
237}
238
239pub async fn refresh_access_token(
241 config: &OAuthProviderConfig,
242 refresh_token: &str,
243) -> Result<TokenResponse, AppError> {
244 let params = vec![
245 ("grant_type", "refresh_token".to_string()),
246 ("refresh_token", refresh_token.to_string()),
247 ];
248
249 let response = build_token_request(config, params).send().await?;
250 parse_token_response(response, config.provider_id, "refresh").await
251}