From 729bf77efb0812d0aed6234576fdd06effa5019e Mon Sep 17 00:00:00 2001 From: Vika Date: Mon, 24 Oct 2022 00:51:46 +0300 Subject: indieauth: parse application metadata --- kittybox-rs/src/bin/kittybox-indieauth-helper.rs | 29 +++++++-- kittybox-rs/src/indieauth/mod.rs | 81 +++++++++++++++++------- kittybox-rs/src/main.rs | 2 +- kittybox-rs/templates/src/indieauth.rs | 23 +++++-- 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 } fn append_query_string( @@ -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::(); 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( Host(host): Host, Query(request): Query, Extension(db): Extension, + Extension(http): Extension, Extension(auth): Extension -) -> Html { +) -> 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( 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( } } -pub fn router(backend: A, db: D) -> axum::Router { +pub fn router(backend: A, db: D, http: reqwest::Client) -> axum::Router { use axum::routing::{Router, get, post}; Router::new() @@ -785,7 +819,7 @@ pub fn router(backend: A, db: D) -> axum:: .route("/webauthn/pre_register", get( #[cfg(feature = "webauthn")] webauthn::webauthn_pre_register::, - #[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(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, - app: serde_json::Value, + app: Option, 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." } -- cgit 1.4.1