1 Commits
Author SHA1 Message Date
Elias Wendland 9328183ce4 Handle oauth2 errors
CI and release / Detect release commit (push) Successful in 31s
CI and release / Run tests (push) Successful in 4m37s
CI and release / Build and publish container (push) Successful in 7m25s
CI and release / Create release (push) Skipped
2026-09-02 10:16:09 +02:00
3 changed files with 79 additions and 5 deletions

No files matched your search

+48 -3
View File
@@ -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)) 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<LoginQuery>) -> Response {
match session.get::<WebUser>("user").await { match session.get::<WebUser>("user").await {
Ok(Some(_)) => Redirect::to("/").into_response(), Ok(Some(_)) => Redirect::to("/").into_response(),
Ok(None) => Html(render_page( Ok(None) => Html(render_page(
"Log in", "Log in",
&render_template(&LoginTemplate), &render_template(&LoginTemplate {
error: query.error.as_deref(),
}),
false, false,
)) ))
.into_response(), .into_response(),
@@ -354,6 +356,9 @@ pub(super) async fn discord_callback(
session: Session, session: Session,
Query(query): Query<OAuthQuery>, Query(query): Query<OAuthQuery>,
) -> Response { ) -> Response {
if !query.code.is_some() {
return oauth2_error_handling(query.error.as_deref().unwrap_or("none"));
}
let state = session.remove::<String>("oauth_state").await; let state = session.remove::<String>("oauth_state").await;
if !matches!(state, Ok(Some(state)) if state == query.state) { if !matches!(state, Ok(Some(state)) if state == query.state) {
return (StatusCode::BAD_REQUEST, "Invalid OAuth state").into_response(); 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_id", data.client_id.as_str()),
("client_secret", data.client_secret.as_str()), ("client_secret", data.client_secret.as_str()),
("grant_type", "authorization_code"), ("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()), ("redirect_uri", data.redirect_uri.as_str()),
]) ])
.send() .send()
@@ -424,6 +429,46 @@ pub(super) async fn discord_callback(
Redirect::to("/").into_response() 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 { pub(super) async fn logout(session: Session) -> Redirect {
let _ = session.delete().await; let _ = session.delete().await;
Redirect::to("/login") Redirect::to("/login")
+20 -2
View File
@@ -90,7 +90,9 @@ struct TimezoneContext {
#[derive(Template)] #[derive(Template)]
#[template(path = "login.html")] #[template(path = "login.html")]
struct LoginTemplate; struct LoginTemplate<'a> {
error: Option<&'a str>,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "privacy.html")] #[template(path = "privacy.html")]
@@ -432,9 +434,16 @@ struct ErrorTemplate<'a> {
message: &'a str, message: &'a str,
} }
#[derive(Deserialize)]
struct LoginQuery {
error: Option<String>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct OAuthQuery { struct OAuthQuery {
code: String, code: Option<String>,
error: Option<String>,
error_description: Option<String>,
state: String, state: String,
} }
@@ -991,6 +1000,15 @@ mod tests {
assert!(html.contains("&#60;new&#62;")); assert!(html.contains("&#60;new&#62;"));
} }
#[test]
fn login_page_renders_and_escapes_query_error() {
let html = render_template(&LoginTemplate {
error: Some("Login failed: <try again>"),
});
assert!(html.contains("Login failed: &#60;try again&#62;"));
}
#[test] #[test]
fn renders_short_pagination_without_arbitrary_page_form() { fn renders_short_pagination_without_arbitrary_page_form() {
let pagination = Pagination::new(2, ITEMS_PER_PAGE * 3); let pagination = Pagination::new(2, ITEMS_PER_PAGE * 3);
+11
View File
@@ -1,4 +1,15 @@
<h2>Log in</h2> <h2>Log in</h2>
{% if let Some(error) = error %}
<div style="
background-color: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
border-radius: 6px;
padding: 12px 16px;
margin: 12px 0;">
{{ error }}
</div>
{% endif %}
<p>It is required to log in with Discord to view the archive.</p> <p>It is required to log in with Discord to view the archive.</p>
<p>The login happens on the official Discord website. This site does not have access to your email or password.</p> <p>The login happens on the official Discord website. This site does not have access to your email or password.</p>
<form method="get" action="/login/discord"> <form method="get" action="/login/discord">