about summary refs log tree commit diff
path: root/src/indieauth.rs
blob: 8d415772c8b0ba760ac6c76365c67d60e354135a (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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"));
    }
}