about summary refs log tree commit diff
path: root/kittybox-rs
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs')
-rw-r--r--kittybox-rs/src/bin/kittybox-indieauth-helper.rs29
-rw-r--r--kittybox-rs/src/indieauth/mod.rs81
-rw-r--r--kittybox-rs/src/main.rs2
-rw-r--r--kittybox-rs/templates/src/indieauth.rs23
4 files changed, 101 insertions, 34 deletions
diff --git a/kittybox-rs/src/bin/kittybox-indieauth-helper.rs b/kittybox-rs/src/bin/kittybox-indieauth-helper.rs
index 37eee5b..e5836d2 100644
--- a/kittybox-rs/src/bin/kittybox-indieauth-helper.rs
+++ b/kittybox-rs/src/bin/kittybox-indieauth-helper.rs
@@ -1,8 +1,13 @@
-use kittybox_indieauth::{AuthorizationRequest, PKCEVerifier, PKCEChallenge, PKCEMethod, GrantRequest, Scope, AuthorizationResponse, TokenData, GrantResponse};
+use kittybox_indieauth::{
+    AuthorizationRequest, PKCEVerifier,
+    PKCEChallenge, PKCEMethod, GrantRequest, Scope,
+    AuthorizationResponse, TokenData, GrantResponse
+};
 use clap::Parser;
 use std::{borrow::Cow, io::Write};
 
 const DEFAULT_CLIENT_ID: &str = "https://kittybox.fireburn.ru/indieauth-helper";
+const DEFAULT_REDIRECT_URI: &str = "http://localhost:60000/callback";
 
 #[derive(Debug, thiserror::Error)]
 enum Error {
@@ -38,6 +43,9 @@ struct Args {
     /// Client ID to use when requesting a token.
     #[clap(short, long, value_parser, default_value = DEFAULT_CLIENT_ID)]
     client_id: url::Url,
+    /// Redirect URI to declare. Note: This will break the flow, use only for testing UI.
+    #[clap(long, value_parser)]
+    redirect_uri: Option<url::Url>
 }
 
 fn append_query_string<T: serde::Serialize>(
@@ -69,7 +77,9 @@ async fn main() -> Result<(), Error> {
         builder.build().unwrap()
     };
 
-    let redirect_uri: url::Url = "http://localhost:60000/callback".parse().unwrap();
+    let redirect_uri: url::Url = args.redirect_uri
+        .clone()
+        .unwrap_or_else(|| DEFAULT_REDIRECT_URI.parse().unwrap());
     
     eprintln!("Checking .well-known for metadata...");
     let metadata = http.get(args.me.join("/.well-known/oauth-authorization-server")?)
@@ -86,7 +96,7 @@ async fn main() -> Result<(), Error> {
         client_id: args.client_id.clone(),
         redirect_uri: redirect_uri.clone(),
         state: kittybox_indieauth::State::new(),
-        code_challenge: PKCEChallenge::new(verifier.clone(), PKCEMethod::default()),
+        code_challenge: PKCEChallenge::new(&verifier, PKCEMethod::default()),
         scope: Some(kittybox_indieauth::Scopes::new(args.scope)),
         me: Some(args.me)
     };
@@ -96,6 +106,13 @@ async fn main() -> Result<(), Error> {
         authorization_request
     )?;
 
+    eprintln!("Please visit the following URL in your browser:\n\n   {}\n", indieauth_url.as_str());
+
+    if args.redirect_uri.is_some() {
+        eprintln!("Custom redirect URI specified, won't be able to catch authorization response.");
+        std::process::exit(0);
+    }
+    
     // Prepare a callback
     let (tx, rx) = tokio::sync::oneshot::channel::<AuthorizationResponse>();
     let server = {
@@ -136,8 +153,6 @@ async fn main() -> Result<(), Error> {
         tokio::task::spawn(server)
     };
     
-    eprintln!("Please visit the following URL in your browser:\n\n   {}\n", indieauth_url.as_str());
-
     let authorization_response = rx.await.unwrap();
 
     // Clean up after the server
@@ -177,7 +192,9 @@ async fn main() -> Result<(), Error> {
         profile,
         access_token,
         expires_in,
-        refresh_token
+        refresh_token,
+        token_type,
+        scope
     } = grant_response {
         eprintln!("Congratulations, {}, access token is ready! {}",
                   me.as_str(),
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",
diff --git a/kittybox-rs/src/main.rs b/kittybox-rs/src/main.rs
index ad76042..1586e60 100644
--- a/kittybox-rs/src/main.rs
+++ b/kittybox-rs/src/main.rs
@@ -133,7 +133,7 @@ async fn main() {
         let media = axum::Router::new()
             .nest("/.kittybox/media", kittybox::media::router(blobstore, auth_backend.clone()));
 
-        let indieauth = kittybox::indieauth::router(auth_backend, database.clone());
+        let indieauth = kittybox::indieauth::router(auth_backend, database.clone(), http.clone());
 
         let technical = axum::Router::new()
             .route(
diff --git a/kittybox-rs/templates/src/indieauth.rs b/kittybox-rs/templates/src/indieauth.rs
index e901d7b..908cd2c 100644
--- a/kittybox-rs/templates/src/indieauth.rs
+++ b/kittybox-rs/templates/src/indieauth.rs
@@ -5,7 +5,7 @@ markup::define! {
     AuthorizationRequestPage(
         request: AuthorizationRequest,
         credentials: Vec<EnrolledCredential>,
-        app: serde_json::Value,
+        app: Option<serde_json::Value>,
         user: serde_json::Value
     ) {
         script[type="module"] {
@@ -31,12 +31,27 @@ document.getElementById("indieauth_page").addEventListener("submit", submit_hand
                     }
 
                     p."mini-h-card" {
-                        @if let Some(icon) = app["properties"]["logo"][0].as_str() {
+                        @if let Some(icon) = app
+                            .as_ref()
+                            .and_then(|app| app["properties"]["logo"][0].as_str())
+                        {
                             img.app_icon[src=icon];
+                        } else if let Some(icon) = app
+                            .as_ref()
+                            .and_then(|app| app["properties"]["logo"][0].as_object())
+                        {
+                            img.app_icon[src=icon["src"].as_str().unwrap(), alt=icon["alt"].as_str().unwrap()];
                         }
                         span {
-                            a[href=app["properties"]["url"][0].as_str().unwrap()] {
-                                @app["properties"]["name"][0].as_str().unwrap()
+                            a[href=app
+                              .as_ref()
+                              .and_then(|app| app["properties"]["url"][0].as_str())
+                              .unwrap_or_else(|| request.client_id.as_str())
+                            ] {
+                                @app
+                                    .as_ref()
+                                    .and_then(|app| app["properties"]["name"][0].as_str())
+                                    .unwrap_or_else(|| request.client_id.as_str())
                             }
                             " wants to confirm your identity."
                         }