Skip to main content

shopify_client/oauth/
mod.rs

1pub mod types;
2
3pub use types::AccessTokenResponse;
4
5use crate::common::http::http_client;
6use crate::common::types::APIError;
7
8const TOKEN_EXCHANGE_GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:token-exchange";
9const ID_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:id_token";
10const OFFLINE_ACCESS_TOKEN_TYPE: &str = "urn:shopify:params:oauth:token-type:offline-access-token";
11
12pub async fn exchange_session_token(
13    shop_url: &str,
14    id_token: &str,
15    client_id: &str,
16    client_secret: &str,
17) -> Result<AccessTokenResponse, APIError> {
18    let body = serde_json::json!({
19        "client_id": client_id,
20        "client_secret": client_secret,
21        "grant_type": TOKEN_EXCHANGE_GRANT_TYPE,
22        "subject_token": id_token,
23        "subject_token_type": ID_TOKEN_TYPE,
24        "requested_token_type": OFFLINE_ACCESS_TOKEN_TYPE,
25    });
26
27    request_access_token(shop_url, &body).await
28}
29
30pub async fn exchange_code(
31    shop_url: &str,
32    code: &str,
33    client_id: &str,
34    client_secret: &str,
35) -> Result<AccessTokenResponse, APIError> {
36    let body = serde_json::json!({
37        "client_id": client_id,
38        "client_secret": client_secret,
39        "code": code,
40    });
41
42    request_access_token(shop_url, &body).await
43}
44
45async fn request_access_token(
46    shop_url: &str,
47    body: &serde_json::Value,
48) -> Result<AccessTokenResponse, APIError> {
49    let endpoint = format!(
50        "{}/admin/oauth/access_token",
51        shop_url.trim_end_matches('/')
52    );
53
54    let response = http_client()
55        .post(&endpoint)
56        .header("Content-Type", "application/json")
57        .json(body)
58        .send()
59        .await
60        .map_err(|_| APIError::NetworkError)?;
61
62    let status = response.status();
63    let response_text = response.text().await.map_err(|_| APIError::FailedToParse)?;
64
65    if !status.is_success() {
66        return Err(APIError::ServerError {
67            errors: format!("{}: {}", status, response_text),
68        });
69    }
70
71    serde_json::from_str::<AccessTokenResponse>(&response_text).map_err(|_| APIError::FailedToParse)
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn deserialize_access_token_response_with_scope() {
80        let json = r#"{"access_token":"shpat_abc123","scope":"read_products,write_products"}"#;
81        let resp: AccessTokenResponse = serde_json::from_str(json).unwrap();
82        assert_eq!(resp.access_token, "shpat_abc123");
83        assert_eq!(resp.scope.as_deref(), Some("read_products,write_products"));
84    }
85
86    #[test]
87    fn deserialize_access_token_response_without_scope() {
88        let json = r#"{"access_token":"shpat_abc123"}"#;
89        let resp: AccessTokenResponse = serde_json::from_str(json).unwrap();
90        assert_eq!(resp.access_token, "shpat_abc123");
91        assert_eq!(resp.scope, None);
92    }
93}