diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/login.rs | 55 |
1 files changed, 54 insertions, 1 deletions
diff --git a/src/api/login.rs b/src/api/login.rs index 1faadf7..73304e8 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -365,6 +365,7 @@ pub(crate) async fn refresh( pub(crate) async fn session( State(state): State<AppState>, token: Option<Extension<Token>>, + headers: axum::http::HeaderMap, body: Option<Json<LimitRequest>>, ) -> Result<Response, ApiError> { let claims = refreshed(&state, token, body).await?; @@ -374,8 +375,9 @@ pub(crate) async fn session( .as_ref() .ok_or(ApiError(StatusCode::SERVICE_UNAVAILABLE, None))?; let max_age = claims.exp.saturating_sub(claims.iat) / 1000; + let secure = if over_tls(&headers) { "; Secure" } else { "" }; let cookie = format!( - "{SESSION_COOKIE}={}; Path=/; HttpOnly; SameSite=Strict; Max-Age={max_age}", + "{SESSION_COOKIE}={}; Path=/; HttpOnly; SameSite=Strict; Max-Age={max_age}{secure}", token::issue(key, &claims) ); if let Ok(value) = axum::http::HeaderValue::from_str(&cookie) { @@ -386,6 +388,37 @@ pub(crate) async fn session( Ok(response) } +/// Whether the request came over TLS, as a reverse proxy in front says +/// (`X-Forwarded-Proto`, `Forwarded`): its session cookie is then sent over TLS only +/// (`Secure`), not over plain HTTP should someone type `http://`. A client saying so +/// itself only keeps its own cookie off plain HTTP. +fn over_tls(headers: &axum::http::HeaderMap) -> bool { + let said = |name: &str| { + headers + .get_all(name) + .iter() + .filter_map(|value| value.to_str().ok()) + .collect::<Vec<_>>() + .join(",") + }; + let forwarded_proto = said("x-forwarded-proto"); + let forwarded = said("forwarded"); + forwarded_proto + .split(',') + .next() + .is_some_and(|proto| proto.trim().eq_ignore_ascii_case("https")) + || forwarded + .split(',') + .next() + .into_iter() + .flat_map(|first| first.split(';')) + .filter_map(|pair| pair.trim().split_once('=')) + .any(|(name, value)| { + name.eq_ignore_ascii_case("proto") + && value.trim_matches('"').eq_ignore_ascii_case("https") + }) +} + /// `DELETE /auth/session`: the session cookie forgotten (the token itself stays valid /// until it expires; `POST /auth/logout` ends every one). A stale one is ignored anyway. pub(crate) async fn end_session() -> Response { @@ -550,6 +583,26 @@ mod tests { assert!(!within(&[cap(&["notes"], &["read", "grant"])], &earlier)); } + /// Over TLS as the proxy says: the first hop's protocol, either header. + #[test] + fn over_tls_as_the_proxy_says() { + let with = |name: &'static str, value: &'static str| { + let mut headers = axum::http::HeaderMap::new(); + headers.insert(name, axum::http::HeaderValue::from_static(value)); + over_tls(&headers) + }; + assert!(with("x-forwarded-proto", "https")); + assert!(with("x-forwarded-proto", "HTTPS, http")); + assert!(!with("x-forwarded-proto", "http")); + assert!(with( + "forwarded", + "for=1.2.3.4;proto=https;host=example.org" + )); + assert!(with("forwarded", "proto=\"https\"")); + assert!(!with("forwarded", "proto=http, proto=https")); + assert!(!over_tls(&axum::http::HeaderMap::new())); + } + /// A token asked to live less ends its session with it; another, the session goes on. #[test] fn a_short_token_ends_its_session() { |
