use std::sync::Arc;
use http::HeaderMap;
use crate::auth::cookie::CookieService;
use crate::auth::jwt::{decode_bearer_token, JwtService};
use crate::auth::session::{Session, SessionManager};
use crate::core::engine::FrozenDiContainer;
use crate::web::context::Claims;
pub struct AuthExtraction {
pub claims: Option<Arc<Claims>>,
pub session: Option<Arc<Session>>,
}
pub fn extract_claims(headers: &HeaderMap, container: &FrozenDiContainer) -> Option<Arc<Claims>> {
decode_bearer_token(headers, container).or_else(|| {
let cookie = container.try_get::<CookieService>()?;
let jwt = container.try_get::<JwtService>()?;
let value = cookie.extract(headers)?;
jwt.decode_access(&value)
})
}
pub fn extract_claims_ws(
headers: &HeaderMap,
query: Option<&str>,
container: &FrozenDiContainer,
) -> Option<Arc<Claims>> {
extract_claims(headers, container).or_else(|| {
let token = access_token_from_query(query?)?;
container.try_get::<JwtService>()?.decode_access(&token)
})
}
fn access_token_from_query(query: &str) -> Option<String> {
serde_urlencoded::from_str::<Vec<(String, String)>>(query)
.ok()?
.into_iter()
.find(|(k, _)| k == "access_token")
.map(|(_, v)| v)
.filter(|t| !t.is_empty())
}
pub async fn extract_auth(headers: &HeaderMap, container: &FrozenDiContainer) -> AuthExtraction {
let claims = extract_claims(headers, container);
let session = match container.try_get::<SessionManager>() {
Some(sm) => sm.load_from_headers(headers).await,
None => None,
};
AuthExtraction { claims, session }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::jwt::JwtConfig;
use crate::core::engine::DiContainerBuilder;
fn jwt() -> JwtService {
JwtService::new(JwtConfig {
secret: "test-secret-for-ws-handshake".to_owned(),
..Default::default()
})
}
fn container_with_jwt() -> (std::sync::Arc<FrozenDiContainer>, JwtService) {
let mut b = DiContainerBuilder::new();
b.register(jwt());
(b.freeze(), jwt())
}
#[test]
fn ws_handshake_authenticates_from_the_query_token() {
let (container, jwt) = container_with_jwt();
let token = jwt
.issue_access("user-1", "admin", "u@example.com")
.unwrap();
let claims = extract_claims_ws(
&HeaderMap::new(),
Some(&format!("access_token={token}")),
&container,
)
.expect("query token must authenticate the handshake");
assert_eq!(claims.get("sub").and_then(|v| v.as_str()), Some("user-1"));
}
#[test]
fn header_wins_over_the_query_token() {
let (container, jwt) = container_with_jwt();
let header_token = jwt
.issue_access("from-header", "admin", "h@example.com")
.unwrap();
let query_token = jwt
.issue_access("from-query", "admin", "q@example.com")
.unwrap();
let mut headers = HeaderMap::new();
headers.insert(
"authorization",
format!("Bearer {header_token}").parse().unwrap(),
);
let claims = extract_claims_ws(
&headers,
Some(&format!("access_token={query_token}")),
&container,
)
.expect("header credential still authenticates");
assert_eq!(
claims.get("sub").and_then(|v| v.as_str()),
Some("from-header"),
);
}
#[test]
fn a_forged_query_token_is_rejected() {
let (container, _) = container_with_jwt();
assert!(
extract_claims_ws(
&HeaderMap::new(),
Some("access_token=not-a-real-jwt"),
&container,
)
.is_none(),
"an unverifiable token must not produce claims"
);
}
#[test]
fn no_credential_anywhere_stays_anonymous() {
let (container, _) = container_with_jwt();
assert!(extract_claims_ws(&HeaderMap::new(), None, &container).is_none());
assert!(extract_claims_ws(&HeaderMap::new(), Some("foo=bar"), &container).is_none());
assert!(extract_claims_ws(&HeaderMap::new(), Some("access_token="), &container).is_none());
}
}