Skip to main content

oxihuman_core/
oauth2_stub.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! OAuth2 token stub — PKCE flow and token exchange helpers.
6
7/// PKCE code verifier and challenge pair.
8#[derive(Clone, Debug)]
9pub struct PkceChallenge {
10    pub code_verifier: String,
11    pub code_challenge: String,
12}
13
14/// An OAuth2 access token with optional refresh token.
15#[derive(Clone, Debug)]
16pub struct OAuth2Token {
17    pub access_token: String,
18    pub token_type: String,
19    pub expires_in_secs: u64,
20    pub refresh_token: Option<String>,
21    pub scope: Option<String>,
22}
23
24/// Configuration for an OAuth2 client.
25#[derive(Clone, Debug)]
26pub struct OAuth2Config {
27    pub client_id: String,
28    pub redirect_uri: String,
29    pub auth_endpoint: String,
30    pub token_endpoint: String,
31}
32
33/// An OAuth2 stub client that manages the PKCE flow.
34pub struct OAuth2Client {
35    pub config: OAuth2Config,
36    pending_verifier: Option<String>,
37}
38
39/// Generates a stub PKCE challenge from a verifier string.
40pub fn generate_pkce_challenge(verifier: &str) -> PkceChallenge {
41    let challenge = format!("sha256:{}", &verifier[..verifier.len().min(32)]);
42    PkceChallenge {
43        code_verifier: verifier.to_owned(),
44        code_challenge: challenge,
45    }
46}
47
48/// Builds the authorization URL with PKCE parameters.
49pub fn build_authorization_url(
50    cfg: &OAuth2Config,
51    challenge: &PkceChallenge,
52    state: &str,
53) -> String {
54    format!(
55        "{}?client_id={}&redirect_uri={}&code_challenge={}&state={}",
56        cfg.auth_endpoint, cfg.client_id, cfg.redirect_uri, challenge.code_challenge, state
57    )
58}
59
60/// Stub: exchanges an authorization code for an OAuth2 token.
61pub fn exchange_code_for_token(
62    cfg: &OAuth2Config,
63    code: &str,
64    verifier: &str,
65) -> Result<OAuth2Token, String> {
66    if code.is_empty() {
67        return Err("empty authorization code".into());
68    }
69    if verifier.is_empty() {
70        return Err("empty code verifier".into());
71    }
72    Ok(OAuth2Token {
73        access_token: format!("at_{}", code),
74        token_type: "Bearer".into(),
75        expires_in_secs: 3600,
76        refresh_token: Some(format!("rt_{}", cfg.client_id)),
77        scope: Some("openid profile".into()),
78    })
79}
80
81/// Stub: refreshes an OAuth2 token using a refresh token.
82pub fn refresh_token(cfg: &OAuth2Config, refresh: &str) -> Result<OAuth2Token, String> {
83    if refresh.is_empty() {
84        return Err("empty refresh token".into());
85    }
86    Ok(OAuth2Token {
87        access_token: format!("at_refreshed_{}", cfg.client_id),
88        token_type: "Bearer".into(),
89        expires_in_secs: 3600,
90        refresh_token: Some(refresh.to_owned()),
91        scope: None,
92    })
93}
94
95impl OAuth2Client {
96    /// Creates a new OAuth2 client from config.
97    pub fn new(config: OAuth2Config) -> Self {
98        Self {
99            config,
100            pending_verifier: None,
101        }
102    }
103
104    /// Starts the PKCE flow, returning the authorization URL.
105    pub fn start_pkce_flow(&mut self, verifier: &str, state: &str) -> String {
106        let challenge = generate_pkce_challenge(verifier);
107        self.pending_verifier = Some(verifier.to_owned());
108        build_authorization_url(&self.config, &challenge, state)
109    }
110
111    /// Completes the PKCE flow by exchanging the auth code.
112    pub fn complete_flow(&mut self, code: &str) -> Result<OAuth2Token, String> {
113        let verifier = self.pending_verifier.take().ok_or("no pending flow")?;
114        exchange_code_for_token(&self.config, code, &verifier)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_pkce_challenge_contains_verifier_prefix() {
124        let ch = generate_pkce_challenge("my-verifier-string");
125        assert!(ch.code_challenge.contains("sha256:"));
126    }
127
128    #[test]
129    fn test_pkce_verifier_preserved() {
130        let verifier = "verifier-abc";
131        let ch = generate_pkce_challenge(verifier);
132        assert_eq!(ch.code_verifier, verifier);
133    }
134
135    #[test]
136    fn test_build_authorization_url_contains_client_id() {
137        let cfg = OAuth2Config {
138            client_id: "cid".into(),
139            redirect_uri: "http://localhost".into(),
140            auth_endpoint: "https://auth.example.com/auth".into(),
141            token_endpoint: "https://auth.example.com/token".into(),
142        };
143        let ch = generate_pkce_challenge("ver");
144        let url = build_authorization_url(&cfg, &ch, "state1");
145        assert!(url.contains("cid"));
146    }
147
148    #[test]
149    fn test_exchange_empty_code_returns_error() {
150        let cfg = OAuth2Config {
151            client_id: "cid".into(),
152            redirect_uri: "".into(),
153            auth_endpoint: "".into(),
154            token_endpoint: "".into(),
155        };
156        assert!(exchange_code_for_token(&cfg, "", "verifier").is_err());
157    }
158
159    #[test]
160    fn test_exchange_valid_code_returns_token() {
161        let cfg = OAuth2Config {
162            client_id: "cid".into(),
163            redirect_uri: "".into(),
164            auth_endpoint: "".into(),
165            token_endpoint: "".into(),
166        };
167        let tok = exchange_code_for_token(&cfg, "code123", "verifier").expect("should succeed");
168        assert!(tok.access_token.contains("code123"));
169    }
170
171    #[test]
172    fn test_refresh_empty_token_returns_error() {
173        let cfg = OAuth2Config {
174            client_id: "cid".into(),
175            redirect_uri: "".into(),
176            auth_endpoint: "".into(),
177            token_endpoint: "".into(),
178        };
179        assert!(refresh_token(&cfg, "").is_err());
180    }
181
182    #[test]
183    fn test_refresh_valid_token_returns_new_token() {
184        let cfg = OAuth2Config {
185            client_id: "myclient".into(),
186            redirect_uri: "".into(),
187            auth_endpoint: "".into(),
188            token_endpoint: "".into(),
189        };
190        let tok = refresh_token(&cfg, "rt_old").expect("should succeed");
191        assert!(tok.access_token.contains("myclient"));
192    }
193
194    #[test]
195    fn test_client_start_and_complete_flow() {
196        let cfg = OAuth2Config {
197            client_id: "c1".into(),
198            redirect_uri: "http://localhost".into(),
199            auth_endpoint: "https://auth.test/auth".into(),
200            token_endpoint: "https://auth.test/token".into(),
201        };
202        let mut client = OAuth2Client::new(cfg);
203        let url = client.start_pkce_flow("verifier-xyz", "s1");
204        assert!(url.contains("c1"));
205        let tok = client.complete_flow("authcode").expect("should succeed");
206        assert!(!tok.access_token.is_empty());
207    }
208
209    #[test]
210    fn test_complete_flow_without_start_errors() {
211        let cfg = OAuth2Config {
212            client_id: "c2".into(),
213            redirect_uri: "".into(),
214            auth_endpoint: "".into(),
215            token_endpoint: "".into(),
216        };
217        let mut client = OAuth2Client::new(cfg);
218        assert!(client.complete_flow("code").is_err());
219    }
220}