1#[cfg(not(target_arch = "wasm32"))]
4use crate::{
5 discovery::discover,
6 errors::{Error, Result},
7 http::HttpClient,
8 types::{ExchangeCode, TokenResponse},
9};
10#[cfg(not(target_arch = "wasm32"))]
11use base64::{engine::general_purpose, Engine as _};
12
13#[cfg(not(target_arch = "wasm32"))]
39pub async fn exchange_code(params: ExchangeCode, http: &dyn HttpClient) -> Result<TokenResponse> {
40 validate_exchange_params(¶ms)?;
42
43 let cache = crate::cache::NoOpCache;
45 let metadata = discover(¶ms.issuer, http, &cache).await?;
46
47 let mut form = vec![
49 ("grant_type".to_string(), "authorization_code".to_string()),
50 ("code".to_string(), params.code.clone()),
51 ("redirect_uri".to_string(), params.redirect_uri.clone()),
52 ];
53
54 if let Some(verifier) = ¶ms.code_verifier {
56 if !verifier.is_empty() {
57 form.push(("code_verifier".to_string(), verifier.clone()));
58 }
59 }
60
61 let auth_method = params.token_endpoint_auth_method
63 .as_deref()
64 .unwrap_or(if params.client_secret.is_some() {
65 "client_secret_basic"
66 } else {
67 "none"
68 });
69
70 let auth_header = match auth_method {
71 "client_secret_basic" => {
72 if let Some(client_secret) = ¶ms.client_secret {
73 let credentials = format!("{}:{}", params.client_id, client_secret);
74 let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
75 Some(("Authorization".to_string(), format!("Basic {}", encoded)))
76 } else {
77 return Err(Error::InvalidParam("client_secret_basic requires client_secret"));
78 }
79 },
80 "client_secret_post" => {
81 form.push(("client_id".to_string(), params.client_id.clone()));
83 if let Some(client_secret) = ¶ms.client_secret {
84 form.push(("client_secret".to_string(), client_secret.clone()));
85 } else {
86 return Err(Error::InvalidParam("client_secret_post requires client_secret"));
87 }
88 None
89 },
90 "none" | "public" => {
91 form.push(("client_id".to_string(), params.client_id.clone()));
93 None
94 },
95 "private_key_jwt" | "client_secret_jwt" => {
96 return Err(Error::InvalidParam(
99 "JWT-based authentication methods are not yet supported"
100 ));
101 },
102 _ => {
103 return Err(Error::InvalidParam(
104 "Unknown authentication method"
105 ));
106 }
107 };
108
109 let response = http
111 .post_form_value(
112 &metadata.token_endpoint,
113 &form,
114 auth_header.as_ref().map(|(k, v)| (k.as_str(), v.as_str())),
115 )
116 .await
117 .map_err(|e| {
118 if let crate::http::HttpClientError::InvalidStatus { status: _, message } = &e {
120 if let Ok(oauth_error) = serde_json::from_str::<OAuthError>(&message) {
121 return Error::oauth(oauth_error.error, oauth_error.error_description);
122 }
123 }
124 Error::Network(format!("Token exchange failed: {}", e))
125 })?;
126
127 let tokens: TokenResponse = serde_json::from_value(response)?;
129
130 Ok(tokens)
131}
132
133#[cfg(not(target_arch = "wasm32"))]
135fn validate_exchange_params(params: &ExchangeCode) -> Result<()> {
136 if params.issuer.is_empty() {
137 return Err(Error::InvalidParam("issuer cannot be empty"));
138 }
139 if params.client_id.is_empty() {
140 return Err(Error::InvalidParam("client_id cannot be empty"));
141 }
142 if params.code.is_empty() {
143 return Err(Error::InvalidParam("code cannot be empty"));
144 }
145 if params.redirect_uri.is_empty() {
146 return Err(Error::InvalidParam("redirect_uri cannot be empty"));
147 }
148 if params.client_secret.is_none() && params.code_verifier.as_ref().map_or(true, |v| v.is_empty()) {
150 return Err(Error::InvalidParam("code_verifier is required for public clients"));
151 }
152 Ok(())
153}
154
155#[cfg(not(target_arch = "wasm32"))]
157#[derive(serde::Deserialize)]
158struct OAuthError {
159 error: String,
160 error_description: Option<String>,
161}
162
163#[cfg(not(target_arch = "wasm32"))]
165#[allow(dead_code)]
166pub async fn exchange_code_with_endpoint(
167 params: ExchangeCode,
168 token_endpoint: &str,
169 http: &dyn HttpClient,
170) -> Result<TokenResponse> {
171 validate_exchange_params(¶ms)?;
173
174 let mut form = vec![
176 ("grant_type".to_string(), "authorization_code".to_string()),
177 ("code".to_string(), params.code.clone()),
178 ("redirect_uri".to_string(), params.redirect_uri.clone()),
179 ];
180
181 if let Some(verifier) = ¶ms.code_verifier {
183 if !verifier.is_empty() {
184 form.push(("code_verifier".to_string(), verifier.clone()));
185 }
186 }
187
188 let auth_header = if let Some(client_secret) = ¶ms.client_secret {
190 let use_post = params
192 .token_endpoint_auth_method
193 .as_ref()
194 .map(|m| m == "client_secret_post")
195 .unwrap_or(false);
196
197 if use_post {
198 form.push(("client_id".to_string(), params.client_id.clone()));
199 form.push(("client_secret".to_string(), client_secret.clone()));
200 None
201 } else {
202 let credentials = format!("{}:{}", params.client_id, client_secret);
204 let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
205 Some(("Authorization".to_string(), format!("Basic {}", encoded)))
206 }
207 } else {
208 form.push(("client_id".to_string(), params.client_id.clone()));
210 None
211 };
212
213 let response = http
215 .post_form_value(
216 token_endpoint,
217 &form,
218 auth_header.as_ref().map(|(k, v)| (k.as_str(), v.as_str())),
219 )
220 .await
221 .map_err(|e| {
222 if let crate::http::HttpClientError::InvalidStatus { status: _, message } = &e {
224 if let Ok(oauth_error) = serde_json::from_str::<OAuthError>(&message) {
225 return Error::oauth(oauth_error.error, oauth_error.error_description);
226 }
227 }
228 Error::Network(format!("Token exchange failed: {}", e))
229 })?;
230
231 let tokens: TokenResponse = serde_json::from_value(response)?;
233
234 Ok(tokens)
235}
236
237
238#[cfg(not(target_arch = "wasm32"))]
265pub async fn refresh_token(
266 params: crate::types::RefreshTokenRequest,
267 http: &dyn HttpClient,
268) -> Result<TokenResponse> {
269 if params.issuer.is_empty() {
271 return Err(Error::InvalidParam("issuer cannot be empty"));
272 }
273 if params.client_id.is_empty() {
274 return Err(Error::InvalidParam("client_id cannot be empty"));
275 }
276 if params.refresh_token.is_empty() {
277 return Err(Error::InvalidParam("refresh_token cannot be empty"));
278 }
279
280 let cache = crate::cache::NoOpCache;
282 let metadata = discover(¶ms.issuer, http, &cache).await?;
283
284 let mut form = vec![
286 ("grant_type".to_string(), "refresh_token".to_string()),
287 ("refresh_token".to_string(), params.refresh_token.clone()),
288 ];
289
290 if let Some(scope) = ¶ms.scope {
292 if !scope.is_empty() {
293 form.push(("scope".to_string(), scope.clone()));
294 }
295 }
296
297 let auth_method = params.token_endpoint_auth_method
299 .as_deref()
300 .unwrap_or(if params.client_secret.is_some() {
301 "client_secret_basic"
302 } else {
303 "none"
304 });
305
306 let auth_header = match auth_method {
307 "client_secret_basic" => {
308 if let Some(client_secret) = ¶ms.client_secret {
309 let credentials = format!("{}:{}", params.client_id, client_secret);
310 let encoded = general_purpose::STANDARD.encode(credentials.as_bytes());
311 Some(("Authorization".to_string(), format!("Basic {}", encoded)))
312 } else {
313 return Err(Error::InvalidParam("client_secret_basic requires client_secret"));
314 }
315 },
316 "client_secret_post" => {
317 form.push(("client_id".to_string(), params.client_id.clone()));
319 if let Some(client_secret) = ¶ms.client_secret {
320 form.push(("client_secret".to_string(), client_secret.clone()));
321 } else {
322 return Err(Error::InvalidParam("client_secret_post requires client_secret"));
323 }
324 None
325 },
326 "none" | "public" => {
327 form.push(("client_id".to_string(), params.client_id.clone()));
329 None
330 },
331 "private_key_jwt" | "client_secret_jwt" => {
332 return Err(Error::InvalidParam(
334 "JWT-based authentication methods are not yet supported"
335 ));
336 },
337 _ => {
338 return Err(Error::InvalidParam(
339 "Unknown authentication method"
340 ));
341 }
342 };
343
344 let response = http
346 .post_form_value(
347 &metadata.token_endpoint,
348 &form,
349 auth_header.as_ref().map(|(k, v)| (k.as_str(), v.as_str())),
350 )
351 .await
352 .map_err(|e| {
353 if let crate::http::HttpClientError::InvalidStatus { status: _, message } = &e {
355 if let Ok(oauth_error) = serde_json::from_str::<OAuthError>(&message) {
356 return Error::oauth(oauth_error.error, oauth_error.error_description);
357 }
358 }
359 Error::Network(format!("Token refresh failed: {}", e))
360 })?;
361
362 let tokens: TokenResponse = serde_json::from_value(response)?;
364
365 Ok(tokens)
366}
367
368#[cfg(target_arch = "wasm32")]
370pub async fn exchange_code(
371 _params: crate::types::ExchangeCode,
372 _http: &dyn crate::http::HttpClient,
373) -> crate::errors::Result<crate::types::TokenResponse> {
374 Err(crate::errors::Error::ServerOnly)
375}
376
377#[cfg(target_arch = "wasm32")]
378pub async fn refresh_token(
379 _params: crate::types::RefreshTokenRequest,
380 _http: &dyn crate::http::HttpClient,
381) -> crate::errors::Result<crate::types::TokenResponse> {
382 Err(crate::errors::Error::ServerOnly)
383}
384
385#[cfg(all(test, not(target_arch = "wasm32")))]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_validate_exchange_params() {
391 let valid = ExchangeCode {
392 issuer: "https://auth.example.com".into(),
393 client_id: "test-client".into(),
394 code: "auth_code".into(),
395 redirect_uri: "https://app.example.com/callback".into(),
396 code_verifier: Some("verifier".into()),
397 client_secret: None,
398 token_endpoint_auth_method: None,
399 };
400 assert!(validate_exchange_params(&valid).is_ok());
401
402 let invalid = ExchangeCode { issuer: "".into(), ..valid.clone() };
403 assert!(validate_exchange_params(&invalid).is_err());
404 }
405}