about summary refs log tree commit diff
path: root/src/frontend/login.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/login.rs')
-rw-r--r--src/frontend/login.rs235
1 files changed, 147 insertions, 88 deletions
diff --git a/src/frontend/login.rs b/src/frontend/login.rs
index 09fa75f..1c7c662 100644
--- a/src/frontend/login.rs
+++ b/src/frontend/login.rs
@@ -1,15 +1,15 @@
-use std::convert::TryInto;
-use std::str::FromStr;
+use http_types::Mime;
 use log::{debug, error};
 use rand::Rng;
-use http_types::Mime;
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use std::convert::TryInto;
+use std::str::FromStr;
 use tide::{Request, Response, Result};
-use serde::{Serialize, Deserialize};
-use sha2::{Sha256, Digest};
 
-use crate::frontend::{FrontendError, IndiewebEndpoints};
-use crate::{ApplicationState, database::Storage};
 use crate::frontend::templates::Template;
+use crate::frontend::{FrontendError, IndiewebEndpoints};
+use crate::{database::Storage, ApplicationState};
 
 markup::define! {
     LoginPage {
@@ -34,30 +34,36 @@ pub async fn form<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
     let storage = &req.state().storage;
     let authorization_endpoint = req.state().authorization_endpoint.to_string();
     let token_endpoint = req.state().token_endpoint.to_string();
-    let blog_name = storage.get_setting("site_name", &owner).await.unwrap_or_else(|_| "Kitty Box!".to_string());
+    let blog_name = storage
+        .get_setting("site_name", &owner)
+        .await
+        .unwrap_or_else(|_| "Kitty Box!".to_string());
     let feeds = storage.get_channels(&owner).await.unwrap_or_default();
 
     Ok(Response::builder(200)
-       .body(Template {
-           title: "Sign in with IndieAuth",
-           blog_name: &blog_name,
-           endpoints: IndiewebEndpoints {
-               authorization_endpoint,
-               token_endpoint,
-               webmention: None,
-               microsub: None
-           },
-           feeds,
-           user: req.session().get("user"),
-           content: LoginPage {}.to_string(),
-       }.to_string())
-       .content_type("text/html; charset=utf-8")
-       .build())
+        .body(
+            Template {
+                title: "Sign in with IndieAuth",
+                blog_name: &blog_name,
+                endpoints: IndiewebEndpoints {
+                    authorization_endpoint,
+                    token_endpoint,
+                    webmention: None,
+                    microsub: None,
+                },
+                feeds,
+                user: req.session().get("user"),
+                content: LoginPage {}.to_string(),
+            }
+            .to_string(),
+        )
+        .content_type("text/html; charset=utf-8")
+        .build())
 }
 
 #[derive(Serialize, Deserialize)]
 struct LoginForm {
-    url: String
+    url: String,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -67,21 +73,19 @@ struct IndieAuthClientState {
     /// The user's initial "me" value.
     me: String,
     /// Authorization endpoint used.
-    authorization_endpoint: String
+    authorization_endpoint: String,
 }
 
-
 #[derive(Serialize, Deserialize)]
 struct IndieAuthRequestParams {
     response_type: String,         // can only have "code". TODO make an enum
     client_id: String,             // always a URL. TODO consider making a URL
     redirect_uri: surf::Url,       // callback URI for IndieAuth
-    state: String,                 // CSRF protection, should include randomness and be passed through
-    code_challenge: String,        // base64-encoded PKCE challenge
+    state: String, // CSRF protection, should include randomness and be passed through
+    code_challenge: String, // base64-encoded PKCE challenge
     code_challenge_method: String, // usually "S256". TODO make an enum
-    scope: Option<String>,         // oAuth2 scopes to grant,
-    me: surf::Url,                 // User's entered profile URL
-    
+    scope: Option<String>, // oAuth2 scopes to grant,
+    me: surf::Url, // User's entered profile URL
 }
 
 /// Handle login requests. Find the IndieAuth authorization endpoint and redirect to it.
@@ -91,16 +95,22 @@ pub async fn handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resul
         return Err(FrontendError::with_code(400, "Use the login form, Luke.").into());
     }
     if content_type.unwrap() != Mime::from_str("application/x-www-form-urlencoded").unwrap() {
-        return Err(FrontendError::with_code(400, "Login form results must be a urlencoded form").into());
+        return Err(
+            FrontendError::with_code(400, "Login form results must be a urlencoded form").into(),
+        );
     }
 
     let form = req.body_form::<LoginForm>().await?; // FIXME check if it returns 400 or 500 on error
     let homepage_uri = surf::Url::parse(&form.url)?;
     let http = &req.state().http_client;
-    
+
     let mut fetch_response = http.get(&homepage_uri).send().await?;
     if fetch_response.status() != 200 {
-        return Err(FrontendError::with_code(500, "Error fetching your authorization endpoint. Check if your website's okay.").into());
+        return Err(FrontendError::with_code(
+            500,
+            "Error fetching your authorization endpoint. Check if your website's okay.",
+        )
+        .into());
     }
 
     let mut authorization_endpoint: Option<surf::Url> = None;
