Skip to main content
Glama

Convex MCP server

Official
by get-convex
auth_config.rs15.8 kB
use std::{ collections::BTreeMap, sync::Arc, }; use anyhow::anyhow; use common::{ auth::{ AuthConfig, SerializedAuthConfig, }, knobs::{ DATABASE_UDF_SYSTEM_TIMEOUT, DATABASE_UDF_USER_TIMEOUT, }, log_lines::LogLevel, runtime::{ Runtime, UnixTimestamp, }, }; use deno_core::{ v8, ModuleSpecifier, }; use errors::ErrorMetadata; use model::{ config::types::AUTH_CONFIG_FILE_NAME, environment_variables::types::{ EnvVarName, EnvVarValue, }, modules::module_versions::{ FullModuleSource, ModuleSource, SourceMap, }, }; use rand_chacha::ChaCha12Rng; use regex::Regex; use serde_json::Value as JsonValue; use value::NamespacedTableMapping; use super::ModuleCodeCacheResult; use crate::{ concurrency_limiter::ConcurrencyPermit, environment::{ helpers::syscall_error::{ syscall_description_for_error, syscall_name_for_error, }, AsyncOpRequest, IsolateEnvironment, }, helpers, isolate::{ Isolate, CONVEX_SCHEME, }, request_scope::RequestScope, strings, timeout::Timeout, }; pub struct AuthConfigEnvironment { auth_config_bundle: ModuleSource, source_map: Option<SourceMap>, environment_variables: BTreeMap<EnvVarName, EnvVarValue>, } impl<RT: Runtime> IsolateEnvironment<RT> for AuthConfigEnvironment { fn trace(&mut self, _level: LogLevel, messages: Vec<String>) -> anyhow::Result<()> { tracing::warn!( "Unexpected Console access when evaluating auth config file: {}", messages.join(" ") ); Ok(()) } fn rng(&mut self) -> anyhow::Result<&mut ChaCha12Rng> { anyhow::bail!(ErrorMetadata::bad_request( "NoRandomDuringAuthConfig", "Math.random unsupported when evaluating auth config file" )) } fn crypto_rng(&mut self) -> anyhow::Result<super::crypto_rng::CryptoRng> { anyhow::bail!(ErrorMetadata::bad_request( "NoCryptoRngDuringAuthConfig", "Cannot use cryptographic randomness when evaluating auth config file" )) } fn unix_timestamp(&mut self) -> anyhow::Result<UnixTimestamp> { anyhow::bail!(ErrorMetadata::bad_request( "NoDateDuringAuthConfig", "Date unsupported when evaluating auth config file" )) } fn get_environment_variable( &mut self, name: EnvVarName, ) -> anyhow::Result<Option<EnvVarValue>> { self.environment_variables .get(&name) .cloned() .ok_or_else(|| { anyhow::anyhow!(ErrorMetadata::bad_request( // Special cased in Convex CLI!!! "AuthConfigMissingEnvironmentVariable", format!( "Environment variable {name} is used in auth config file but its value \ was not set" ), )) }) .map(Some) } fn get_all_table_mappings(&mut self) -> anyhow::Result<NamespacedTableMapping> { anyhow::bail!(ErrorMetadata::bad_request( "NoTableMappingFetchDuringAuthConfig", "Getting the table mapping unsupported when evaluating auth config file" )) } async fn lookup_source( &mut self, path: &str, _timeout: &mut Timeout<RT>, _permit: &mut Option<ConcurrencyPermit>, ) -> anyhow::Result<Option<(Arc<FullModuleSource>, ModuleCodeCacheResult)>> { if path != AUTH_CONFIG_FILE_NAME { anyhow::bail!(ErrorMetadata::bad_request( "NoImportModuleDuringAuthConfig", format!("Can't import {path} while evaluating auth config file") )) } Ok(Some(( Arc::new(FullModuleSource { source: self.auth_config_bundle.clone(), source_map: self.source_map.clone(), }), ModuleCodeCacheResult::noop(), ))) } fn syscall(&mut self, name: &str, _args: JsonValue) -> anyhow::Result<JsonValue> { anyhow::bail!(ErrorMetadata::bad_request( "NoSyscallDuringAuthConfig", format!("Syscall {name} unsupported when evaluating auth config file") )) } fn start_async_syscall( &mut self, name: String, _args: JsonValue, _resolver: v8::Global<v8::PromiseResolver>, ) -> anyhow::Result<()> { anyhow::bail!(ErrorMetadata::bad_request( format!("No{}DuringAuthConfig", syscall_name_for_error(&name)), format!( "{} unsupported while evaluating auth config file", syscall_description_for_error(&name), ), )) } fn start_async_op( &mut self, request: AsyncOpRequest, _resolver: v8::Global<v8::PromiseResolver>, ) -> anyhow::Result<()> { anyhow::bail!(ErrorMetadata::bad_request( format!("No{}DuringAuthConfig", request.name_for_error()), format!( "{} unsupported while evaluating auth config file", request.description_for_error() ), )) } fn user_timeout(&self) -> std::time::Duration { *DATABASE_UDF_USER_TIMEOUT } fn system_timeout(&self) -> std::time::Duration { *DATABASE_UDF_SYSTEM_TIMEOUT } } impl AuthConfigEnvironment { pub async fn evaluate_auth_config<RT: Runtime>( client_id: String, isolate: &mut Isolate<RT>, v8_context: v8::Global<v8::Context>, auth_config_bundle: ModuleSource, source_map: Option<SourceMap>, environment_variables: BTreeMap<EnvVarName, EnvVarValue>, ) -> anyhow::Result<AuthConfig> { let environment = Self { auth_config_bundle, source_map, environment_variables, }; let client_id = Arc::new(client_id); let (handle, state) = isolate.start_request(client_id, environment).await?; let mut handle_scope = isolate.handle_scope(); let v8_context = v8::Local::new(&mut handle_scope, v8_context); let mut context_scope = v8::ContextScope::new(&mut handle_scope, v8_context); let mut isolate_context = RequestScope::new(&mut context_scope, handle.clone(), state, false).await?; let handle = isolate_context.handle(); let result = Self::run_evaluate_auth_config(&mut isolate_context).await; // Drain the microtask queue, to clean up the isolate. isolate_context.checkpoint(); // Unlink the request from the isolate. // After this point, it's unsafe to run js code in the isolate that // expects the current request's environment. // If the microtask queue is somehow nonempty after this point but before // the next request starts, the isolate may panic. drop(isolate_context); handle.take_termination_error(None, "auth")??; result } async fn run_evaluate_auth_config<RT: Runtime>( isolate: &mut RequestScope<'_, '_, RT, Self>, ) -> anyhow::Result<AuthConfig> { let mut v8_scope = isolate.scope(); let mut scope = RequestScope::<RT, Self>::enter(&mut v8_scope); let auth_config_url = ModuleSpecifier::parse(&format!("{CONVEX_SCHEME}:/auth.config.js"))?; let module = scope.eval_module(&auth_config_url).await?; let namespace = module .get_module_namespace() .to_object(&mut scope) .ok_or_else(|| anyhow!("Module namespace wasn't an object?"))?; let default_str = strings::default.create(&mut scope)?; let config_val: v8::Local<v8::Value> = namespace .get(&mut scope, default_str.into()) .ok_or_else(missing_export_error)?; if config_val.is_null_or_undefined() { anyhow::bail!(missing_export_error()); } let config_str = json_stringify(&mut scope, config_val)?; // Custom errors for misconfigured `convex/auth.config.ts` files that // are helpful because we allow extra properties in the // authoritative deserialization. check_for_common_confusions(&config_str)?; let config: AuthConfig = serde_json::from_str::<SerializedAuthConfig>(&config_str) .map_err(|error| config_not_matching_schema_error(strip_position(&error.to_string())))? .try_into()?; Ok(config) } } fn check_for_common_confusions(config_str: &str) -> anyhow::Result<()> { let raw_config: JsonValue = serde_json::from_str(config_str) .map_err(|error| config_not_matching_schema_error(strip_position(&error.to_string())))?; if let JsonValue::Object(ref config_obj) = raw_config { if let Some(JsonValue::Array(providers)) = config_obj.get("providers") { for (index, config_obj) in providers.iter().enumerate() { if let JsonValue::Object(obj) = config_obj { let has_domain = obj.contains_key("domain"); let has_issuer = obj.contains_key("issuer"); let issuer = obj.get("issuer").and_then(|v| v.as_str()); let has_bad_application_id = obj.contains_key("applicationId") || obj.contains_key("applicationid"); let has_application_id = obj.contains_key("applicationID"); let type_value = obj .get("type") .and_then(|v| v.as_str()) .unwrap_or("unknown"); if has_bad_application_id { anyhow::bail!(config_not_matching_schema_error(format!( "Provider at index {index} must have applicationID property spelled \ lowercase 'application', capital I, capital D." ))); } if type_value != "customJwt" && type_value != "oidc" && type_value != "unknown" { anyhow::bail!(config_not_matching_schema_error(format!( "Provider at index {index} has unexpected 'type' value '{type_value}'" ))); } if type_value == "customJwt" && has_domain { anyhow::bail!(config_not_matching_schema_error(format!( "Provider at index {index} is a customJwt so cannot have a 'domain' \ specified", ))); } let is_oidc = type_value == "oidc" || type_value == "unknown"; if is_oidc && has_issuer { anyhow::bail!(config_not_matching_schema_error(format!( "Provider at index {index} is oidc so cannot have an 'issuer' \ specified.", ))); } if !has_application_id && (issuer == Some("https://api.workos.com/") || issuer == Some("https://api.workos.com")) { anyhow::bail!(ErrorMetadata::bad_request( "InsecureConfiguration", format!( "This auth configuration appears potentially insecure: Provider \ at index {index} has an issuer that is shared among many \ applications, so must to specify an ApplicationID to check \ against an `aud` field of a JWT.", ), )); } } } } } Ok(()) } // It's not meaningful for the user to see the serialized // position in the result of the auth config execution fn strip_position(error_message: &str) -> String { let re = Regex::new(r"at line \d+ column \d+$").unwrap(); re.replace(error_message, "").to_string() } fn json_stringify( scope: &mut v8::HandleScope, value: v8::Local<v8::Value>, ) -> anyhow::Result<String> { let json_stringify_code = strings::json_stringify.create(scope)?; let json_stringify_fn = v8::Script::compile(scope, json_stringify_code, None) .ok_or_else(|| anyhow!("Unexpected: Could not compile JSON.stringify"))? .run(scope) .ok_or_else(|| anyhow!("Unexpected: Could run compiled JSON.stringify"))?; let json_stringify_fn = v8::Local::<v8::Function>::try_from(json_stringify_fn).unwrap(); let result = json_stringify_fn .call(scope, value, &[value]) .ok_or_else(|| { ErrorMetadata::bad_request( "AuthConfigUnserializableError", format!("auth config file can only contain strings {SEE_AUTH_DOCS}"), ) })?; let result: v8::Local<v8::String> = result.try_into()?; helpers::to_rust_string(scope, &result) } const SEE_AUTH_DOCS: &str = "To learn more, see the auth documentation at https://docs.convex.dev/auth."; pub fn missing_export_error() -> ErrorMetadata { ErrorMetadata::bad_request( "AuthConfigMissingExportError", format!("auth config file is missing default export. {SEE_AUTH_DOCS}"), ) } pub fn config_not_matching_schema_error(error: String) -> ErrorMetadata { ErrorMetadata::bad_request( "AuthConfigNotMatchingSchemaError", format!("auth config file must include a list of provider credentials: {error}"), ) } #[cfg(test)] mod tests { use super::*; #[test] fn test_traditional_oidc() -> anyhow::Result<()> { // not "legacy" because we'll support it forever, the only legacy aspect is not // having a "type" field. let valid_config = r#"{"providers": [{"domain": "a", "applicationID": "b"}]}"#; check_for_common_confusions(valid_config)?; Ok(()) } #[test] fn test_newer_oidc() -> anyhow::Result<()> { let valid_config = r#"{"providers": [{"type": "oidc", "domain": "example.com", "applicationID": "c"}]}"#; check_for_common_confusions(valid_config)?; Ok(()) } #[test] fn test_misspell_application_id() -> anyhow::Result<()> { let invalid_config = r#"{"providers": [{"domain": "example.com", "applicationId": "b"}]}"#; let result = check_for_common_confusions(invalid_config); assert!(result.is_err()); let error_message = result.unwrap_err().to_string(); assert!(error_message.contains("applicationID")); assert!(error_message.contains("spelled lowercase 'application', capital I, capital D.")); Ok(()) } #[test] fn test_valid_custom_jwt() -> anyhow::Result<()> { let valid_config = r#"{ "providers": [ { "type": "customJwt", "applicationID": "your-application-id", "issuer": "https://your.issuer.url.com", "jwks": "https://your.issuer.url.com/.well-known/jwks.json", "algorithm": "RS256" }]}"#; check_for_common_confusions(valid_config)?; Ok(()) } #[test] fn test_valid_custom_jwt_without_application() -> anyhow::Result<()> { let valid_config = r#"{ "providers": [ { "type": "customJwt", "issuer": "https://your.issuer.url.com", "jwks": "https://your.issuer.url.com/.well-known/jwks.json", "algorithm": "RS256" }]}"#; check_for_common_confusions(valid_config)?; Ok(()) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server