oauth2_linode/
authorization_code_grant.rs1use 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 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 #[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}