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