oauth2_linode/
authorization_code_grant.rs

1use oauth2_client::{
2    oauth2_core::{
3        re_exports::{AccessTokenResponseErrorBody, AccessTokenResponseSuccessfulBody},
4        types::{AccessTokenType, ScopeParameter},
5    },
6    re_exports::{
7        serde_json, thiserror, Body, ClientId, ClientSecret, RedirectUri, Response, SerdeJsonError,
8        Url, UrlParseError,
9    },
10    Provider, ProviderExtAuthorizationCodeGrant,
11};
12use serde::{Deserialize, Serialize};
13
14use crate::{LinodeScope, AUTHORIZATION_URL, TOKEN_URL};
15
16#[derive(Debug, Clone)]
17pub struct LinodeProviderWithWebApplication {
18    client_id: ClientId,
19    client_secret: ClientSecret,
20    redirect_uri: RedirectUri,
21    //
22    token_endpoint_url: Url,
23    authorization_endpoint_url: Url,
24}
25impl LinodeProviderWithWebApplication {
26    pub fn new(
27        client_id: ClientId,
28        client_secret: ClientSecret,
29        redirect_uri: RedirectUri,
30    ) -> Result<Self, UrlParseError> {
31        Ok(Self {
32            client_id,
33            client_secret,
34            redirect_uri,
35            token_endpoint_url: TOKEN_URL.parse()?,
36            authorization_endpoint_url: AUTHORIZATION_URL.parse()?,
37        })
38    }
39}
40impl Provider for LinodeProviderWithWebApplication {
41    type Scope = LinodeScope;
42
43    fn client_id(&self) -> Option<&ClientId> {
44        Some(&self.client_id)
45    }
46
47    fn client_secret(&self) -> Option<&ClientSecret> {
48        Some(&self.client_secret)
49    }
50
51    fn token_endpoint_url(&self) -> &Url {
52        &self.token_endpoint_url
53    }
54}
55impl ProviderExtAuthorizationCodeGrant for LinodeProviderWithWebApplication {
56    fn redirect_uri(&self) -> Option<&RedirectUri> {
57        Some(&self.redirect_uri)
58    }
59
60    fn scopes_default(&self) -> Option<Vec<<Self as Provider>::Scope>> {
61        Some(vec![LinodeScope::AccountReadOnly])
62    }
63
64    fn authorization_endpoint_url(&self) -> &Url {
65        &self.authorization_endpoint_url
66    }
67
68    #[allow(clippy::type_complexity)]
69    fn access_token_response_parsing(
70        &self,
71        response: &Response<Body>,
72    ) -> Option<
73        Result<
74            Result<
75                AccessTokenResponseSuccessfulBody<<Self as Provider>::Scope>,
76                AccessTokenResponseErrorBody,
77            >,
78            Box<dyn std::error::Error + Send + Sync + 'static>,
79        >,
80    > {
81        fn doing(
82            response: &Response<Body>,
83        ) -> Result<LinodeAccessTokenResponseBody, Box<dyn std::error::Error + Send + Sync + 'static>>
84        {
85            let body = serde_json::from_slice::<LinodeAccessTokenResponseBody>(response.body())
86                .map_err(AccessTokenResponseParsingError::DeResponseBodyFailed)?;
87
88            Ok(body)
89        }
90
91        Some(doing(response).map(Into::into))
92    }
93}
94
95#[derive(Serialize, Deserialize)]
96pub struct LinodeAccessTokenResponseBody {
97    pub access_token: String,
98    pub token_type: AccessTokenType,
99    pub expires_in: Option<usize>,
100    pub refresh_token: Option<String>,
101    pub scope: Option<ScopeParameter<LinodeScope>>,
102}
103
104impl From<LinodeAccessTokenResponseBody>
105    for Result<AccessTokenResponseSuccessfulBody<LinodeScope>, AccessTokenResponseErrorBody>
106{
107    fn from(body: LinodeAccessTokenResponseBody) -> Self {
108        Ok(AccessTokenResponseSuccessfulBody {
109            access_token: body.access_token.to_owned(),
110            token_type: body.token_type,
111            expires_in: body.expires_in,
112            refresh_token: body.refresh_token.to_owned(),
113            scope: body.scope,
114            id_token: None,
115            _extra: None,
116        })
117    }
118}
119
120#[derive(thiserror::Error, Debug)]
121pub enum AccessTokenResponseParsingError {
122    //
123    #[error("DeResponseBodyFailed {0}")]
124    DeResponseBodyFailed(SerdeJsonError),
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    use oauth2_client::{
132        authorization_code_grant::AccessTokenEndpoint,
133        re_exports::{Endpoint as _, Response},
134    };
135
136    #[test]
137    fn access_token_response() -> Result<(), Box<dyn std::error::Error>> {
138        let provider = LinodeProviderWithWebApplication::new(
139            "CLIENT_ID".to_owned(),
140            "CLIENT_SECRET".to_owned(),
141            RedirectUri::new("https://client.example.com/cb")?,
142        )?;
143
144        let response_body = include_str!(
145            "../tests/response_body_json_files/access_token_with_authorization_code_grant.json"
146        );
147        let body_ret = AccessTokenEndpoint::new(&provider, "CODE".to_owned())
148            .parse_response(Response::builder().body(response_body.as_bytes().to_vec())?)?;
149
150        match body_ret {
151            Ok(_body) => {}
152            Err(body) => panic!("{body:?}"),
153        }
154
155        Ok(())
156    }
157}