use crate::dbs::capabilities::NetTarget; use crate::err::Error; use crate::kvs::Datastore; use chrono::{DateTime, Duration, Utc}; use jsonwebtoken::jwk::{AlgorithmParameters::*, Jwk, JwkSet, KeyOperations, PublicKeyUse}; use jsonwebtoken::{Algorithm::*, DecodingKey, Validation}; use once_cell::sync::Lazy; use reqwest::{Client, Url}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; pub(crate) type JwksCache = HashMap; #[derive(Clone, Serialize, Deserialize)] pub(crate) struct JwksCacheEntry { jwks: JwkSet, time: DateTime, } #[cfg(test)] static CACHE_EXPIRATION: Lazy = Lazy::new(|| Duration::seconds(1)); #[cfg(not(test))] static CACHE_EXPIRATION: Lazy = Lazy::new(|| match std::env::var("SURREAL_JWKS_CACHE_EXPIRATION_SECONDS") { Ok(seconds_str) => { let seconds = seconds_str.parse::().expect( "Expected a valid number of seconds for SURREAL_JWKS_CACHE_EXPIRATION_SECONDS", ); Duration::seconds(seconds as i64) } Err(_) => { Duration::seconds(43200) // Set default cache expiration of 12 hours } }); #[cfg(test)] static CACHE_COOLDOWN: Lazy = Lazy::new(|| Duration::seconds(300)); #[cfg(not(test))] static CACHE_COOLDOWN: Lazy = Lazy::new(|| match std::env::var("SURREAL_JWKS_CACHE_COOLDOWN_SECONDS") { Ok(seconds_str) => { let seconds = seconds_str.parse::().expect( "Expected a valid number of seconds for SURREAL_JWKS_CACHE_COOLDOWN_SECONDS", ); Duration::seconds(seconds as i64) } Err(_) => { Duration::seconds(300) // Set default cache refresh cooldown of 5 minutes } }); #[cfg(not(target_arch = "wasm32"))] static REMOTE_TIMEOUT: Lazy = Lazy::new(|| match std::env::var("SURREAL_JWKS_REMOTE_TIMEOUT_MILLISECONDS") { Ok(milliseconds_str) => { let milliseconds = milliseconds_str .parse::() .expect("Expected a valid number of milliseconds for SURREAL_JWKS_REMOTE_TIMEOUT_MILLISECONDS"); Duration::milliseconds(milliseconds as i64) } Err(_) => { Duration::milliseconds(1000) // Set default remote timeout to 1 second } }); // Generates a verification configuration from a JWKS object hosted in a remote location // Performs local caching of all JWKS objects to prevent unnecessary network requests // Implements checks to prevent denial of service and unauthorized network requests // Validates the JWK objects found in the JWKS object according to RFC 7517 // Source: https://datatracker.ietf.org/doc/html/rfc7517 pub(super) async fn config( kvs: &Datastore, kid: &str, url: &str, token_alg: jsonwebtoken::Algorithm, ) -> Result<(DecodingKey, Validation), Error> { // Retrieve JWKS cache let cache = kvs.jwks_cache(); // Attempt to fetch relevant JWK object either from local cache or remote location let jwk = match fetch_jwks_from_cache(cache, url).await { Some(jwks) => { trace!("Successfully fetched JWKS object from local cache"); // Check that the cached JWKS object has not expired yet if Utc::now().signed_duration_since(jwks.time) < *CACHE_EXPIRATION { // Attempt to find JWK in JWKS object from local cache match jwks.jwks.find(kid) { Some(jwk) => jwk.to_owned(), _ => { trace!("Could not find valid JWK object with key identifier '{kid}' in cached JWKS object"); // Check that the cached JWKS object has not been recently updated if Utc::now().signed_duration_since(jwks.time) < *CACHE_COOLDOWN { debug!("Refused to refresh cache before cooldown period is over"); return Err(Error::InvalidAuth); // Return opaque error } find_jwk_from_url(kvs, url, kid).await? } } } else { trace!("Fetched JWKS object from local cache has expired"); find_jwk_from_url(kvs, url, kid).await? } } None => { trace!("Could not fetch JWKS object from local cache"); find_jwk_from_url(kvs, url, kid).await? } }; // Use algorithm provided, if specified // This parameter is not required to be present, although is usually expected // When missing, tokens must be validated using only the required key type parameter // This is discouraged, as it requires relying on the algorithm specified in the token // Source: https://datatracker.ietf.org/doc/html/rfc7517#section-4.4 let alg = match jwk.common.algorithm { Some(alg) => alg, // If not specified, use the algorithm provided in the token header // It is critical that the JWT library prevents the "none" algorithm from being used // Reference: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/#Meet-the--None--Algorithm // In the case of "jsonwebtoken", the "none" algorithm is not part of the enumeration // Source: https://docs.rs/jsonwebtoken/latest/jsonwebtoken/enum.Algorithm.html // Confirmation: https://github.com/Keats/jsonwebtoken/issues/381 _ => { // Ensure that the algorithm specified in the token matches the key type defined in the JWK match (&jwk.algorithm, token_alg) { (RSA(_), RS256 | RS384 | RS512 | PS256 | PS384 | PS512) => token_alg, (RSA(key), _) => { warn!( "Algorithm from token '{:?}' does not match JWK key type '{:?}'", token_alg, key.key_type ); return Err(Error::InvalidAuth); // Return opaque error } (EllipticCurve(_), ES256 | ES384) => token_alg, (EllipticCurve(key), _) => { warn!( "Algorithm from token '{:?}' does not match JWK key type '{:?}'", token_alg, key.key_type ); return Err(Error::InvalidAuth); // Return opaque error } (OctetKey(_), HS256 | HS384 | HS512) => token_alg, (OctetKey(key), _) => { warn!( "Algorithm from token '{:?}' does not match JWK key type '{:?}'", token_alg, key.key_type ); return Err(Error::InvalidAuth); // Return opaque error } (OctetKeyPair(_), EdDSA) => token_alg, (OctetKeyPair(key), _) => { warn!( "Algorithm from token '{:?}' does not match JWK key type '{:?}'", token_alg, key.key_type ); return Err(Error::InvalidAuth); // Return opaque error } } } }; // Check if the key use (if specified) is intended to be used for signing // Source: https://datatracker.ietf.org/doc/html/rfc7517#section-4.2 match &jwk.common.public_key_use { Some(PublicKeyUse::Signature) => (), Some(key_use) => { warn!("Invalid value for parameter 'use' in JWK object: '{:?}'", key_use); return Err(Error::InvalidAuth); // Return opaque error } None => (), } // Check if the key operations (if specified) include verification // Source: https://datatracker.ietf.org/doc/html/rfc7517#section-4.3 if let Some(ops) = &jwk.common.key_operations { if !ops.iter().any(|op| *op == KeyOperations::Verify) { warn!( "Invalid values for parameter 'key_ops' in JWK object: '{:?}'", jwk.common.key_operations ); return Err(Error::InvalidAuth); // Return opaque error } } // Return verification configuration if a decoding key can be retrieved from the JWK object match DecodingKey::from_jwk(&jwk) { Ok(dec) => Ok((dec, Validation::new(alg))), Err(err) => { warn!("Failed to retrieve decoding key from JWK object: '{}'", err); Err(Error::InvalidAuth) // Return opaque error } } } // Checks if network access to a remote location is allowed by the datastore capabilities // Attempts to find a relevant JWK object inside a JWKS object fetched from the remote location async fn find_jwk_from_url(kvs: &Datastore, url: &str, kid: &str) -> Result { // Check that the datastore capabilities allow connections to the URL host if let Err(err) = check_capabilities_url(kvs, url) { warn!("Network access to JWKS location is not allowed: '{}'", err); return Err(Error::InvalidAuth); // Return opaque error } // Retrieve JWKS cache let cache = kvs.jwks_cache(); // Attempt to fetch JWKS object from remote location match fetch_jwks_from_url(cache, url).await { Ok(jwks) => { trace!("Successfully fetched JWKS object from remote location"); // Attempt to find JWK in JWKS by the key identifier match jwks.find(kid) { Some(jwk) => Ok(jwk.to_owned()), _ => { debug!("Failed to find JWK object with key identifier '{kid}' in remote JWKS object"); Err(Error::InvalidAuth) // Return opaque error } } } Err(err) => { warn!("Failed to fetch JWKS object from remote location: '{}'", err); Err(Error::InvalidAuth) // Return opaque error } } } // Returns an error if network access to the address from a given URL string is not allowed fn check_capabilities_url(kvs: &Datastore, url: &str) -> Result<(), Error> { let url_parsed = match Url::parse(url) { Ok(url) => url, Err(_) => { return Err(Error::InvalidUrl(url.to_string())); } }; let addr = match url_parsed.host_str() { Some(host) => { if let Some(port) = url_parsed.port() { format!("{host}:{port}") } else { host.to_string() } } None => { return Err(Error::InvalidUrl(url.to_string())); } }; let target = match NetTarget::from_str(&addr) { Ok(host) => host, Err(_) => { return Err(Error::InvalidUrl(url.to_string())); } }; if !kvs.allows_network_target(&target) { return Err(Error::InvalidUrl(url.to_string())); } Ok(()) } // Attempts to fetch a JWKS object from a remote location and stores it in the cache if successful async fn fetch_jwks_from_url(cache: &Arc>, url: &str) -> Result { let client = Client::new(); #[cfg(not(target_arch = "wasm32"))] let res = client.get(url).timeout((*REMOTE_TIMEOUT).to_std().unwrap()).send().await?; #[cfg(target_arch = "wasm32")] let res = client.get(url).send().await?; if !res.status().is_success() { warn!("Unsuccessful HTTP status code received when fetching JWKS object from remote location: '{:?}'", res.status()); return Err(Error::InvalidAuth); // Return opaque error } let jwks = res.bytes().await?; match serde_json::from_slice::(&jwks) { Ok(jwks) => { // If successful, cache the JWKS object by its URL match store_jwks_in_cache(cache, jwks.clone(), url).await { None => trace!("Successfully added JWKS object to local cache"), Some(_) => trace!("Successfully updated JWKS object in local cache"), }; Ok(jwks) } Err(err) => { warn!("Failed to parse malformed JWKS object: '{}'", err); Err(Error::InvalidAuth) // Return opaque error } } } // Attempts to fetch a JWKS object from the local cache async fn fetch_jwks_from_cache( cache: &Arc>, url: &str, ) -> Option { let path = cache_key_from_url(url); let cache = cache.read().await; cache.get(&path).cloned() } // Attempts to store a JWKS object in the local cache async fn store_jwks_in_cache( cache: &Arc>, jwks: JwkSet, url: &str, ) -> Option { let entry = JwksCacheEntry { jwks, time: Utc::now(), }; let path = cache_key_from_url(url); let mut cache = cache.write().await; cache.insert(path, entry) } // Generates a unique cache key for a given URL string fn cache_key_from_url(url: &str) -> String { let mut hasher = Sha256::new(); hasher.update(url); let result = hasher.finalize(); format!("{:x}", result) } #[cfg(test)] mod tests { use super::*; use crate::dbs::capabilities::{Capabilities, NetTarget, Targets}; use rand::{distributions::Alphanumeric, Rng}; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; // Use unique path to prevent accidental cache reuse fn random_path() -> String { let rng = rand::thread_rng(); rng.sample_iter(&Alphanumeric).take(8).map(char::from).collect() } static DEFAULT_JWKS: Lazy = Lazy::new(|| { JwkSet{ keys: vec![Jwk{ common: jsonwebtoken::jwk::CommonParameters { public_key_use: Some(jsonwebtoken::jwk::PublicKeyUse::Signature), key_operations: None, algorithm: Some(jsonwebtoken::Algorithm::RS256), key_id: Some("test_1".to_string()), x509_url: None, x509_chain: Some(vec![ "MIIDBTCCAe2gAwIBAgIJdeyDfUXHLwX+MA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNVBAMTFWdlcmF1c2VyLmV1LmF1dGgwLmNvbTAeFw0yMzEwMzExNzI5MDBaFw0zNzA3MDkxNzI5MDBaMCAxHjAcBgNVBAMTFWdlcmF1c2VyLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANp7Er60Z7sOjiyQqpbZIkQBldO/t+YrHT8Mk661kNz8MRGXPpQ26rkO2fRZMeWPpXVcwW0xxLG5oQ2Iwbm1JUuHYbLb6WA/d/et0sOkOoEI6MP0+MVqrGnro+D6XGoz4yP8m2w8C2u2yFxAc+wAt1AIMWNJIYhEX6tqrliGnitDCye2wXKchhe4WctUlHoUNfO/sgazPQ7ItqisUF/fNSRbHLRJyS2mm76FlDELDLnEyVwUaeV/2xie9F44AfOQzVk1asO18BH3v6YjOQ3L41XEfOm2DMPkLOOmtyM7yA7OeF/fvn6zN+SByza6cFW37IOKoJsmvxkzxeDUlWm9MWkCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUOeOmT9I3/MJ/zI/lS74gPQmAQfEwDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQBDue8iM90XJcLORvr6e+h15f5tlvVjZ/cAzv09487QSHJtUd6qwTlunEABS5818TgMEMFDRafQ7CDX3KaAAXFpo2bnyWY9c+Ozp0PWtp8ChunOs94ayaG+viO0AiTrIY28cc26ehNBZ/4gC4/1k0IlXEk8rd1e03hQuhNiy7SQaxS2f1xkJfR4vCeF8HTN5omjKvIMcWRqkkwFZm4gdgkMfi2lNsV8V6+HXyTc3XUIdcwOUcC+Ms/m+vKxnyxw0UGh0RliB1qBc0ADg83hOsXEqZjneHh1ZhqqVF4IkKSJTfK5ofcc14GqvpLjjTR3s2eX6zxdujzwf4gnHdxjVvdJ".to_string(), ]), x509_sha1_fingerprint: None, x509_sha256_fingerprint: None, }, algorithm: jsonwebtoken::jwk::AlgorithmParameters::RSA( jsonwebtoken::jwk::RSAKeyParameters{ key_type: jsonwebtoken::jwk::RSAKeyType::RSA, n: "2nsSvrRnuw6OLJCqltkiRAGV07-35isdPwyTrrWQ3PwxEZc-lDbquQ7Z9Fkx5Y-ldVzBbTHEsbmhDYjBubUlS4dhstvpYD93963Sw6Q6gQjow_T4xWqsaeuj4PpcajPjI_ybbDwLa7bIXEBz7AC3UAgxY0khiERfq2quWIaeK0MLJ7bBcpyGF7hZy1SUehQ187-yBrM9Dsi2qKxQX981JFsctEnJLaabvoWUMQsMucTJXBRp5X_bGJ70XjgB85DNWTVqw7XwEfe_piM5DcvjVcR86bYMw-Qs46a3IzvIDs54X9--frM35IHLNrpwVbfsg4qgmya_GTPF4NSVab0xaQ".to_string(), e: "AQAB".to_string(), } ), }, Jwk{ common: jsonwebtoken::jwk::CommonParameters { public_key_use: Some(jsonwebtoken::jwk::PublicKeyUse::Signature), key_operations: None, algorithm: Some(jsonwebtoken::Algorithm::RS256), key_id: Some("test_2".to_string()), x509_url: None, x509_chain: Some(vec![ "MIIDBTCCAe2gAwIBAgIJUzJ062XCgOVQMA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNVBAMTFWdlcmF1c2VyLmV1LmF1dGgwLmNvbTAeFw0yMzEwMzExNzI5MDBaFw0zNzA3MDkxNzI5MDBaMCAxHjAcBgNVBAMTFWdlcmF1c2VyLmV1LmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7TgjrojPySPO5kPBOiAgiS0guSpYkfb0AtLNVyVeANe5vgjJoBQDe7vAGm68SHft841GQBPp5KWpxDTO+liECVAnbdR9YHwuZWOGuPRVVwNqtVmS8A75YG/mTWGV4tr2h+dLjjV3jvV0hvXRJwVFShlUS9+BqgevFBoF6zxi5AHIx/k1tCg1y2fhSlzYUHxEiFRgx0RhtJfizyv9QHoLSY3RFI4QOAkPtYwN5C1X69nEHPK0Q+W+POkeV7wuMQZWTRRT+xZuYn+JIYQCQviZ52FoJsrTzOEO5jlmrUa9PMEJpn0Aw68OdyLHjQPsip8B2JSegoVP1LTc0tDoqVGqUCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUYp67WM42b2pqF7ES0LsFvAI/Qy8wDgYDVR0PAQH/BAQDAgKEMA0GCSqGSIb3DQEBCwUAA4IBAQANOhmYz0jxNJG6pZ0klNtH00E6SoEsM/MNYH+atraTVZNeqPLAZH9514gMcqdu7+rBfQ/pRjpQG1YbkdZGQBaq5cZNlE6hNCT4BSgKddBYsN0WbfTGxstDVdoXLySgGCYpyKO6Hek4ULxwAf1LmMyOYpn4JrECy4mYShsCcfe504qfzUTd7pz1VaZ4minclOhz0dZbgYa+slUepe0C2+w+T3US138x0lPB9C266SLDakb6n/JTum+Czn2xlFBf4K4w6eWuSknvTlRrqTGE8RX3vzOiKTM3hpDdjU7Tu7eNsZpLkDR1e+w33m5NMi9iYgJcyTGsIeeHr0xjrRPD9Dwh".to_string(), ]), x509_sha1_fingerprint: None, x509_sha256_fingerprint: None, }, algorithm: jsonwebtoken::jwk::AlgorithmParameters::RSA( jsonwebtoken::jwk::RSAKeyParameters{ key_type: jsonwebtoken::jwk::RSAKeyType::RSA, n: "vtOCOuiM_JI87mQ8E6ICCJLSC5KliR9vQC0s1XJV4A17m-CMmgFAN7u8AabrxId-3zjUZAE-nkpanENM76WIQJUCdt1H1gfC5lY4a49FVXA2q1WZLwDvlgb-ZNYZXi2vaH50uONXeO9XSG9dEnBUVKGVRL34GqB68UGgXrPGLkAcjH-TW0KDXLZ-FKXNhQfESIVGDHRGG0l-LPK_1AegtJjdEUjhA4CQ-1jA3kLVfr2cQc8rRD5b486R5XvC4xBlZNFFP7Fm5if4khhAJC-JnnYWgmytPM4Q7mOWatRr08wQmmfQDDrw53IseNA-yKnwHYlJ6ChU_UtNzS0OipUapQ".to_string(), e: "AQAB".to_string(), } ), } ], } }); #[tokio::test] async fn test_golden_path() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); // Get first token configuration from remote location let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_ok(), "Failed to validate token the first time: {:?}", res.err()); // Drop server to force usage of the local cache drop(mock_server); // Get second token configuration from local cache let res = config( &ds, "test_2", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_ok(), "Failed to validate token the second time: {:?}", res.err()); } #[tokio::test] async fn test_capabilities_default() { let ds = Datastore::new("memory").await.unwrap().with_capabilities(Capabilities::default()); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .expect(0) .mount(&mock_server) .await; let url = mock_server.uri(); // Get token configuration from unallowed remote location let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_err(), "Unexpected success validating token from unallowed remote location"); } #[tokio::test] async fn test_capabilities_specific_port() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1:443").unwrap()].into(), // Different port from server )), ); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .expect(0) .mount(&mock_server) .await; let url = mock_server.uri(); // Get token configuration from unallowed remote location let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_err(), "Unexpected success validating token from unallowed remote location"); } #[tokio::test] async fn test_cache_expiration() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .expect(2) .mount(&mock_server) .await; let url = mock_server.uri(); // Get token configuration from remote location let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_ok(), "Failed to validate token the first time: {:?}", res.err()); // Wait for cache to expire std::thread::sleep((*CACHE_EXPIRATION + Duration::seconds(1)).to_std().unwrap()); // Get same token configuration again after cache has expired let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_ok(), "Failed to validate token the second time: {:?}", res.err()); // The server will panic if it does not receive exactly two expected requests } #[tokio::test] async fn test_cache_cooldown() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .expect(1) .mount(&mock_server) .await; let url = mock_server.uri(); // Use token with invalid key identifier claim to force cache refresh let res = config( &ds, "invalid", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_err(), "Unexpected success validating token with invalid key identifier"); // Use token with invalid key identifier claim to force cache refresh again before cooldown let res = config( &ds, "invalid", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_err(), "Unexpected success validating token with invalid key identifier"); // The server will panic if it receives more than the single expected request } #[tokio::test] async fn test_cache_expiration_remote_down() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .up_to_n_times(1) // Only respond the first time .mount(&mock_server) .await; let url = mock_server.uri(); // Get token configuration from remote location let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!(res.is_ok(), "Failed to validate token the first time: {:?}", res.err()); // Wait for cache to expire std::thread::sleep((*CACHE_EXPIRATION + Duration::seconds(1)).to_std().unwrap()); // Get same token configuration again after cache has expired let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_err(), "Unexpected success validating token with an expired cache and remote down" ); } #[tokio::test] async fn test_no_algorithm() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let mut jwks = DEFAULT_JWKS.clone(); jwks.keys[0].common.algorithm = None; let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_ok(), "Failed to validate token with key that does not specify algorithm: {:?}", res.err() ); } #[tokio::test] // An attacker can issue token indicating that it has been signed with an HMAC algorithm // If the original issuer was trusted using RSA, this may allow the attacker to sign the token with the public key // This test verifies that SurrealDB will not trust a token specifying an algorithm that does not match the key type // Reference: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/#RSA-or-HMAC async fn test_no_algorithm_invalid() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let mut jwks = DEFAULT_JWKS.clone(); jwks.keys[0].common.algorithm = None; let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), // The token is signed using HMAC jsonwebtoken::Algorithm::HS256, ) .await; assert!( res.is_err(), "Unexpected success validating token signed with algorithm that does not match the defined key type" ); } #[tokio::test] async fn test_no_key_use() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let mut jwks = DEFAULT_JWKS.clone(); jwks.keys[0].common.public_key_use = None; let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_ok(), "Failed to validate token with key that does not specify use: {:?}", res.err() ); } #[tokio::test] async fn test_key_use_enc() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let mut jwks = DEFAULT_JWKS.clone(); jwks.keys[0].common.public_key_use = Some(jsonwebtoken::jwk::PublicKeyUse::Encryption); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_err(), "Unexpected success validating token with key that only supports encryption" ); } #[tokio::test] async fn test_key_ops_encrypt_only() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let mut jwks = DEFAULT_JWKS.clone(); jwks.keys[0].common.key_operations = Some(vec![jsonwebtoken::jwk::KeyOperations::Encrypt]); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200).set_body_json(jwks); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_err(), "Unexpected success validating token with key that only supports encryption" ); } #[tokio::test] async fn test_remote_down() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(500); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .mount(&mock_server) .await; let url = mock_server.uri(); // Get token configuration from remote location responding with Internal Server Error let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_err(), "Unexpected success validating token configuration with unavailable remote location" ); } #[tokio::test] #[cfg(not(target_arch = "wasm32"))] async fn test_remote_timeout() { let ds = Datastore::new("memory").await.unwrap().with_capabilities( Capabilities::default().with_network_targets(Targets::::Some( [NetTarget::from_str("127.0.0.1").unwrap()].into(), )), ); let jwks = DEFAULT_JWKS.clone(); let jwks_path = format!("{}/jwks.json", random_path()); let mock_server = MockServer::start().await; let response = ResponseTemplate::new(200) .set_body_json(jwks) .set_delay((*REMOTE_TIMEOUT + Duration::seconds(10)).to_std().unwrap()); Mock::given(method("GET")) .and(path(&jwks_path)) .respond_with(response) .expect(1) .mount(&mock_server) .await; let url = mock_server.uri(); let start_time = Utc::now(); // Get token configuration from remote location responding very slowly let res = config( &ds, "test_1", &format!("{}/{}", &url, &jwks_path), jsonwebtoken::Algorithm::RS256, ) .await; assert!( res.is_err(), "Unexpected success validating token configuration with unavailable remote location" ); assert!( Utc::now() - start_time < *REMOTE_TIMEOUT + Duration::seconds(1), "Remote request was not aborted immediately after timeout" ); } }