Skip to main content

game_manager_lib/utils/oauth/
core.rs

1//! Utilitários genéricos para fluxo OAuth2 com PKCE.
2//!
3//! Fornece funcionalidades para gerar desafios PKCE e iniciar um servidor temporário
4//! para capturar o código de autorização retornado pelo provedor OAuth2.
5
6use crate::constants::OAUTH_CALLBACK_TIMEOUT_SECS;
7use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
8use rand::{distr::Alphanumeric, RngExt};
9use sha2::{Digest, Sha256};
10use std::{sync::mpsc, thread, time::Duration};
11use tiny_http::{Response, Server};
12use url::Url;
13
14/// Estrutura para o desafio PKCE (Proof Key for Code Exchange).
15pub struct PkceChallenge {
16    pub verifier: String,
17    pub challenge: String,
18}
19
20/// Resultado do callback OAuth capturado no servidor local.
21pub struct AuthCallbackResult {
22    pub code: String,
23    pub state: Option<String>,
24}
25
26impl PkceChallenge {
27    pub fn generate() -> Self {
28        let verifier: String = rand::rng()
29            .sample_iter(&Alphanumeric)
30            .take(64)
31            .map(char::from)
32            .collect();
33
34        let hash = Sha256::digest(verifier.as_bytes());
35        let challenge = URL_SAFE_NO_PAD.encode(hash);
36
37        Self {
38            verifier,
39            challenge,
40        }
41    }
42}
43
44/// Gera um valor aleatório para o parâmetro `state` (proteção CSRF).
45pub fn generate_state() -> String {
46    rand::rng()
47        .sample_iter(&Alphanumeric)
48        .take(32)
49        .map(char::from)
50        .collect()
51}
52
53/// Inicia um servidor temporário para receber o callback do OAuth.
54/// Bloqueia a thread até receber o código ou dar timeout.
55pub fn wait_for_auth_code(port: u16) -> Result<AuthCallbackResult, String> {
56    let address = format!("127.0.0.1:{}", port);
57    // Tenta iniciar o servidor
58    let server =
59        Server::http(&address).map_err(|e| format!("Porta ocupada ou erro de rede: {}", e))?;
60
61    let (tx, rx) = mpsc::channel::<AuthCallbackResult>();
62
63    thread::spawn(move || {
64        // Aguarda requisição (Timeout configurável)
65        if let Ok(Some(request)) =
66            server.recv_timeout(Duration::from_secs(OAUTH_CALLBACK_TIMEOUT_SECS))
67        {
68            // Parser da URL de callback
69            let url_string = format!("http://localhost{}", request.url());
70            if let Ok(url) = Url::parse(&url_string) {
71                // Busca o parâmetro ?code=...
72                if let Some((_, code)) = url.query_pairs().find(|(k, _)| k == "code") {
73                    let state = url
74                        .query_pairs()
75                        .find(|(k, _)| k == "state")
76                        .map(|(_, v)| v.to_string());
77                    let _ = tx.send(AuthCallbackResult {
78                        code: code.to_string(),
79                        state,
80                    });
81
82                    // Resposta bonita para o usuário no navegador
83                    let html = "
84                        <html>
85                        <body style='background:#1a1a1a; color:#fff; font-family:sans-serif; text-align:center; padding-top:50px;'>
86                            <h1 style='color:#4ade80'>Login Concluído!</h1>
87                            <p>Você já pode fechar esta janela e voltar para o Playlite.</p>
88                            <script>window.close();</script>
89                        </body>
90                        </html>
91                    ";
92                    let _ = request.respond(
93                        Response::from_string(html).with_header(
94                            tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..])
95                                .unwrap(),
96                        ),
97                    );
98                    return;
99                }
100            }
101            // Se não achou code, responde erro
102            let _ = request.respond(Response::from_string("Erro: Código não encontrado."));
103        }
104    });
105
106    // Aguarda o canal enviar o código
107    rx.recv_timeout(Duration::from_secs(OAUTH_CALLBACK_TIMEOUT_SECS))
108        .map_err(|_| "Tempo limite de login excedido.".to_string())
109}