Added 403 page. Added error callback handler
Build and Publish / Build and Publish Docker Image (push) Successful in 7m25s
Build and Publish / Build and Publish Docker Image (push) Successful in 7m25s
This commit is contained in:
+137
-6
@@ -120,15 +120,53 @@ async fn auth_inner(state: &AppState, headers: &HeaderMap) -> Response {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CallbackParams {
|
||||
pub code: String,
|
||||
pub state: String,
|
||||
pub code: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub error_description: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn callback(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<CallbackParams>,
|
||||
) -> Response {
|
||||
let auth_state = match decrypt_state::<AuthStatePayload>(&state.crypto, ¶ms.state) {
|
||||
// Handle OIDC error responses (e.g. authentication_expired, access_denied)
|
||||
if let Some(error) = ¶ms.error {
|
||||
let desc = params.error_description.as_deref().unwrap_or("");
|
||||
tracing::warn!(error, description = desc, "OIDC provider returned error");
|
||||
metrics::counter!("callback_requests_total", "result" => "error").increment(1);
|
||||
|
||||
// If we have state, try to redirect back to the original URL to retry
|
||||
if let Some(state_str) = ¶ms.state {
|
||||
if let Ok(auth_state) = decrypt_state::<AuthStatePayload>(&state.crypto, state_str) {
|
||||
return redirect_to_login(&state, &auth_state.original_url);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("Authentication failed: {} {}", error, desc),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let code = match ¶ms.code {
|
||||
Some(c) => c.as_str(),
|
||||
None => {
|
||||
metrics::counter!("callback_requests_total", "result" => "error").increment(1);
|
||||
return (StatusCode::BAD_REQUEST, "missing code parameter").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let state_str = match ¶ms.state {
|
||||
Some(s) => s.as_str(),
|
||||
None => {
|
||||
metrics::counter!("callback_requests_total", "result" => "error").increment(1);
|
||||
return (StatusCode::BAD_REQUEST, "missing state parameter").into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let auth_state = match decrypt_state::<AuthStatePayload>(&state.crypto, state_str) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "invalid callback state");
|
||||
@@ -139,7 +177,7 @@ pub async fn callback(
|
||||
|
||||
let token_response = match state
|
||||
.oidc
|
||||
.exchange_code(¶ms.code, &auth_state.pkce_verifier)
|
||||
.exchange_code(code, &auth_state.pkce_verifier)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
@@ -354,7 +392,7 @@ async fn authorize_request(state: &AppState, session: &Session, host: &str) -> R
|
||||
Some(r) => r,
|
||||
None => {
|
||||
tracing::debug!(host, "host not found in routes, denying");
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
return forbidden_page(host, &session.username, "This service is not registered.");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -369,7 +407,14 @@ async fn authorize_request(state: &AppState, session: &Session, host: &str) -> R
|
||||
user = session.username,
|
||||
"user not in allowed groups"
|
||||
);
|
||||
return StatusCode::FORBIDDEN.into_response();
|
||||
return forbidden_page(
|
||||
host,
|
||||
&session.username,
|
||||
&format!(
|
||||
"You need to be a member of one of these groups: {}",
|
||||
route.allowed_groups.join(", ")
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +431,92 @@ async fn authorize_request(state: &AppState, session: &Session, host: &str) -> R
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn forbidden_page(host: &str, username: &str, reason: &str) -> Response {
|
||||
let html = format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>403 - Access Denied</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
||||
background: #0f0f0f;
|
||||
color: #e0e0e0;
|
||||
}}
|
||||
.box {{
|
||||
text-align: center;
|
||||
padding: 3rem 2rem;
|
||||
max-width: 480px;
|
||||
}}
|
||||
.lock {{
|
||||
font-size: 5rem;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}}
|
||||
h1 {{
|
||||
font-size: 1.8rem;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 0.5rem;
|
||||
}}
|
||||
.host {{
|
||||
font-size: 0.95rem;
|
||||
color: #888;
|
||||
margin-bottom: 1.5rem;
|
||||
word-break: break-all;
|
||||
}}
|
||||
.reason {{
|
||||
font-size: 1rem;
|
||||
color: #aaa;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.5;
|
||||
}}
|
||||
.user {{
|
||||
display: inline-block;
|
||||
background: #1a1a2e;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.85rem;
|
||||
color: #7c7cf0;
|
||||
}}
|
||||
.hint {{
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<div class="lock">🚫</div>
|
||||
<h1>Access Denied</h1>
|
||||
<div class="host">{host}</div>
|
||||
<div class="reason">{reason}</div>
|
||||
<div class="user">Logged in as <strong>{username}</strong></div>
|
||||
<div class="hint">Contact your administrator if you think this is a mistake.</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
host = host,
|
||||
reason = reason,
|
||||
username = username,
|
||||
);
|
||||
|
||||
(
|
||||
StatusCode::FORBIDDEN,
|
||||
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
|
||||
html,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn redirect_to_login(state: &AppState, original_url: &str) -> Response {
|
||||
let (pkce_verifier, pkce_challenge) = generate_pkce();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user