about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/indieauth/mod.rs2
-rw-r--r--src/tokenauth.rs358
2 files changed, 1 insertions, 359 deletions
diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs
index b3db77f..0ac3dfd 100644
--- a/src/indieauth/mod.rs
+++ b/src/indieauth/mod.rs
@@ -59,7 +59,7 @@ impl axum::response::IntoResponse for IndieAuthResourceError {
             InvalidToken => (
                 StatusCode::UNAUTHORIZED,
                 [("WWW-Authenticate", "Bearer, error=\"invalid_token\"")],
-                Json(&serde_json::json!({"error": "unauthorized"}))
+                Json(&serde_json::json!({"error": "not_authorized"}))
             ).into_response()
         }
     }
diff --git a/src/tokenauth.rs b/src/tokenauth.rs
deleted file mode 100644
index 414454a..0000000
--- a/src/tokenauth.rs
+++ /dev/null
@@ -1,358 +0,0 @@
-use serde::{Deserialize, Serialize};
-use url::Url;
-
-#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
-pub struct User {
-    pub me: Url,
-    pub client_id: Url,
-    scope: String,
-}
-
-#[derive(Debug, Clone, PartialEq, Copy)]
-pub enum ErrorKind {
-    PermissionDenied,
-    NotAuthorized,
-    TokenEndpointError,
-    JsonParsing,
-    InvalidHeader,
-    Other,
-}
-
-#[derive(Deserialize, Serialize, Debug, Clone)]
-pub struct TokenEndpointError {
-    error: String,
-    error_description: String,
-}
-
-#[derive(Debug)]
-pub struct IndieAuthError {
-    source: Option<Box<dyn std::error::Error + Send + Sync>>,
-    kind: ErrorKind,
-    msg: String,
-}
-
-impl std::error::Error for IndieAuthError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        self.source
-            .as_ref()
-            .map(|e| e.as_ref() as &dyn std::error::Error)
-    }
-}
-
-impl std::fmt::Display for IndieAuthError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{}: {}",
-            match self.kind {
-                ErrorKind::TokenEndpointError => "token endpoint returned an error: ",
-                ErrorKind::JsonParsing => "error while parsing token endpoint response: ",
-                ErrorKind::NotAuthorized => "token endpoint did not recognize the token: ",
-                ErrorKind::PermissionDenied => "token endpoint rejected the token: ",
-                ErrorKind::InvalidHeader => "authorization header parsing error: ",
-                ErrorKind::Other => "token endpoint communication error: ",
-            },
-            self.msg
-        )
-    }
-}
-
-impl From<serde_json::Error> for IndieAuthError {
-    fn from(err: serde_json::Error) -> Self {
-        Self {
-            msg: format!("{}", err),
-            source: Some(Box::new(err)),
-            kind: ErrorKind::JsonParsing,
-        }
-    }
-}
-
-impl From<reqwest::Error> for IndieAuthError {
-    fn from(err: reqwest::Error) -> Self {
-        Self {
-            msg: format!("{}", err),
-            source: Some(Box::new(err)),
-            kind: ErrorKind::Other,
-        }
-    }
-}
-
-impl From<axum::extract::rejection::TypedHeaderRejection> for IndieAuthError {
-    fn from(err: axum::extract::rejection::TypedHeaderRejection) -> Self {
-        Self {
-            msg: format!("{:?}", err.reason()),
-            source: Some(Box::new(err)),
-            kind: ErrorKind::InvalidHeader,
-        }
-    }
-}
-
-impl axum::response::IntoResponse for IndieAuthError {
-    fn into_response(self) -> axum::response::Response {
-        let status_code: StatusCode = match self.kind {
-            ErrorKind::PermissionDenied => StatusCode::FORBIDDEN,
-            ErrorKind::NotAuthorized => StatusCode::UNAUTHORIZED,
-            ErrorKind::TokenEndpointError => StatusCode::INTERNAL_SERVER_ERROR,
-            ErrorKind::JsonParsing => StatusCode::BAD_REQUEST,
-            ErrorKind::InvalidHeader => StatusCode::UNAUTHORIZED,
-            ErrorKind::Other => StatusCode::INTERNAL_SERVER_ERROR,
-        };
-
-        let body = serde_json::json!({
-            "error": match self.kind {
-                ErrorKind::PermissionDenied => "forbidden",
-                ErrorKind::NotAuthorized => "unauthorized",
-                ErrorKind::TokenEndpointError => "token_endpoint_error",
-                ErrorKind::JsonParsing => "invalid_request",
-                ErrorKind::InvalidHeader => "unauthorized",
-                ErrorKind::Other => "unknown_error",
-            },
-            "error_description": self.msg
-        });
-
-        (status_code, axum::response::Json(body)).into_response()
-    }
-}
-
-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()
-    }
-    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(),
-        }
-    }
-}
-
-use axum::{
-    extract::{Extension, FromRequest, RequestParts, TypedHeader},
-    headers::{
-        authorization::{Bearer, Credentials},
-        Authorization,
-    },
-    http::StatusCode,
-};
-
-// this newtype is required due to axum::Extension retrieving items by type
-// it's based on compiler magic matching extensions by their type's hashes
-#[derive(Debug, Clone)]
-pub struct TokenEndpoint(pub url::Url);
-
-#[async_trait::async_trait]
-impl<B> FromRequest<B> for User
-where
-    B: Send,
-{
-    type Rejection = IndieAuthError;
-
-    #[cfg_attr(
-        all(debug_assertions, not(test)),
-        allow(unreachable_code, unused_variables)
-    )]
-    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
-        // Return a fake user if we're running a debug build
-        // I don't wanna bother with authentication
-        #[cfg(all(debug_assertions, not(test)))]
-        return Ok(User::new(
-            "http://localhost:8080/",
-            "https://quill.p3k.io/",
-            "create update delete media",
-        ));
-
-        let TypedHeader(Authorization(token)) =
-            TypedHeader::<Authorization<Bearer>>::from_request(req)
-                .await
-                .map_err(IndieAuthError::from)?;
-
-        let Extension(TokenEndpoint(token_endpoint)): Extension<TokenEndpoint> =
-            Extension::from_request(req).await.unwrap();
-
-        let Extension(http): Extension<reqwest_middleware::ClientWithMiddleware> =
-            Extension::from_request(req).await.unwrap();
-
-        match http
-            .get(token_endpoint)
-            .header("Authorization", token.encode())
-            .header("Accept", "application/json")
-            .send()
-            .await
-        {
-            Ok(res) => match res.status() {
-                StatusCode::OK => match res.json::<serde_json::Value>().await {
-                    Ok(json) => match serde_json::from_value::<User>(json.clone()) {
-                        Ok(user) => Ok(user),
-                        Err(err) => {
-                            if let Some(false) = json["active"].as_bool() {
-                                Err(IndieAuthError {
-                                    source: None,
-                                    kind: ErrorKind::NotAuthorized,
-                                    msg: "The token is not active for this user.".to_owned(),
-                                })
-                            } else {
-                                Err(IndieAuthError::from(err))
-                            }
-                        }
-                    },
-                    Err(err) => Err(IndieAuthError::from(err)),
-                },
-                StatusCode::BAD_REQUEST => match res.json::<TokenEndpointError>().await {
-                    Ok(err) => {
-                        if err.error == "unauthorized" {
-                            Err(IndieAuthError {
-                                source: None,
-                                kind: ErrorKind::NotAuthorized,
-                                msg: err.error_description,
-                            })
-                        } else {
-                            Err(IndieAuthError {
-                                source: None,
-                                kind: ErrorKind::TokenEndpointError,
-                                msg: err.error_description,
-                            })
-                        }
-                    }
-                    Err(err) => Err(IndieAuthError::from(err)),
-                },
-                _ => Err(IndieAuthError {
-                    source: None,
-                    msg: format!("Token endpoint returned {}", res.status()),
-                    kind: ErrorKind::TokenEndpointError,
-                }),
-            },
-            Err(err) => Err(IndieAuthError::from(err)),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::User;
-    use axum::{
-        extract::FromRequest,
-        http::{Method, Request},
-    };
-    use wiremock::{MockServer, Mock, ResponseTemplate};
-    use wiremock::matchers::{method, path, header};
-
-    #[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"));
-    }
-
-    #[inline]
-    fn get_http_client() -> reqwest_middleware::ClientWithMiddleware {
-        reqwest_middleware::ClientWithMiddleware::new()
-    }
-
-    fn request<A: Into<Option<&'static str>>>(
-        auth: A,
-        endpoint: String,
-    ) -> Request<()> {
-        let request = Request::builder().method(Method::GET);
-
-        match auth.into() {
-            Some(auth) => request.header("Authorization", auth),
-            None => request,
-        }
-        .extension(super::TokenEndpoint(endpoint.parse().unwrap()))
-        .extension(get_http_client())
-        .body(())
-        .unwrap()
-    }
-
-    #[tokio::test]
-    async fn test_require_token_with_token() {
-        let server = MockServer::start().await;
-
-        Mock::given(path("/token"))
-            .and(header("Authorization", "Bearer token"))
-            .respond_with(ResponseTemplate::new(200)
-                          .set_body_json(User::new(
-                              "https://fireburn.ru/",
-                              "https://quill.p3k.io/",
-                              "create update media",
-                          ))
-            )
-            .mount(&server)
-            .await;
-
-        let request = request("Bearer token", format!("{}/token", &server.uri()));
-        let mut parts = axum::extract::RequestParts::new(request);
-        let user = User::from_request(&mut parts).await.unwrap();
-
-        assert_eq!(user.me.as_str(), "https://fireburn.ru/")
-    }
-
-    #[tokio::test]
-    async fn test_require_token_fake_token() {
-        let server = MockServer::start().await;
-
-        Mock::given(path("/refuse_token"))
-            .respond_with(ResponseTemplate::new(200)
-                          .set_body_json(serde_json::json!({"active": false}))
-            )
-            .mount(&server)
-            .await;
-
-        let request = request("Bearer token", format!("{}/refuse_token", &server.uri()));
-        let mut parts = axum::extract::RequestParts::new(request);
-        let err = User::from_request(&mut parts).await.unwrap_err();
-
-        assert_eq!(err.kind, super::ErrorKind::NotAuthorized)
-    }
-
-    #[tokio::test]
-    async fn test_require_token_no_token() {
-        let server = MockServer::start().await;
-
-        Mock::given(path("/should_never_be_called"))
-            .respond_with(ResponseTemplate::new(500))
-            .expect(0)
-            .mount(&server)
-            .await;
-
-        let request = request(None, format!("{}/should_never_be_called", &server.uri()));
-        let mut parts = axum::extract::RequestParts::new(request);
-        let err = User::from_request(&mut parts).await.unwrap_err();
-
-        assert_eq!(err.kind, super::ErrorKind::InvalidHeader);
-    }
-
-    #[tokio::test]
-    async fn test_require_token_400_error_unauthorized() {
-        let server = MockServer::start().await;
-
-        Mock::given(path("/refuse_token_with_400"))
-            .and(header("Authorization", "Bearer token"))
-            .respond_with(ResponseTemplate::new(400)
-                          .set_body_json(serde_json::json!({
-                              "error": "unauthorized",
-                              "error_description": "The token provided was malformed"
-                          }))
-            )
-            .mount(&server)
-            .await;
-
-        let request = request(
-            "Bearer token",
-            format!("{}/refuse_token_with_400", &server.uri()),
-        );
-        let mut parts = axum::extract::RequestParts::new(request);
-        let err = User::from_request(&mut parts).await.unwrap_err();
-
-        assert_eq!(err.kind, super::ErrorKind::NotAuthorized);
-    }
-}