use super::*; use dever_runtime::{ api, api::Request, auth::{Error, Identity, Route, Service, Verification}, }; use std::sync::{ Arc, atomic::{AtomicU64, Ordering}, }; static NEXT: AtomicU64 = AtomicU64::new(0); pub(super) fn configured(origin: Option<&str>, verify: &str) -> Result { let path = std::env::temp_dir().join(format!( "dever-auth-config-{}-{}.json", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); let settings = serde_json::json!({ "http":{"host":"127.0.0.1","port":8181}, "sites":{"admin":{"path":"admin","auth":"session","hosts":["unit.test"],"origin":origin}}, "auth":{"providers":{"session":{"verify":verify,"jwtSecret":KEY,"ttlSeconds":60,"cookie":"session"}}} }); std::fs::write(&path, settings.to_string()).unwrap(); let config = api::Config::load(&path, &["admin"]); std::fs::remove_file(path).unwrap(); Service::new( Arc::new(config?), &[Verification { name: "user.account.verify", user_model: 13, tenant_model: 17, }], &[Route { topic: "admin", required: true, user_models: &[], tenant_models: &[], }], ) } pub(super) fn request(method: &str, fields: &[(&str, &str)]) -> Request { let mut request = hyper::Request::builder() .method(method) .uri("/user/account/admin/me"); for (name, value) in fields { request = request.header(*name, *value); } request.body(Default::default()).unwrap() } pub(super) fn cookie(service: &Service) -> String { let request = request( "POST", &[("Host", "unit.test"), ("Origin", "https://unit.test")], ); let (mut context, claims) = service.prepare("admin", &request, true, 100).unwrap(); assert!(claims.is_none()); context .issue_cookie("user", "session-id", None, 100) .unwrap(); let mut response = api::response("{}"); assert!(!response.headers().contains_key("set-cookie")); context.publish(&mut response); let cookie = response.headers().get("set-cookie").unwrap(); let value = std::str::from_utf8(cookie.as_bytes()).unwrap(); assert!(value.contains("Secure; HttpOnly; SameSite=Lax; Max-Age=60")); value.split(';').next().unwrap().into() } #[test] fn authentication_needs_compiled_verifier_before_it_establishes_identity() { assert!(configured(None, "user.account.verify").is_err()); assert!(configured(Some("https://unit.test"), "user.account.arbitrary").is_err()); for origin in [ "https://unit.test/path", "https://unit.test?query", "https://user@unit.test", "https://unit.test:bad", "https://unit.test:99999", "https://unit.test:0", ] { assert!( configured(Some(origin), "user.account.verify").is_err(), "{origin}" ); } let service = configured(Some("https://unit.test"), "user.account.verify").unwrap(); let cookie = cookie(&service); let request = request("GET", &[("Host", "unit.test"), ("Cookie", &cookie)]); let (mut context, claims) = service.prepare("admin", &request, false, 100).unwrap(); assert_eq!(context.id().unwrap_err(), Error::Unauthorized); assert_eq!(claims.unwrap().subject, "user"); assert_eq!(context.verifier(), Some("user.account.verify")); context .establish(Identity { id: "verified-user".into(), user_id: Some(7), tenant_id: None, }) .unwrap(); assert_eq!(context.id().unwrap(), "verified-user"); assert_eq!(context.session().unwrap(), "session-id"); assert_eq!(context.user_id(Some(13)).unwrap(), Some(7)); assert_eq!(context.user_id(Some(17)).unwrap_err(), Error::Internal); } #[test] fn cookie_writes_require_exact_external_origin_and_host() { let service = configured(Some("https://unit.test"), "user.account.verify").unwrap(); let cookie = cookie(&service); for fields in [ vec![("Host", "unit.test")], vec![("Host", "unit.test"), ("Origin", "null")], vec![("Host", "unit.test"), ("Origin", "http://unit.test")], vec![("Host", "unit.test"), ("Origin", "https://unit.test:444")], vec![ ("Host", "internal.test"), ("Origin", "https://unit.test"), ("X-Forwarded-Host", "unit.test"), ], vec![("Host", "unit.test:bad"), ("Origin", "https://unit.test")], vec![ ("Host", "unit.test"), ("Origin", "https://unit.test"), ("Origin", "https://unit.test"), ], ] { let mut fields = fields; fields.push(("Cookie", &cookie)); let request = request("POST", &fields); assert!(matches!( service.prepare("admin", &request, false, 100), Err(Error::Forbidden) )); let (mut public, _) = service.prepare("admin", &request, true, 100).unwrap(); assert_eq!(public.clear_cookie().unwrap_err(), Error::Forbidden); let mut response = api::response("{}"); public.publish(&mut response); assert!(!response.headers().contains_key("set-cookie")); } let request = request( "POST", &[ ("Host", "UNIT.TEST:443"), ("Origin", "https://unit.test"), ("Cookie", &cookie), ], ); assert!(service.prepare("admin", &request, false, 100).is_ok()); } #[test] fn public_ignores_old_cookie_but_rejects_explicit_bad_authorization() { let service = configured(Some("https://unit.test"), "user.account.verify").unwrap(); let cookie = cookie(&service); let old = request( "GET", &[("Host", "unit.test"), ("Cookie", "session=old-expired")], ); assert!( service .prepare("admin", &old, true, 100) .unwrap() .1 .is_none() ); assert!(matches!( service.prepare("admin", &old, false, 100), Err(Error::Unauthorized) )); for fields in [ vec![("Authorization", "Bearer invalid")], vec![ ("Authorization", "Bearer invalid"), ("Authorization", "Bearer invalid"), ], vec![ ("Cookie", cookie.as_str()), ("Authorization", "Bearer invalid"), ], vec![("Cookie", cookie.as_str()), ("Cookie", cookie.as_str())], ] { let request = request("GET", &fields); assert!(matches!( service.prepare("admin", &request, false, 100), Err(Error::Unauthorized) )); if fields.iter().any(|(name, _)| *name == "Authorization") { assert!(matches!( service.prepare("admin", &request, true, 100), Err(Error::Unauthorized) )); } } }