diff options
Diffstat (limited to 'src/indieauth.rs')
-rw-r--r-- | src/indieauth.rs | 116 |
1 files changed, 116 insertions, 0 deletions
diff --git a/src/indieauth.rs b/src/indieauth.rs new file mode 100644 index 0000000..8d41577 --- /dev/null +++ b/src/indieauth.rs @@ -0,0 +1,116 @@ +use log::{error,info}; +use std::future::Future; +use std::pin::Pin; +use url::Url; +use tide::prelude::*; +use tide::{Request, Response, Next, Result}; + +use crate::database; +use crate::ApplicationState; + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct User { + pub me: Url, + pub client_id: Url, + scope: String +} + +impl User { + pub fn check_scope(&self, scope: &str) -> bool { + self.scopes().any(|i| i == scope) + } + pub fn scopes(&self) -> std::str::SplitAsciiWhitespace<'_> { + self.scope.split_ascii_whitespace() + } + #[cfg(test)] + pub fn new(me: &str, client_id: &str, scope: &str) -> Self { + Self { + me: Url::parse(me).unwrap(), + client_id: Url::parse(client_id).unwrap(), + scope: scope.to_string() + } + } +} + +async fn get_token_data(token: String, token_endpoint: &http_types::Url, http_client: &surf::Client) -> (http_types::StatusCode, Option<User>) { + match http_client.get(token_endpoint).header("Authorization", token).header("Accept", "application/json").send().await { + Ok(mut resp) => { + if resp.status() == 200 { + match resp.body_json::<User>().await { + Ok(user) => { + info!("Token endpoint request successful. Validated user: {}", user.me); + (resp.status(), Some(user)) + }, + Err(err) => { + error!("Token endpoint parsing error (HTTP status {}): {}", resp.status(), err); + (http_types::StatusCode::InternalServerError, None) + } + } + } else { + error!("Token endpoint returned non-200: {}", resp.status()); + (resp.status(), None) + } + } + Err(err) => { + error!("Token endpoint connection error: {}", err); + (http_types::StatusCode::InternalServerError, None) + } + } +} + +// TODO: Figure out how to cache these authorization values - they can potentially take a lot of processing time +pub fn check_auth<'a, Backend>(mut req: Request<ApplicationState<Backend>>, next: Next<'a, ApplicationState<Backend>>) -> Pin<Box<dyn Future<Output = Result> + Send + 'a>> +where + Backend: database::Storage + Send + Sync + Clone +{ + Box::pin(async { + let header = req.header("Authorization"); + match header { + None => { + Ok(Response::builder(401).body(json!({ + "error": "unauthorized", + "error_description": "Please provide an access token." + })).build()) + }, + Some(value) => { + // TODO check the token + let endpoint = &req.state().token_endpoint; + let http_client = &req.state().http_client; + match get_token_data(value.last().to_string(), endpoint, http_client).await { + (http_types::StatusCode::Ok, Some(user)) => { + req.set_ext(user); + Ok(next.run(req).await) + }, + (http_types::StatusCode::InternalServerError, None) => { + Ok(Response::builder(500).body(json!({ + "error": "token_endpoint_fail", + "error_description": "Token endpoint made a boo-boo and refused to answer." + })).build()) + }, + (_, None) => { + Ok(Response::builder(401).body(json!({ + "error": "unauthorized", + "error_description": "The token endpoint refused to accept your token." + })).build()) + }, + (_, Some(_)) => { + // This shouldn't happen. + panic!("The token validation function has caught rabies and returns malformed responses. Aborting."); + } + } + } + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn user_scopes_are_checkable() { + let user = User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"); + + assert!(user.check_scope("create")); + assert!(!user.check_scope("delete")); + } +} \ No newline at end of file |