diff --git a/src/web/auth.rs b/src/web/auth.rs index 3966102..8d0d64d 100644 --- a/src/web/auth.rs +++ b/src/web/auth.rs @@ -156,12 +156,14 @@ pub(super) async fn request_is_allowed(pool: &PgPool, user: &WebUser, path: &str channel_id.is_some_and(|channel_id| user.channel_ids.contains(&channel_id)) } -pub(super) async fn login(session: Session) -> Response { +pub(super) async fn login(session: Session, Query(query): Query) -> Response { match session.get::("user").await { Ok(Some(_)) => Redirect::to("/").into_response(), Ok(None) => Html(render_page( "Log in", - &render_template(&LoginTemplate), + &render_template(&LoginTemplate { + error: query.error.as_deref(), + }), false, )) .into_response(), @@ -354,6 +356,9 @@ pub(super) async fn discord_callback( session: Session, Query(query): Query, ) -> Response { + if !query.code.is_some() { + return oauth2_error_handling(query.error.as_deref().unwrap_or("none")); + } let state = session.remove::("oauth_state").await; if !matches!(state, Ok(Some(state)) if state == query.state) { return (StatusCode::BAD_REQUEST, "Invalid OAuth state").into_response(); @@ -365,7 +370,7 @@ pub(super) async fn discord_callback( ("client_id", data.client_id.as_str()), ("client_secret", data.client_secret.as_str()), ("grant_type", "authorization_code"), - ("code", query.code.as_str()), + ("code", query.code.expect("Missing code in query").as_str()), ("redirect_uri", data.redirect_uri.as_str()), ]) .send() @@ -424,6 +429,46 @@ pub(super) async fn discord_callback( Redirect::to("/").into_response() } +fn oauth2_error_handling(error_code: &str) -> Response { + return match error_code { + "access_denied" => Redirect::to( + "/login?error=You denied the request. Please log in again and hit Authorize to continue." + ).into_response(), + + "invalid_request" => Redirect::to( + "/login?error=Error code: invalid_request. Please try again. If this keeps happening, report this issue." + ).into_response(), + + "unauthorized_client" => Redirect::to( + "/login?error=Error code: unauthorized_client. Please try again. If this keeps happening, report this issue." + ).into_response(), + + "unsupported_response_type" => Redirect::to( + "/login?error=Error code: unsupported_response_type. Please try again. If this keeps happening, report this issue." + ).into_response(), + + "invalid_scope" => Redirect::to( + "/login?error=Error code: invalid_scope. Please try again. If this keeps happening, report this issue. Also if this is just you messing with the oauth2 scope, stop." + ).into_response(), + + "server_error" => Redirect::to( + "/login?error=Discord encountered an internal error. Please try again." + ).into_response(), + + "temporarily_unavailable" => Redirect::to( + "/login?error=Discord authentication is temporarily unavailable. Please try again later and check discordstatus.com for updates." + ).into_response(), + + "none" => Redirect::to( + "/login?error=Error code: none. Please try again. If this keeps happening, report this issue. Also if this is just you messing with the oauth2 scope, stop." + ).into_response(), + + _ => Redirect::to( + "/login?error=Error code: unknown. Please try again. If this keeps happening, report this issue." + ).into_response(), + }; +} + pub(super) async fn logout(session: Session) -> Redirect { let _ = session.delete().await; Redirect::to("/login") diff --git a/src/web/mod.rs b/src/web/mod.rs index a3098d4..dbd5f84 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -90,7 +90,9 @@ struct TimezoneContext { #[derive(Template)] #[template(path = "login.html")] -struct LoginTemplate; +struct LoginTemplate<'a> { + error: Option<&'a str>, +} #[derive(Template)] #[template(path = "privacy.html")] @@ -432,9 +434,16 @@ struct ErrorTemplate<'a> { message: &'a str, } +#[derive(Deserialize)] +struct LoginQuery { + error: Option, +} + #[derive(Deserialize)] struct OAuthQuery { - code: String, + code: Option, + error: Option, + error_description: Option, state: String, } @@ -991,6 +1000,15 @@ mod tests { assert!(html.contains("<new>")); } + #[test] + fn login_page_renders_and_escapes_query_error() { + let html = render_template(&LoginTemplate { + error: Some("Login failed: "), + }); + + assert!(html.contains("Login failed: <try again>")); + } + #[test] fn renders_short_pagination_without_arbitrary_page_form() { let pagination = Pagination::new(2, ITEMS_PER_PAGE * 3); diff --git a/static/login.html b/static/login.html index cb8919f..54060ed 100644 --- a/static/login.html +++ b/static/login.html @@ -1,4 +1,15 @@

Log in

+{% if let Some(error) = error %} +
+ {{ error }} +
+{% endif %}

It is required to log in with Discord to view the archive.

The login happens on the official Discord website. This site does not have access to your email or password.