eve_esi_api/api/authent/
oauth2.rs

1use log::debug;
2
3use oauth2::basic::{BasicClient, BasicErrorResponseType, BasicTokenType};
4use oauth2::reqwest::async_http_client;
5use oauth2::{
6    AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, EmptyExtraTokenFields,
7    PkceCodeChallenge, RedirectUrl, RevocationErrorResponseType, Scope, StandardErrorResponse,
8    StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse,
9    TokenResponse, TokenUrl,
10};
11use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13use std::path::PathBuf;
14use std::time::Duration;
15use tokio::fs::File;
16use tokio::io::AsyncWriteExt;
17use tokio::io::BufReader;
18use tokio::io::{AsyncBufReadExt, AsyncReadExt};
19use tokio::net::TcpListener;
20use url::Url;
21
22use crate::errors::EveEsiError;
23
24use crate::Result;
25
26type Oauth2Client = Client<
27    StandardErrorResponse<BasicErrorResponseType>,
28    StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
29    BasicTokenType,
30    StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>,
31    StandardRevocableToken,
32    StandardErrorResponse<RevocationErrorResponseType>,
33>;
34
35/// Authenticate to Eve Esi Api
36///
37pub(crate) struct Oauth2Authent {
38    pub(super) token_path: Option<PathBuf>,
39    pub(super) authent_token: Option<Authent>,
40    pub(super) user_agent: String,
41    pub(super) scopes: Vec<Scope>,
42    pub(super) verify_url: String, // https://login.eveonline.com/oauth/verif
43    pub(super) auth_url: String,   // https://login.eveonline.com/v2/oauth/authorize
44    pub(super) token_url: String,  // https://login.eveonline.com/v2/oauth/token
45    pub(super) callback_url: String, //http://localhost:8569/callback
46    pub(super) client_id: String,
47    pub(super) client_secret: String,
48}
49
50#[derive(Serialize, Deserialize)]
51pub struct Authent {
52    pub token: StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
53}
54
55#[derive(Serialize, Deserialize, Debug, Clone)]
56pub struct Verify {
57    #[serde(rename = "CharacterID")]
58    pub character_id: usize,
59    #[serde(rename = "CharacterName")]
60    pub character_name: String,
61}
62
63impl Oauth2Authent {
64    /// entry point
65    pub async fn authenticate(&mut self) -> Result<()> {
66        match self.handle_existing_token().await {
67            Ok(()) => {
68                if self.need_refresh(24)? {
69                    let _ = self.refresh_token().await;
70                }
71                Ok(())
72            }
73            Err(EveEsiError::NoTokenFound) | Err(EveEsiError::ScopesChanged) => {
74                self.connect().await?;
75                self.serialize_token().await?;
76                Ok(())
77            }
78            other => other,
79        }
80    }
81    /// Serialize token from mem to file
82    async fn serialize_token(&self) -> Result<()> {
83        if let Some(path) = &self.token_path {
84            let mut file = File::create(path).await?;
85            Ok(file
86                .write_all(serde_json::to_string(&self.authent_token)?.as_bytes())
87                .await?)
88        } else {
89            Ok(())
90        }
91    }
92
93    /// Deserialize token from file to mem
94    async fn deserialize_token(&mut self) -> Result<()> {
95        if let Some(path) = &self.token_path {
96            let mut file = File::open(path).await?;
97
98            let mut buf = String::new();
99            file.read_to_string(&mut buf).await?;
100            self.authent_token = serde_json::from_str(buf.as_str())?;
101            Ok(())
102        } else {
103            Ok(())
104        }
105    }
106
107    /// Verify token in mem
108    pub async fn verify(&self) -> Result<Verify> {
109        let token = self.get_token()?.unwrap();
110        let client = reqwest::Client::builder()
111            .user_agent(self.user_agent.as_str())
112            .build()?;
113
114        let r = client
115            .get(self.verify_url.as_str())
116            .bearer_auth(token.token.access_token().secret())
117            .send()
118            .await?;
119
120        let text_response = r.error_for_status()?.text().await?;
121
122        Ok(serde_json::from_str(text_response.as_str())?)
123    }
124
125    /// Refresh token in mem
126    async fn refresh_token(&mut self) -> Result<()> {
127        let token = self.get_token()?.unwrap();
128        let client = self.build_client()?;
129        let token = client
130            .exchange_refresh_token(token.token.refresh_token().unwrap())
131            .request_async(async_http_client)
132            .await?;
133        self.authent_token = Some(Authent { token });
134        Ok(())
135    }
136
137    /// Get token in mem, or Err if not in mem
138    fn get_token(&self) -> Result<Option<&Authent>> {
139        if let Some(t) = &self.authent_token {
140            Ok(Some(t))
141        } else {
142            Err(EveEsiError::NoTokenFound)
143        }
144    }
145
146    fn build_client(&self) -> Result<Oauth2Client> {
147        // let id = env!("CLIENT_ID", "CLIENT_ID empty").to_string();
148        // let secret = env!("CLIENT_SECRET", "CLIENT_SECRET empty").to_string();
149
150        let client_id = ClientId::new(self.client_id.clone());
151        let client_secret = ClientSecret::new(self.client_secret.clone());
152        let auth_url = AuthUrl::new(self.auth_url.clone()).unwrap();
153        let token_url = TokenUrl::new(self.token_url.clone()).unwrap();
154
155        Ok(
156            BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url))
157                .set_redirect_uri(RedirectUrl::new(self.callback_url.clone()).unwrap()),
158        )
159    }
160
161    /// Connect: get a fresh token through a full OAuth2 authentication
162    async fn connect(&mut self) -> Result<()> {
163        let client = self.build_client()?;
164        let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
165
166        // Generate the full authorization URL.
167        let (auth_url, csrf_state) = client
168            .authorize_url(CsrfToken::new_random)
169            .add_scopes(self.scopes.clone())
170            // Set the PKCE code challenge.
171            .set_pkce_challenge(pkce_challenge)
172            .url();
173
174        println!("Browse to: {}", auth_url);
175
176        // A very naive implementation of the redirect server.
177        let listener = TcpListener::bind("127.0.0.1:8569").await.unwrap();
178
179        if let Ok((mut stream, _)) = listener.accept().await {
180            let code;
181            let state;
182            {
183                let mut reader = BufReader::new(&mut stream);
184
185                let mut request_line = String::new();
186                reader.read_line(&mut request_line).await.unwrap();
187
188                let redirect_url = request_line.split_whitespace().nth(1).unwrap();
189                let url = Url::parse(&("http://localhost".to_string() + redirect_url)).unwrap();
190
191                let code_pair = url
192                    .query_pairs()
193                    .find(|pair| {
194                        let (key, _) = pair;
195                        key == "code"
196                    })
197                    .unwrap();
198
199                let (_, value) = code_pair;
200                code = AuthorizationCode::new(value.into_owned());
201
202                let state_pair = url
203                    .query_pairs()
204                    .find(|pair| {
205                        let (key, _) = pair;
206                        key == "state"
207                    })
208                    .unwrap();
209
210                let (_, value) = state_pair;
211                state = CsrfToken::new(value.into_owned());
212            }
213
214            let message = "Go back to your terminal :)";
215            let response = format!(
216                "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
217                message.len(),
218                message
219            );
220            stream.write_all(response.as_bytes()).await.unwrap();
221
222            debug!("EVE returned the following code:\n{}\n", code.secret());
223            debug!(
224                "EVE returned the following state:\n{} (expected `{}`)\n",
225                state.secret(),
226                csrf_state.secret()
227            );
228
229            // Exchange the code with a token.
230            let token_res = client
231                .exchange_code(code)
232                .set_pkce_verifier(pkce_verifier)
233                .request_async(async_http_client)
234                .await?;
235
236            debug!("EVE returned the following token:\n{:?}\n", token_res);
237
238            self.authent_token = Some(Authent { token: token_res });
239            Ok(())
240        } else {
241            // The server will terminate itself after collecting the first code.
242            Err(EveEsiError::NoTokenFound)
243        }
244    }
245
246    /// Exit point : Consumes the thing to extract the token
247    pub fn get_authent(self) -> Result<Authent> {
248        if let Some(auth) = self.authent_token {
249            Ok(auth)
250        } else {
251            Err(EveEsiError::NoTokenFound)
252        }
253    }
254
255    /// Delete the serialized token
256    pub async fn _delete_token(&self) -> Result<()> {
257        if let Some(path) = &self.token_path {
258            Ok(std::fs::remove_file(path)?)
259        } else {
260            Ok(())
261        }
262    }
263
264    fn get_token_scopes(&self) -> Result<Option<&Vec<Scope>>> {
265        Ok(self.get_token()?.unwrap().token.scopes())
266    }
267
268    fn get_token_expires_in(&self) -> Result<Option<Duration>> {
269        Ok(self.get_token()?.unwrap().token.expires_in())
270    }
271
272    fn need_refresh(&self, hours: u64) -> Result<bool> {
273        if let Some(duration) = self.get_token_expires_in()? {
274            if duration > Duration::from_secs(60 * 60 * hours) {
275                return Ok(true);
276            }
277        }
278        Ok(false)
279    }
280
281    fn have_scopes_changed(&self) -> Result<bool> {
282        let existant = self.get_token_scopes()?;
283        let required = Some(&self.scopes);
284
285        let existant_set: Option<HashSet<&Scope>> = existant.map(|v| HashSet::from_iter(v.iter()));
286        let requiret_set = required.map(|v| HashSet::from_iter(v.iter()));
287
288        Ok(existant_set == requiret_set)
289    }
290
291    async fn handle_existing_token(&mut self) -> Result<()> {
292        // do we have a token in memory ?
293        if self.authent_token.is_some() {
294            self.verify().await?;
295        }
296        // first try local stored token :
297        else if self.deserialize_token().await.is_ok() {
298            if let Ok(v) = self.verify().await {
299                debug!("logged as {:#?}", v);
300            } else {
301                // try refreshing
302                if self.refresh_token().await.is_err() {
303                    return Err(EveEsiError::NoTokenFound);
304                }
305            }
306        } else {
307            return Err(EveEsiError::NoTokenFound);
308        }
309        if self.have_scopes_changed()? {
310            return Err(EveEsiError::ScopesChanged);
311        }
312        Ok(())
313    }
314}