Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
543 changes: 166 additions & 377 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ tower-http = { version = "0.6.8", features = ["full"] }
tower-layer = "0.3.3"
tower = "0.5.3"
http = "1.4.0"
axum-jwt-auth = "0.6.3"
futures = { version = "0.3", features = ["std", "alloc"] }
jsonwebtoken = "10.3.0"
jsonwebtoken = {version= "10.3.0", features=["rust_crypto"]}
chrono = "0.4.44"
redis = { version = "1.2.1", features = [
"default",
Expand All @@ -59,7 +58,6 @@ redis = { version = "1.2.1", features = [
] }
clap = { version = "4.5.60", features = ["derive", "env"] }
thiserror = "2.0.18"
openid = "0.23.0"
url = "2.5.8"
rmp-serde = "1.3.1"
async-trait = "0.1.89"
Expand Down
1 change: 0 additions & 1 deletion crates/contextforge-gateway-rs-apis/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::fs;

use contextforge_gateway_rs_apis::{User, user_store};

use schemars::SchemaGenerator;
use user_store::UserConfig;
#[allow(clippy::print_stdout)]
Expand Down
4 changes: 1 addition & 3 deletions crates/contextforge-gateway-rs-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ tower-http.workspace = true
tower-layer.workspace = true
tower.workspace = true
http.workspace = true
axum-jwt-auth.workspace = true
futures.workspace = true
jsonwebtoken.workspace = true
chrono.workspace = true
redis.workspace = true
clap.workspace = true
thiserror.workspace = true
openid.workspace = true
openidconnect = "4.0.1"
Comment thread
dawid-nowak marked this conversation as resolved.
url.workspace = true
rmp-serde.workspace = true
async-trait.workspace = true
Expand All @@ -47,7 +46,6 @@ hyper-util = "0.1.20"
hyper = { version = "1.4.0" }
rustls.workspace = true
rustls-pki-types = { version = "1.14.1", features = ["std","alloc"] }

tokio-rustls = "0.26.4"
typed-builder.workspace = true

Expand Down
124 changes: 77 additions & 47 deletions crates/contextforge-gateway-rs-lib/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,94 @@ use std::{
sync::Arc,
};

use axum_jwt_auth::JwtDecoder;
use chrono::{Duration, Utc};
use chrono::Duration;
use clap::{Parser, ValueEnum};
use http::uri::Authority;
use openid::{CompactJson, CustomClaims, StandardClaims};
use jsonwebtoken::DecodingKey;
use redis::{ConnectionAddr, IntoConnectionInfo, RedisError};

use rustls_pki_types::{CertificateDer, PrivatePkcs8KeyDer, pem::PemObject};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;
use typed_builder::TypedBuilder;
use uuid::Uuid;

use crate::{
const_values::{CONTEXT_FORGE_GATEWAY_AUDIENCE, CONTEXT_FORGE_GATEWAY_ISSUER},
user_config_store::UserConfigStore,
};

use crate::{const_values::CONEXT_FORGE_GATEWAY_AUDIENCE, user_config_store::UserConfigStore};
#[derive(Clone)]
pub struct JwtTokenDecoders {
pub rs: Option<DecodingKey>,
pub hmac_sha: Option<DecodingKey>,
}

#[derive(Clone)]
pub struct ContextForgeGatewayAppState {
pub(crate) jwt_token_decoder: Arc<dyn JwtDecoder<ContextForgeGatewayClaims> + Send + Sync>,
pub(crate) jwt_token_decoding_keys: JwtTokenDecoders,
pub(crate) config_store: Arc<dyn UserConfigStore + Send + Sync>,
pub(crate) config: Config,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ContextForgeGatewayClaims {
pub additional_claim: Option<String>,
#[serde(flatten)]
pub standard_claims: StandardClaims,
#[derive(Clone, Debug, Serialize, Deserialize, TypedBuilder)]
pub struct User {
email: String,
full_name: String,
is_admin: bool,
auth_provider: String,
}

impl CustomClaims for ContextForgeGatewayClaims {
fn standard_claims(&self) -> &StandardClaims {
&self.standard_claims
}
#[derive(Clone, Debug, Serialize, Deserialize, TypedBuilder)]
pub struct Scopes {
server_id: Option<String>,
permissions: Vec<String>,
ip_restrictions: Vec<String>,
time_restrictions: Option<serde_json::Value>,
}

impl CompactJson for ContextForgeGatewayClaims {}
#[derive(Clone, Debug, Serialize, Deserialize, TypedBuilder)]
pub struct ContextForgeClaims {
pub sub: String,
pub jti: String,
pub token_use: String,
pub iat: Option<u64>,
pub iss: String,
pub aud: String,
pub exp: u64,
pub teams: Option<Vec<String>>,
pub user: User,
pub scopes: Scopes,
Comment thread
dawid-nowak marked this conversation as resolved.
}

impl ContextForgeClaims {
pub fn new(user_id: &str) -> Self {
let audience = CONTEXT_FORGE_GATEWAY_AUDIENCE.to_owned();
let start = std::time::SystemTime::now();
let now = start.duration_since(std::time::UNIX_EPOCH).expect("Time went backwards").as_secs();
Self {
iss: CONTEXT_FORGE_GATEWAY_ISSUER.to_owned(),
sub: user_id.to_owned(),
aud: audience,
exp: now + Duration::hours(1).num_seconds().cast_unsigned(),
iat: Some(now),
jti: Uuid::new_v4().to_string(),
token_use: "api".to_owned(),
teams: Some(vec!["team_awesome".to_owned()]),
user: User::builder()
.email(user_id.to_owned())
.auth_provider("api_token".to_owned())
.full_name("API Token User".to_owned())
.is_admin(true)
.build(),
scopes: Scopes::builder()
.server_id(Some("my_id".to_owned()))
.ip_restrictions(vec!["192.169.1.0/24".to_owned()])
.permissions(vec!["tools.read".to_owned(), "servers.use".to_owned()])
.time_restrictions(None)
.build(),
}
}
}

pub type RedisClient = redis::Client;

Expand Down Expand Up @@ -110,12 +163,15 @@ pub struct Config {
pub address: Option<SocketAddr>,

#[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PUBLIC_KEY")]
pub token_verification_public_key: PathBuf,
pub token_verification_public_key: Option<PathBuf>,

#[cfg(feature = "with_tools")]
#[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TOKEN_VERIFICATION_PRIVATE_KEY")]
pub token_verification_private_key: PathBuf,

#[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_TOKEN_SECRET")]
pub token_verification_secret: Option<String>,

#[arg(long, env = "CONTEXTFORGE_GATEWAY_RS_ENABLE_OPEN_TELEMETRY")]
pub enable_open_telemetry: Option<bool>,

Expand Down Expand Up @@ -301,38 +357,12 @@ impl TryFrom<&Config> for reqwest::Client {
fn extract_identity(config: &Config) -> crate::Result<reqwest::Identity> {
match (config.upstream_private_key.as_ref(), config.upstream_certificate.as_ref()) {
(Some(private_key), Some(certificate)) => {
let cert = fs::read(certificate)?;
let mut cert = fs::read(certificate)?;
let key = fs::read(private_key)?;
Ok(reqwest::Identity::from_pkcs8_pem(&cert, &key)?)
cert.extend(key);
Ok(reqwest::Identity::from_pem(&cert)?)
},

_ => Err("Invalid/missing configuration".into()),
}
}

#[derive(Deserialize, Serialize)]
pub struct DefaultClaims {
iss: Url,
sub: String,
aud: String,
exp: i64,
iat: Option<i64>,
userinfo: openid::Userinfo,
}

impl DefaultClaims {
pub fn new(user_id: String) -> Self {
let url = "http://contextforge-gateway-rs".parse().expect("Expecting this to work");
let audience = CONEXT_FORGE_GATEWAY_AUDIENCE.to_owned();
let user_info = openid::Userinfo { sub: user_id.clone(), ..Default::default() };
Self {
iss: url,
sub: user_id,
aud: audience,
exp: (Utc::now() + Duration::hours(1)).timestamp(),
iat: Some(Utc::now().timestamp()),

userinfo: user_info,
}
}
}
4 changes: 3 additions & 1 deletion crates/contextforge-gateway-rs-lib/src/const_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ use std::time::Duration;

pub const LRU_CACHE_ENTRIES: usize = 50_000;
pub const LRU_CACHE_EXPIRY_DURATION: Duration = Duration::from_hours(1);
pub const CONEXT_FORGE_GATEWAY_AUDIENCE: &str = "mcp-audience";
pub const CONTEXT_FORGE_GATEWAY_AUDIENCE: &str = "mcpgateway-api";
pub const CONTEXT_FORGE_GATEWAY_ISSUER: &str = "mcpgateway";
pub const MCP_SESSION_ID: &str = "mcp-session-id";
pub const REDIS_RETRIES: usize = 1000; // keep re-trying forver
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tokio::sync::Mutex;
use super::{SessionMapping, SessionStoreError, UserSession, UserSessionStore};
use crate::{
common::RedisClient,
const_values::{LRU_CACHE_ENTRIES, LRU_CACHE_EXPIRY_DURATION},
const_values::{LRU_CACHE_ENTRIES, LRU_CACHE_EXPIRY_DURATION, REDIS_RETRIES},
};

#[derive(Clone)]
Expand All @@ -30,7 +30,9 @@ impl RedisUserSessionStore {
LRU_CACHE_ENTRIES,
))),
connection: redis_client
.get_connection_manager_with_config(ConnectionManagerConfig::default())
.get_connection_manager_with_config(
ConnectionManagerConfig::default().set_number_of_retries(REDIS_RETRIES),
)
.await
.map_err(|_| SessionStoreError::InvalidConnection)?,
})
Expand Down
Loading