@@ -123,14 +133,18 @@ pub async fn handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resul
                                 || trimmed == "rel=authorization_endpoint"
                             {
                                 if let Ok(endpoint) = homepage_uri.join(uri) {
-                                    debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str());
+                                    debug!(
+                                        "Found authorization endpoint {} for user {}",
+                                        endpoint,
+                                        homepage_uri.as_str()
+                                    );
                                     authorization_endpoint = Some(endpoint);
                                     break;
                                 }
                             }
                         }
                     }
-                },
+                }
                 None => continue,
             }
         }
@@ -139,64 +153,93 @@ pub async fn handler<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resul
     // bring out the big guns and parse HTML to find it.
     if authorization_endpoint.is_none() {
         let body = fetch_response.body_string().await?;
-        let pattern = easy_scraper::Pattern::new(r#"<link rel="authorization_endpoint" href="{{url}}">"#)
-            .expect("Cannot parse the pattern for authorization_endpoint");
+        let pattern =
+            easy_scraper::Pattern::new(r#"<link rel="authorization_endpoint" href="{{url}}">"#)
+                .expect("Cannot parse the pattern for authorization_endpoint");
         let matches = pattern.matches(&body);
         debug!("Matches for authorization_endpoint in HTML: {:?}", matches);
         if !matches.is_empty() {
             if let Ok(endpoint) = homepage_uri.join(&matches[0]["url"]) {
-                debug!("Found authorization endpoint {} for user {}", endpoint, homepage_uri.as_str());
+                debug!(
+                    "Found authorization endpoint {} for user {}",
+                    endpoint,
+                    homepage_uri.as_str()
+                );
                 authorization_endpoint = Some(endpoint)
             }
         }
     };
     // If even after this the authorization endpoint is still not found, bail out.
     if authorization_endpoint.is_none() {
-        error!("Couldn't find authorization_endpoint for {}", homepage_uri.as_str());
-        return Err(FrontendError::with_code(400, "Your website doesn't support the IndieAuth protocol.").into());
+        error!(
+            "Couldn't find authorization_endpoint for {}",
+            homepage_uri.as_str()
+        );
+        return Err(FrontendError::with_code(
+            400,
+            "Your website doesn't support the IndieAuth protocol.",
+        )
+        .into());
     }
     let mut authorization_endpoint: surf::Url = authorization_endpoint.unwrap();
     let mut rng = rand::thread_rng();
-    let state: String = data_encoding::BASE64URL.encode(serde_urlencoded::to_string(IndieAuthClientState {
-        nonce: (0..8).map(|_| {
+    let state: String = data_encoding::BASE64URL.encode(
+        serde_urlencoded::to_string(IndieAuthClientState {
+            nonce: (0..8)
+                .map(|_| {
+                    let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len());
+                    INDIEAUTH_PKCE_CHARSET[idx] as char
+                })
+                .collect(),
+            me: homepage_uri.to_string(),
+            authorization_endpoint: authorization_endpoint.to_string(),
+        })?
+        .as_bytes(),
+    );
+    // PKCE code generation
+    let code_verifier: String = (0..128)
+        .map(|_| {
             let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len());
             INDIEAUTH_PKCE_CHARSET[idx] as char
-        }).collect(),
-        me: homepage_uri.to_string(),
-        authorization_endpoint: authorization_endpoint.to_string()
-    })?.as_bytes());
-    // PKCE code generation
-    let code_verifier: String = (0..128).map(|_| {
-        let idx = rng.gen_range(0..INDIEAUTH_PKCE_CHARSET.len());
-        INDIEAUTH_PKCE_CHARSET[idx] as char
-    }).collect();
+        })
+        .collect();
     let mut hasher = Sha256::new();
     hasher.update(code_verifier.as_bytes());
     let code_challenge: String = data_encoding::BASE64URL.encode(&hasher.finalize());
 
-    authorization_endpoint.set_query(Some(&serde_urlencoded::to_string(IndieAuthRequestParams {
-        response_type: "code".to_string(),
-        client_id: req.url().origin().ascii_serialization(),
-        redirect_uri: req.url().join("login/callback")?,
-        state: state.clone(), code_challenge,
-        code_challenge_method: "S256".to_string(),
-        scope: Some("profile".to_string()),
-        me: homepage_uri,
-    })?));
+    authorization_endpoint.set_query(Some(&serde_urlencoded::to_string(
+        IndieAuthRequestParams {
+            response_type: "code".to_string(),
+            client_id: req.url().origin().ascii_serialization(),
+            redirect_uri: req.url().join("login/callback")?,
+            state: state.clone(),
+            code_challenge,
+            code_challenge_method: "S256".to_string(),
+            scope: Some("profile".to_string()),
+            me: homepage_uri,
+        },
+    )?));
 
     let cookies = vec![
-        format!(r#"indieauth_state="{}"; Same-Site: None; Secure; Max-Age: 600"#, state),
-        format!(r#"indieauth_code_verifier="{}"; Same-Site: None; Secure; Max-Age: 600"#, code_verifier)
+        format!(
+            r#"indieauth_state="{}"; Same-Site: None; Secure; Max-Age: 600"#,
+            state
+        ),
+        format!(
+            r#"indieauth_code_verifier="{}"; Same-Site: None; Secure; Max-Age: 600"#,
+            code_verifier
+        ),
     ];
 
-    let cookie_header = cookies.iter().map(|i| -> http_types::headers::HeaderValue {
-        (i as &str).try_into().unwrap()
-    }).collect::<Vec<_>>();
-    
+    let cookie_header = cookies
+        .iter()
+        .map(|i| -> http_types::headers::HeaderValue { (i as &str).try_into().unwrap() })
+        .collect::<Vec<_>>();
+
     Ok(Response::builder(302)
-       .header("Location", authorization_endpoint.to_string())
-       .header("Set-Cookie", &*cookie_header)
-       .build())
+        .header("Location", authorization_endpoint.to_string())
+        .header("Set-Cookie", &*cookie_header)
+        .build())
 }
 
 const INDIEAUTH_PKCE_CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
