about summary refs log tree commit diff
path: root/src/indieauth
diff options
context:
space:
mode:
Diffstat (limited to 'src/indieauth')
-rw-r--r--src/indieauth/mod.rs96
1 files changed, 67 insertions, 29 deletions
diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs
index 06ec1f7..e466d98 100644
--- a/src/indieauth/mod.rs
+++ b/src/indieauth/mod.rs
@@ -7,14 +7,10 @@ use axum::{
 };
 #[cfg_attr(not(feature = "webauthn"), allow(unused_imports))]
 use axum_extra::extract::cookie::{CookieJar, Cookie};
-use axum_extra::{TypedHeader, headers::{authorization::Bearer, Authorization}};
+use axum_extra::{headers::{authorization::Bearer, Authorization, ContentType, HeaderMapExt}, TypedHeader};
 use crate::database::Storage;
 use kittybox_indieauth::{
-    Metadata, IntrospectionEndpointAuthMethod, RevocationEndpointAuthMethod,
-    Scope, Scopes, PKCEMethod, Error, ErrorKind, ResponseType,
-    AuthorizationRequest, AuthorizationResponse,
-    GrantType, GrantRequest, GrantResponse, Profile,
-    TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest, TokenData
+    AuthorizationRequest, AuthorizationResponse, ClientMetadata, Error, ErrorKind, GrantRequest, GrantResponse, GrantType, IntrospectionEndpointAuthMethod, Metadata, PKCEMethod, Profile, ProfileUrl, ResponseType, RevocationEndpointAuthMethod, Scope, Scopes, TokenData, TokenIntrospectionRequest, TokenIntrospectionResponse, TokenRevocationRequest
 };
 use std::str::FromStr;
 
@@ -99,15 +95,7 @@ impl <A: AuthBackend + FromRef<St>, St: Clone + Send + Sync + 'static> axum::ext
 pub async fn metadata(
     Host(host): Host
 ) -> Metadata {
-    let issuer: url::Url = format!(
-        "{}://{}/",
-        if cfg!(debug_assertions) {
-            "http"
-        } else {
-            "https"
-        },
-        host
-    ).parse().unwrap();
+    let issuer: url::Url = format!("https://{}/", host).parse().unwrap();
 
     let indieauth: url::Url = issuer.join("/.kittybox/indieauth/").unwrap();
     Metadata {
@@ -146,11 +134,22 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
     State(http): State<reqwest::Client>,
     State(auth): State<A>
 ) -> Response {
-    let me = format!("https://{host}/").parse().unwrap();
-    let h_app = {
+    let me: url::Url = format!("https://{host}/").parse().unwrap();
+    // XXX: attempt fetching OAuth application metadata
+    let h_app: ClientMetadata = if request.client_id.domain().unwrap() == "localhost" && me.domain().unwrap() != "localhost" {
+        // If client is localhost, but we aren't localhost, generate synthetic metadata.
+        tracing::warn!("Client is localhost, not fetching metadata");
+        let mut metadata = ClientMetadata::new(request.client_id.clone(), request.client_id.clone()).unwrap();
+
+        metadata.client_name = Some("Your locally hosted app".to_string());
+
+        metadata
+    } else {
         tracing::debug!("Sending request to {} to fetch metadata", request.client_id);
-        match http.get(request.client_id.clone()).send().await {
-            Ok(response) => {
+        let metadata_request = http.get(request.client_id.clone())
+            .header("Accept", "application/json, text/html");
+        match metadata_request.send().await.and_then(|res| res.error_for_status()) {
+            Ok(response) if response.headers().typed_get::<ContentType>() == Some(ContentType::html()) => {
                 let url = response.url().clone();
                 let text = response.text().await.unwrap();
                 tracing::debug!("Received {} bytes in response", text.len());
@@ -172,7 +171,7 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
                                 .into_response()
                         }
 
-                        mf2.items
+                        if let Some(app) = mf2.items
                             .iter()
                             .find(|&i| i.r#type.iter()
                                 .any(|i| {
@@ -181,23 +180,60 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>(
                                 })
                             )
                             .cloned()
-                            .map(|i| {
-                                serde_json::to_value(&i).unwrap()
-                            })
+                        {
+                            // Create a synthetic metadata document. Be forgiving.
+                            let mut metadata = ClientMetadata::new(
+                                request.client_id.clone(),
+                                app.properties.get("url")
+                                    .and_then(|v| v.first())
+                                    .and_then(|i| match i {
+                                        microformats::types::PropertyValue::Url(url) => Some(url.clone()),
+                                        _ => None
+                                    })
+                                    .unwrap_or_else(|| request.client_id.clone())
+                            ).unwrap();
+
+                            metadata.client_name = app.properties.get("name")
+                                .and_then(|v| v.first())
+                                .and_then(|i| match i {
+                                    microformats::types::PropertyValue::Plain(name) => Some(name.to_owned()),
+                                    _ => None
+                                });
+
+                            metadata
+                        } else {
+                            return (StatusCode::BAD_REQUEST, [("Content-Type", "text/plain")], "No h-app or JSON application metadata found.").into_response()
+                        }
                     },
                     Err(err) => {
                         tracing::error!("Error parsing application metadata: {}", err);
-                        return (StatusCode::BAD_REQUEST,
-                                [("Content-Type", "text/plain")],
-                                "Parsing application metadata failed.").into_response()
+                        return (
+                            StatusCode::BAD_REQUEST,
+                            [("Content-Type", "text/plain")],
+                            "Parsing h-app metadata failed.").into_response()
                     }
                 }
             },
+            Ok(response) => match response.json::<ClientMetadata>().await {
+                Ok(client_metadata) => {
+                    client_metadata
+                },
+                Err(err) => {
+                    tracing::error!("Error parsing JSON application metadata: {}", err);
+                    return (
+                        StatusCode::BAD_REQUEST,
+                        [("Content-Type", "text/plain")],
+                        format!("Parsing OAuth2 JSON app metadata failed: {}", err)
+                    ).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()
+                return (
+                    StatusCode::BAD_REQUEST,
+                    [("Content-Type", "text/plain")],
+                    format!("Fetching app metadata failed: {}", err)
+                ).into_response()
             }
         }
     };
@@ -233,6 +269,7 @@ struct AuthorizationConfirmation {
     request: AuthorizationRequest
 }
 
+#[tracing::instrument(skip(auth, credential))]
 async fn verify_credential<A: AuthBackend>(
     auth: &A,
     website: &url::Url,
@@ -272,6 +309,7 @@ async fn authorization_endpoint_confirm<A: AuthBackend>(
         authorization_method: credential,
         request: mut auth
     } = confirmation;
+
     match verify_credential(&backend, &website, credential, challenge_id).await {
         Ok(verified) => if !verified {
             error!("User failed verification, bailing out.");