use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use dever_runtime::{ adapter::Document, auth::{Claims, Settings}, value::secret::Secret, }; use hmac::{Hmac, Mac}; use sha2::Sha256; #[cfg(feature = "authorization")] mod entry; mod metadata; mod request; mod tenant; const KEY: &str = "unit-test-only-key-not-for-deployment"; fn settings() -> Settings { let document = Document::parse(serde_json::json!({"auth":{"providers":{ "session":{"verify":"user.account.verify","jwtSecret":KEY,"ttlSeconds":60,"cookie":"session"}, "other":{"verify":"user.account.verify","jwtSecret":KEY,"ttlSeconds":60} }}}).to_string()).unwrap(); Settings::load(&document).unwrap() } fn signed(header: &str, payload: &str, key: &str) -> Secret { let signed = format!( "{}.{}", URL_SAFE_NO_PAD.encode(header), URL_SAFE_NO_PAD.encode(payload) ); let mut mac = Hmac::::new_from_slice(key.as_bytes()).unwrap(); mac.update(signed.as_bytes()); Secret::from_input( format!( "{signed}.{}", URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()) ) .into_bytes(), ) } #[test] fn signed_claims_remain_bound_to_provider_site_session_and_time() { let settings = settings(); let provider = &settings.providers["session"]; let claims = Claims { subject: "账号".into(), session: "opaque-session".into(), tenant: None, site: "admin".into(), }; let token = provider.issue(&claims, 100).unwrap(); assert_eq!(provider.verify_token(&token, "admin", 100).unwrap(), claims); assert_eq!(provider.verify_token(&token, "admin", 159).unwrap(), claims); assert!(provider.verify_token(&token, "admin", 160).is_err()); assert!(provider.verify_token(&token, "admin", 99).is_err()); assert!(provider.verify_token(&token, "front", 100).is_err()); assert!( settings.providers["other"] .verify_token(&token, "admin", 100) .is_err() ); assert!(!format!("{token:?}").contains("opaque-session")); } #[test] fn token_validation_rejects_algorithm_confusion_tampering_and_ambiguous_claims() { let settings = settings(); let provider = &settings.providers["session"]; let payload = r#"{"iss":"session","aud":"admin","sub":"u","sid":"s","tenant":null,"iat":100,"exp":160}"#; let header = r#"{"alg":"HS256","typ":"JWT"}"#; assert!( provider .verify_token(&signed(header, payload, KEY), "admin", 100) .is_ok() ); for token in [ signed(r#"{"alg":"none","typ":"JWT"}"#, payload, KEY), signed(r#"{"alg":"RS256","typ":"JWT"}"#, payload, KEY), signed(header, payload, "different-unit-test-signing-key"), signed( header, &payload.replace("\"sub\":\"u\"", "\"sub\":\"u\",\"sub\":\"v\""), KEY, ), signed( header, &payload.replace("\"sub\":\"u\"", "\"sub\":\"\""), KEY, ), signed(header, &payload.replace("\"exp\":160", "\"exp\":161"), KEY), signed(header, &payload.replace("\"iat\":100", "\"iat\":101"), KEY), signed(header, &payload.replace("\"iat\":100", "\"iat\":-1"), KEY), signed(header, &payload.replace("\"exp\":160", "\"exp\":1e9"), KEY), signed( header, &payload.replace("\"tenant\":null,", "\"permissions\":[\"*\"],"), KEY, ), Secret::from_input(vec![b'a'; 8193]), ] { assert_eq!( provider.verify_token(&token, "admin", 100).unwrap_err(), "invalid authentication" ); } } #[test] fn authentication_settings_require_deployment_key_and_unambiguous_fields() { for value in [ serde_json::json!({"verify":"user.account.verify","jwtSecret":"short","ttlSeconds":60}), serde_json::json!({"verify":"user.account.verify","jwtSecret":KEY,"ttlSeconds":0}), serde_json::json!({"verify":"user.account.verify","jwtSecret":KEY,"ttlSeconds":60,"cookie":"session; bad"}), serde_json::json!({"verify":"user.account","jwtSecret":KEY,"ttlSeconds":60}), serde_json::json!({"verify":"user.account.verify","jwtSecret":KEY,"ttlSeconds":60,"algorithm":"none"}), ] { let document = Document::parse( serde_json::json!({"auth":{"providers":{"session":value}}}).to_string(), ) .unwrap(); assert!(Settings::load(&document).is_err()); } assert!( Settings::load(&Document::parse("{}".into()).unwrap()) .unwrap() .providers .is_empty() ); }