@@ -211,12 +254,12 @@ struct IndieAuthCallbackResponse {
     #[allow(dead_code)]
     error_uri: Option<String>,
     // This needs to be further decoded to receive state back and will always be present
-    state: String
+    state: String,
 }
 
 impl IndieAuthCallbackResponse {
     fn is_successful(&self) -> bool {
-        !self.code.is_none()
+        self.code.is_some()
     }
 }
 
@@ -226,7 +269,7 @@ struct IndieAuthCodeRedeem {
     code: String,
     client_id: String,
     redirect_uri: String,
-    code_verifier: String
+    code_verifier: String,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -234,7 +277,7 @@ struct IndieWebProfile {
     name: Option<String>,
     url: Option<String>,
     email: Option<String>,
-    photo: Option<String>
+    photo: Option<String>,
 }
 
 #[derive(Serialize, Deserialize)]
@@ -243,7 +286,7 @@ struct IndieAuthResponse {
     scope: Option<String>,
     access_token: Option<String>,
     token_type: Option<String>,
-    profile: Option<IndieWebProfile>
+    profile: Option<IndieWebProfile>,
 }
 
 /// Handle IndieAuth parameters, fetch the final h-card and redirect the user to the homepage.
@@ -252,41 +295,57 @@ pub async fn callback<S: Storage>(mut req: Request<ApplicationState<S>>) -> Resu
     let http: &surf::Client = &req.state().http_client;
     let origin = req.url().origin().ascii_serialization();
 
-    if req.cookie("indieauth_state").unwrap().value() != &params.state {
+    if req.cookie("indieauth_state").unwrap().value() != params.state {
         return Err(FrontendError::with_code(400, "The state doesn't match. A possible CSRF attack was prevented. Please try again later.").into());
     }
-    let state: IndieAuthClientState = serde_urlencoded::from_bytes(
-        &data_encoding::BASE64URL.decode(params.state.as_bytes())?
-    )?;
+    let state: IndieAuthClientState =
+        serde_urlencoded::from_bytes(&data_encoding::BASE64URL.decode(params.state.as_bytes())?)?;
 
     if !params.is_successful() {
-        return Err(FrontendError::with_code(400, &format!("The authorization endpoint indicated a following error: {:?}: {:?}", &params.error, &params.error_description)).into())
+        return Err(FrontendError::with_code(
+            400,
+            &format!(
+                "The authorization endpoint indicated a following error: {:?}: {:?}",
+                &params.error, &params.error_description
+            ),
+        )
+        .into());
     }
 
     let authorization_endpoint = surf::Url::parse(&state.authorization_endpoint).unwrap();
-    let mut code_response = http.post(authorization_endpoint)
+    let mut code_response = http
+        .post(authorization_endpoint)
         .body_string(serde_urlencoded::to_string(IndieAuthCodeRedeem {
             grant_type: "authorization_code".to_string(),
             code: params.code.unwrap().to_string(),
             client_id: origin.to_string(),
             redirect_uri: origin + "/login/callback",
-            code_verifier: req.cookie("indieauth_code_verifier").unwrap().value().to_string()
+            code_verifier: req
+                .cookie("indieauth_code_verifier")
+                .unwrap()
+                .value()
+                .to_string(),
         })?)
         .header("Content-Type", "application/x-www-form-urlencoded")
         .header("Accept", "application/json")
-        .send().await?;
+        .send()
+        .await?;
 
     if code_response.status() != 200 {
-        return Err(FrontendError::with_code(code_response.status(), &format!("Authorization endpoint returned an error when redeeming the code: {}", code_response.body_string().await?)).into());
+        return Err(FrontendError::with_code(
+            code_response.status(),
+            &format!(
+                "Authorization endpoint returned an error when redeeming the code: {}",
+                code_response.body_string().await?
+            ),
+        )
+        .into());
     }
 
     let json: IndieAuthResponse = code_response.body_json().await?;
-    drop(http);
     let session = req.session_mut();
     session.insert("user", &json.me)?;
 
     // TODO redirect to the page user came from
-    Ok(Response::builder(302)
-       .header("Location", "/")
-       .build())
+    Ok(Response::builder(302).header("Location", "/").build())
 }