oauth2_apple/
authorization_code_grant.rs

1use oauth2_client::{
2    authorization_code_grant::provider_ext::ProviderExtAuthorizationCodeGrantOidcSupportType,
3    re_exports::{ClientId, ClientSecret, RedirectUri, Url, UrlParseError},
4    Provider, ProviderExtAuthorizationCodeGrant,
5};
6
7use crate::{AppleScope, AUTHORIZATION_URL, TOKEN_URL};
8
9#[derive(Debug, Clone)]
10pub struct AppleProviderWithAppleJs {
11    client_id: ClientId,
12    client_secret: ClientSecret,
13    redirect_uri: RedirectUri,
14    //
15    token_endpoint_url: Url,
16    authorization_endpoint_url: Url,
17}
18impl AppleProviderWithAppleJs {
19    pub fn new(
20        client_id: ClientId,
21        client_secret: ClientSecret,
22        redirect_uri: RedirectUri,
23    ) -> Result<Self, UrlParseError> {
24        Ok(Self {
25            client_id,
26            client_secret,
27            redirect_uri,
28            token_endpoint_url: TOKEN_URL.parse()?,
29            authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
30        })
31    }
32}
33impl Provider for AppleProviderWithAppleJs {
34    type Scope = AppleScope;
35
36    fn client_id(&self) -> Option<&ClientId> {
37        Some(&self.client_id)
38    }
39
40    fn client_secret(&self) -> Option<&ClientSecret> {
41        Some(&self.client_secret)
42    }
43
44    fn token_endpoint_url(&self) -> &Url {
45        &self.token_endpoint_url
46    }
47}
48impl ProviderExtAuthorizationCodeGrant for AppleProviderWithAppleJs {
49    fn redirect_uri(&self) -> Option<&RedirectUri> {
50        Some(&self.redirect_uri)
51    }
52
53    fn oidc_support_type(&self) -> Option<ProviderExtAuthorizationCodeGrantOidcSupportType> {
54        Some(ProviderExtAuthorizationCodeGrantOidcSupportType::Force)
55    }
56
57    fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
58        None
59    }
60
61    fn authorization_endpoint_url(&self) -> &Url {
62        &self.authorization_endpoint_url
63    }
64
65    fn authorization_request_url_modifying(&self, url: &mut Url) {
66        let query_pairs: Vec<_> = url
67            .query_pairs()
68            .map(|(k, v)| (k.to_string(), v.to_string()))
69            .collect::<Vec<_>>();
70        let mut query_pairs_mut = url.query_pairs_mut();
71        query_pairs_mut.clear();
72        for (k, v) in query_pairs {
73            match k.as_str() {
74                "response_type" => {
75                    query_pairs_mut.append_pair(k.as_str(), "code id_token");
76                }
77                "scope" => {}
78                _ => {
79                    query_pairs_mut.append_pair(k.as_str(), v.as_str());
80                }
81            }
82        }
83        query_pairs_mut.finish();
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    use oauth2_client::{
92        authorization_code_grant::{AccessTokenEndpoint, AuthorizationEndpoint},
93        re_exports::{Endpoint as _, Response},
94    };
95
96    #[test]
97    fn authorization_request() -> Result<(), Box<dyn std::error::Error>> {
98        let provider = AppleProviderWithAppleJs::new(
99            "CLIENT_ID".to_owned(),
100            "CLIENT_SECRET".to_owned(),
101            RedirectUri::new("https://client.example.com/cb")?,
102        )?;
103
104        let request = AuthorizationEndpoint::new(&provider, vec![AppleScope::Email])
105            .configure(|x| x.state = Some("STATE".to_owned()))
106            .render_request()?;
107
108        assert_eq!(request.uri(), "https://appleid.apple.com/auth/authorize?response_type=code+id_token&client_id=CLIENT_ID&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb&state=STATE");
109
110        Ok(())
111    }
112
113    #[test]
114    fn access_token_response() -> Result<(), Box<dyn std::error::Error>> {
115        let provider = AppleProviderWithAppleJs::new(
116            "CLIENT_ID".to_owned(),
117            "CLIENT_SECRET".to_owned(),
118            RedirectUri::new("https://client.example.com/cb")?,
119        )?;
120
121        let response_body = include_str!(
122            "../tests/response_body_json_files/access_token_with_authorization_code_grant.json"
123        );
124        let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
125            .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
126
127        match body_ret {
128            Ok(body) => {
129                assert!(body.id_token.is_some());
130            }
131            Err(body) => panic!("{body:?}"),
132        }
133
134        Ok(())
135    }
136}