oauth2_apple/
client_credentials_grant.rs

1use oauth2_client::{
2    re_exports::{Body, ClientId, ClientSecret, Request, Url, UrlParseError},
3    Provider, ProviderExtClientCredentialsGrant,
4};
5
6use crate::{AppleScope, OAUTH2_TOKEN_URL};
7
8//
9// https://developer.apple.com/documentation/apple_search_ads/implementing_oauth_for_the_apple_search_ads_api
10//
11#[derive(Debug, Clone)]
12pub struct AppleProviderForSearchAdsApi {
13    client_id: ClientId,
14    client_secret: ClientSecret,
15    //
16    token_endpoint_url: Url,
17}
18impl AppleProviderForSearchAdsApi {
19    pub fn new(client_id: ClientId, client_secret: ClientSecret) -> Result<Self, UrlParseError> {
20        Ok(Self {
21            client_id,
22            client_secret,
23            token_endpoint_url: OAUTH2_TOKEN_URL.parse()?,
24        })
25    }
26}
27impl Provider for AppleProviderForSearchAdsApi {
28    type Scope = AppleScope;
29
30    fn client_id(&self) -> Option<&ClientId> {
31        Some(&self.client_id)
32    }
33
34    fn client_secret(&self) -> Option<&ClientSecret> {
35        Some(&self.client_secret)
36    }
37
38    fn token_endpoint_url(&self) -> &Url {
39        &self.token_endpoint_url
40    }
41}
42impl ProviderExtClientCredentialsGrant for AppleProviderForSearchAdsApi {
43    fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
44        Some(vec![AppleScope::Searchadsorg])
45    }
46
47    fn access_token_request_url_modifying(&self, url: &mut Url) {
48        let mut query_pairs_mut = url.query_pairs_mut();
49        query_pairs_mut.clear();
50
51        query_pairs_mut.append_pair("grant_type", "client_credentials");
52        query_pairs_mut.append_pair("scope", AppleScope::Searchadsorg.to_string().as_str());
53        query_pairs_mut.append_pair("client_id", &self.client_id);
54        query_pairs_mut.append_pair("client_secret", &self.client_secret);
55
56        query_pairs_mut.finish();
57    }
58
59    fn access_token_request_modifying(&self, request: &mut Request<Body>) {
60        let body = request.body_mut();
61        body.clear();
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    use oauth2_client::{
70        client_credentials_grant::AccessTokenEndpoint,
71        re_exports::{Endpoint as _, Response},
72    };
73
74    #[test]
75    fn access_token_request_for_search_ads_api() -> Result<(), Box<dyn std::error::Error>> {
76        let provider = AppleProviderForSearchAdsApi::new(
77            "SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577".to_owned(),
78            "eyJhbGciOiJFUzI1NiIsImtpZCI6ImJhY2FlYmRhLWUyMTktNDFlZS1hOTA3LWUyYzI1YjI0ZDFiMiJ9.eyJpc3MiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3IiwiaWF0IjoxNjU0NDczNjAwLCJleHAiOjE2NzAwMjU2MDAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3In0.bN3KRWDJft-rjqRbOuuzfsImPT4RPEy01ILYJRBe4v_WJtJdi-7xBpi9UCcSN1WRe3Ozobvou5ruxXjVFnB_6Q".to_owned(),
79        )?;
80
81        let request = AccessTokenEndpoint::new(&provider, None).render_request()?;
82
83        assert_eq!(request.method(), "POST");
84        assert_eq!(
85            request.uri(),
86            "https://appleid.apple.com/auth/oauth2/token?grant_type=client_credentials&scope=searchadsorg&client_id=SEARCHADS.27478e71-3bb0-4588-998c-182e2b405577&client_secret=eyJhbGciOiJFUzI1NiIsImtpZCI6ImJhY2FlYmRhLWUyMTktNDFlZS1hOTA3LWUyYzI1YjI0ZDFiMiJ9.eyJpc3MiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3IiwiaWF0IjoxNjU0NDczNjAwLCJleHAiOjE2NzAwMjU2MDAsImF1ZCI6Imh0dHBzOi8vYXBwbGVpZC5hcHBsZS5jb20iLCJzdWIiOiJTRUFSQ0hBRFMuMjc0NzhlNzEtM2JiMC00NTg4LTk5OGMtMTgyZTJiNDA1NTc3In0.bN3KRWDJft-rjqRbOuuzfsImPT4RPEy01ILYJRBe4v_WJtJdi-7xBpi9UCcSN1WRe3Ozobvou5ruxXjVFnB_6Q"
87        );
88        assert_eq!(
89            request.headers().get("Content-Type").unwrap().as_bytes(),
90            b"application/x-www-form-urlencoded"
91        );
92        assert_eq!(request.body(), b"");
93
94        Ok(())
95    }
96
97    #[test]
98    fn access_token_response_for_search_ads_api() -> Result<(), Box<dyn std::error::Error>> {
99        let provider =
100            AppleProviderForSearchAdsApi::new("CLIENT_ID".to_owned(), "CLIENT_SECRET".to_owned())?;
101
102        let response_body =
103            include_str!("../tests/response_body_json_files/access_token_for_search_ads_api.json");
104        let body_ret = AccessTokenEndpoint::new(&provider, None)
105            .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
106
107        match body_ret {
108            Ok(body) => {
109                assert_eq!(body.expires_in, Some(3600));
110            }
111            Err(body) => panic!("{body:?}"),
112        }
113
114        Ok(())
115    }
116}