feat: JWT token generation API + auto-inject TORI_JWT in executor
- POST /tori/api/token — sign ES256 JWT with configurable private key - exec.rs auto-generates and injects TORI_JWT env var for all commands - Config: jwt_private_key field for PEM file path
This commit is contained in:
100
Cargo.lock
generated
100
Cargo.lock
generated
@@ -279,6 +279,15 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -899,6 +908,21 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "9.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"js-sys",
|
||||||
|
"pem",
|
||||||
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -1054,6 +1078,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -1070,6 +1104,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.46"
|
version = "0.1.46"
|
||||||
@@ -1135,6 +1175,16 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem"
|
||||||
|
version = "3.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pem-rfc7468"
|
name = "pem-rfc7468"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -1198,6 +1248,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -1685,6 +1741,18 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_asn1"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -2000,6 +2068,37 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -2107,6 +2206,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
|
"jsonwebtoken",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"nix",
|
"nix",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
|
|||||||
@@ -28,3 +28,4 @@ mime_guess = "2"
|
|||||||
tokio-util = { version = "0.7", features = ["io"] }
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
nix = { version = "0.29", features = ["signal"] }
|
nix = { version = "0.29", features = ["signal"] }
|
||||||
pulldown-cmark = "0.12"
|
pulldown-cmark = "0.12"
|
||||||
|
jsonwebtoken = "9"
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ COPY --from=frontend /app/web/dist ./web/dist/
|
|||||||
COPY scripts/embed.py ./scripts/
|
COPY scripts/embed.py ./scripts/
|
||||||
COPY app-templates/ ./templates/
|
COPY app-templates/ ./templates/
|
||||||
COPY config.yaml .
|
COPY config.yaml .
|
||||||
|
COPY data/jwt-private.pem ./data/
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["./tori"]
|
CMD ["./tori"]
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ pub struct AgentManager {
|
|||||||
llm_config: LlmConfig,
|
llm_config: LlmConfig,
|
||||||
template_repo: Option<crate::TemplateRepoConfig>,
|
template_repo: Option<crate::TemplateRepoConfig>,
|
||||||
kb: Option<Arc<crate::kb::KbManager>>,
|
kb: Option<Arc<crate::kb::KbManager>>,
|
||||||
|
jwt_private_key_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentManager {
|
impl AgentManager {
|
||||||
@@ -87,6 +88,7 @@ impl AgentManager {
|
|||||||
llm_config: LlmConfig,
|
llm_config: LlmConfig,
|
||||||
template_repo: Option<crate::TemplateRepoConfig>,
|
template_repo: Option<crate::TemplateRepoConfig>,
|
||||||
kb: Option<Arc<crate::kb::KbManager>>,
|
kb: Option<Arc<crate::kb::KbManager>>,
|
||||||
|
jwt_private_key_path: Option<String>,
|
||||||
) -> Arc<Self> {
|
) -> Arc<Self> {
|
||||||
Arc::new(Self {
|
Arc::new(Self {
|
||||||
agents: RwLock::new(HashMap::new()),
|
agents: RwLock::new(HashMap::new()),
|
||||||
@@ -97,6 +99,7 @@ impl AgentManager {
|
|||||||
llm_config,
|
llm_config,
|
||||||
template_repo,
|
template_repo,
|
||||||
kb,
|
kb,
|
||||||
|
jwt_private_key_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +180,7 @@ async fn agent_loop(
|
|||||||
let pool = mgr.pool.clone();
|
let pool = mgr.pool.clone();
|
||||||
let llm_config = mgr.llm_config.clone();
|
let llm_config = mgr.llm_config.clone();
|
||||||
let llm = LlmClient::new(&llm_config);
|
let llm = LlmClient::new(&llm_config);
|
||||||
let exec = LocalExecutor::new();
|
let exec = LocalExecutor::new(mgr.jwt_private_key_path.clone());
|
||||||
let workdir = format!("/app/data/workspaces/{}", project_id);
|
let workdir = format!("/app/data/workspaces/{}", project_id);
|
||||||
|
|
||||||
tracing::info!("Agent loop started for project {}", project_id);
|
tracing::info!("Agent loop started for project {}", project_id);
|
||||||
|
|||||||
95
src/api/auth.rs
Normal file
95
src/api/auth.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::post,
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use jsonwebtoken::{encode, EncodingKey, Header, Algorithm};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct TokenResponse {
|
||||||
|
token: String,
|
||||||
|
expires_in: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TokenRequest {
|
||||||
|
/// Subject claim (e.g. "oseng", "tori-agent")
|
||||||
|
#[serde(default = "default_sub")]
|
||||||
|
sub: String,
|
||||||
|
/// Token validity in seconds (default: 300)
|
||||||
|
#[serde(default = "default_ttl")]
|
||||||
|
ttl_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_sub() -> String {
|
||||||
|
"oseng".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_ttl() -> u64 {
|
||||||
|
300
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Claims {
|
||||||
|
sub: String,
|
||||||
|
iat: usize,
|
||||||
|
exp: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn generate_token(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(body): Json<TokenRequest>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let privkey_pem = match &state.config.jwt_private_key {
|
||||||
|
Some(path) => match std::fs::read_to_string(path) {
|
||||||
|
Ok(pem) => pem,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to read JWT private key {}: {}", path, e);
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, "JWT private key not readable").into_response();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
return (StatusCode::SERVICE_UNAVAILABLE, "JWT not configured (jwt_private_key missing)").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = match EncodingKey::from_ec_pem(privkey_pem.as_bytes()) {
|
||||||
|
Ok(k) => k,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Invalid EC private key: {}", e);
|
||||||
|
return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid JWT private key").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let now = chrono::Utc::now().timestamp() as usize;
|
||||||
|
let claims = Claims {
|
||||||
|
sub: body.sub,
|
||||||
|
iat: now,
|
||||||
|
exp: now + body.ttl_secs as usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
let header = Header::new(Algorithm::ES256);
|
||||||
|
match encode(&header, &claims, &key) {
|
||||||
|
Ok(token) => Json(TokenResponse {
|
||||||
|
token,
|
||||||
|
expires_in: body.ttl_secs,
|
||||||
|
}).into_response(),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to encode JWT: {}", e);
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to generate token").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router(state: Arc<AppState>) -> Router {
|
||||||
|
Router::new()
|
||||||
|
.route("/token", post(generate_token))
|
||||||
|
.with_state(state)
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod auth;
|
||||||
mod chat;
|
mod chat;
|
||||||
mod kb;
|
mod kb;
|
||||||
pub mod obj;
|
pub mod obj;
|
||||||
@@ -33,6 +34,7 @@ pub fn router(state: Arc<AppState>) -> Router {
|
|||||||
.merge(kb::router(state.clone()))
|
.merge(kb::router(state.clone()))
|
||||||
.merge(settings::router(state.clone()))
|
.merge(settings::router(state.clone()))
|
||||||
.merge(chat::router(state.clone()))
|
.merge(chat::router(state.clone()))
|
||||||
|
.merge(auth::router(state.clone()))
|
||||||
.route("/projects/{id}/files/{*path}", get(serve_project_file))
|
.route("/projects/{id}/files/{*path}", get(serve_project_file))
|
||||||
.route("/projects/{id}/app/{*path}", any(proxy_to_service).with_state(state.clone()))
|
.route("/projects/{id}/app/{*path}", any(proxy_to_service).with_state(state.clone()))
|
||||||
.route("/projects/{id}/app/", any(proxy_to_service_root).with_state(state))
|
.route("/projects/{id}/app/", any(proxy_to_service_root).with_state(state))
|
||||||
|
|||||||
37
src/exec.rs
37
src/exec.rs
@@ -1,8 +1,10 @@
|
|||||||
pub struct LocalExecutor;
|
pub struct LocalExecutor {
|
||||||
|
jwt_private_key_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl LocalExecutor {
|
impl LocalExecutor {
|
||||||
pub fn new() -> Self {
|
pub fn new(jwt_private_key_path: Option<String>) -> Self {
|
||||||
Self
|
Self { jwt_private_key_path }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn execute(&self, command: &str, workdir: &str) -> anyhow::Result<ExecResult> {
|
pub async fn execute(&self, command: &str, workdir: &str) -> anyhow::Result<ExecResult> {
|
||||||
@@ -16,14 +18,19 @@ impl LocalExecutor {
|
|||||||
Err(_) => venv_bin,
|
Err(_) => venv_bin,
|
||||||
};
|
};
|
||||||
|
|
||||||
let output = tokio::process::Command::new("sh")
|
let mut cmd = tokio::process::Command::new("sh");
|
||||||
.arg("-c")
|
cmd.arg("-c")
|
||||||
.arg(command)
|
.arg(command)
|
||||||
.current_dir(workdir)
|
.current_dir(workdir)
|
||||||
.env("PATH", &path)
|
.env("PATH", &path)
|
||||||
.env("VIRTUAL_ENV", format!("{}/.venv", workdir))
|
.env("VIRTUAL_ENV", format!("{}/.venv", workdir));
|
||||||
.output()
|
|
||||||
.await?;
|
// Auto-inject GLIDE_JWT if a private key is configured
|
||||||
|
if let Some(token) = self.generate_jwt() {
|
||||||
|
cmd.env("TORI_JWT", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = cmd.output().await?;
|
||||||
|
|
||||||
Ok(ExecResult {
|
Ok(ExecResult {
|
||||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||||
@@ -31,6 +38,20 @@ impl LocalExecutor {
|
|||||||
exit_code: output.status.code().unwrap_or(-1),
|
exit_code: output.status.code().unwrap_or(-1),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_jwt(&self) -> Option<String> {
|
||||||
|
let path = self.jwt_private_key_path.as_ref()?;
|
||||||
|
let pem = std::fs::read_to_string(path).ok()?;
|
||||||
|
let key = jsonwebtoken::EncodingKey::from_ec_pem(pem.as_bytes()).ok()?;
|
||||||
|
let now = chrono::Utc::now().timestamp() as usize;
|
||||||
|
let claims = serde_json::json!({
|
||||||
|
"sub": "tori",
|
||||||
|
"iat": now,
|
||||||
|
"exp": now + 300,
|
||||||
|
});
|
||||||
|
let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::ES256);
|
||||||
|
jsonwebtoken::encode(&header, &claims, &key).ok()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExecResult {
|
pub struct ExecResult {
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub struct Config {
|
|||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub template_repo: Option<TemplateRepoConfig>,
|
pub template_repo: Option<TemplateRepoConfig>,
|
||||||
|
/// Path to EC private key PEM file for JWT signing
|
||||||
|
#[serde(default)]
|
||||||
|
pub jwt_private_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
@@ -104,6 +107,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
config.llm.clone(),
|
config.llm.clone(),
|
||||||
config.template_repo.clone(),
|
config.template_repo.clone(),
|
||||||
kb_arc.clone(),
|
kb_arc.clone(),
|
||||||
|
config.jwt_private_key.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
timer::start_timer_runner(database.pool.clone(), agent_mgr.clone());
|
timer::start_timer_runner(database.pool.clone(), agent_mgr.clone());
|
||||||
|
|||||||
Reference in New Issue
Block a user