about summary refs log tree commit diff
path: root/kittybox-rs/src/indieauth
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs/src/indieauth')
-rw-r--r--kittybox-rs/src/indieauth/mod.rs81
1 files changed, 58 insertions, 23 deletions
diff --git a/kittybox-rs/src/indieauth/mod.rs b/kittybox-rs/src/indieauth/mod.rs
index aaa3301..f71b5be 100644
--- a/kittybox-rs/src/indieauth/mod.rs
+++ b/kittybox-rs/src/indieauth/mod.rs
@@ -18,6 +18,8 @@ use kittybox_indieauth::{
     GrantType, GrantRequest, GrantResponse, Profile,
     TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest, TokenData
 };
+use std::str::FromStr;
+use std::ops::Deref;
 
 pub mod backend;
 #[cfg(feature = "webauthn")]
@@ -145,11 +147,59 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
     Host(host): Host,
     Query(request): Query<AuthorizationRequest>,
     Extension(db): Extension<D>,
+    Extension(http): Extension<reqwest::Client>,
     Extension(auth): Extension<A>
-) -> Html<String> {
+) -> Response {
     let me = format!("https://{}/", host).parse().unwrap();
-    // TODO fetch h-app from client_id
-    // TODO verify redirect_uri registration
+    let h_app = {
+        match http.get(request.client_id.clone()).send().await {
+            Ok(response) => {
+                let url = response.url().clone();
+                let text = response.text().await.unwrap();
+                match microformats::from_html(&text, url) {
+                    Ok(mf2) => {
+                        if let Some(relation) = mf2.rels.items.get(&request.redirect_uri) {
+                            if !relation.rels.iter().any(|i| i == "redirect_uri") {
+                                return (StatusCode::BAD_REQUEST,
+                                        [("Content-Type", "text/plain")],
+                                        "The redirect_uri provided was declared as \
+                                         something other than redirect_uri.")
+                                    .into_response()
+                            }
+                        } else if request.redirect_uri.origin() != request.client_id.origin() {
+                            return (StatusCode::BAD_REQUEST,
+                                    [("Content-Type", "text/plain")],
+                                    "The redirect_uri didn't match the origin \
+                                     and wasn't explicitly allowed. You were being tricked.")
+                                .into_response()
+                        }
+
+                        mf2.items.iter()
+                            .cloned()
+                            .find(|i| (**i).borrow().r#type.iter()
+                                  .any(|i| *i == microformats::types::Class::from_str("h-app").unwrap()
+                                       || *i == microformats::types::Class::from_str("h-x-app").unwrap()))
+                            .map(|i| serde_json::to_value(i.borrow().deref()).unwrap())
+                    },
+                    Err(err) => {
+                        tracing::error!("Error parsing application metadata: {}", err);
+                        return (StatusCode::BAD_REQUEST,
+                                [("Content-Type", "text/plain")],
+                                "Parsing application metadata failed.").into_response()
+                    }
+                }
+            },
+            Err(err) => {
+                tracing::error!("Error fetching application metadata: {}", err);
+                return (StatusCode::INTERNAL_SERVER_ERROR,
+                        [("Content-Type", "text/plain")],
+                        "Fetching application metadata failed.").into_response()
+            }
+        }
+    };
+
+    tracing::debug!("Application metadata: {:#?}", h_app);
+
     Html(kittybox_frontend_renderer::Template {
         title: "Confirm sign-in via IndieAuth",
         blog_name: "Kittybox",
@@ -159,26 +209,10 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
             request,
             credentials: auth.list_user_credential_types(&me).await.unwrap(),
             user: db.get_post(me.as_str()).await.unwrap().unwrap(),
-            // XXX parse MF2
-            app: serde_json::json!({
-                "type": [
-                    "h-app",
-                    "h-x-app"
-                ],
-                "properties": {
-                    "name": [
-                        "Quill"
-                    ],
-                    "logo": [
-                        "https://quill.p3k.io/images/quill-logo-144.png"
-                    ],
-                    "url": [
-                        "https://quill.p3k.io/"
-                    ]
-                }
-            })
+            app: h_app
         }.to_string(),
     }.to_string())
+        .into_response()
 }
 
 #[derive(Deserialize, Debug)]
@@ -753,7 +787,7 @@ async fn userinfo_endpoint_get<A: AuthBackend, D: Storage + 'static>(
     }
 }
 
-pub fn router<A: AuthBackend, D: Storage + 'static>(backend: A, db: D) -> axum::Router {
+pub fn router<A: AuthBackend, D: Storage + 'static>(backend: A, db: D, http: reqwest::Client) -> axum::Router {
     use axum::routing::{Router, get, post};
 
     Router::new()
@@ -785,7 +819,7 @@ pub fn router<A: AuthBackend, D: Storage + 'static>(backend: A, db: D) -> axum::
                 .route("/webauthn/pre_register",
                        get(
                            #[cfg(feature = "webauthn")] webauthn::webauthn_pre_register::<A, D>,
-                           #[cfg(not(feature = "webauthn"))] || async { axum::http::StatusCode::NOT_FOUND }
+                           #[cfg(not(feature = "webauthn"))] || std::future::ready(axum::http::StatusCode::NOT_FOUND)
                        )
                 )
                 .layer(tower_http::cors::CorsLayer::new()
@@ -799,6 +833,7 @@ pub fn router<A: AuthBackend, D: Storage + 'static>(backend: A, db: D) -> axum::
             // If I could, I would've designed a separate trait for getting profiles
             // And made databases implement it, for example
                 .layer(Extension(db))
+                .layer(Extension(http))
         )
         .route(
             "/.well-known/oauth-authorization-server",