use crate::error::Result;
use forklaunch_axum::module_auth::HmacJwksAuth;
use forklaunch_core::auth::jwt::{RemoteJwks, RemoteJwksConfig};
use forklaunch_iam_base::http::DirectoryAuthorizationMode;
use forklaunch_core::config;
use jsonwebtoken::{Algorithm, Validation};
#[derive(Clone)]
pub struct ServiceConfig {
pub host: String,
pub port: u16,
pub tenant_id: String,
pub directory_mode: DirectoryAuthorizationMode,
pub browser_origin: Option<String>,
pub oidc: Option<OidcConfig>,
pub hmac_secret: String,
pub jwks_url: String,
pub jwt_issuer: Option<String>,
pub jwt_audience: Option<String>,
pub cors_origins: Vec<String>,
}
#[derive(Clone)]
pub struct OidcConfig {
pub issuer: String,
pub client_id: String,
pub client_secret: Option<String>,
}
impl ServiceConfig {
pub fn from_env() -> Result<Self> {
let directory_mode: String = config::required("IAM_DIRECTORY_AUTHORIZATION")?;
let directory_mode = match directory_mode.as_str() {
"jwt-claims" => DirectoryAuthorizationMode::JwtClaims,
"stored-session" => DirectoryAuthorizationMode::StoredSessions,
_ => return Err("IAM_DIRECTORY_AUTHORIZATION must be jwt-claims or stored-session".into()),
};
let issuer: Option<String> = config::optional("SESSION_OIDC_ISSUER")?;
let client_id: Option<String> = config::optional("SESSION_OIDC_CLIENT_ID")?;
let client_secret: Option<String> = config::optional("SESSION_OIDC_CLIENT_SECRET")?;
let browser_origin: Option<String> = config::optional("SESSION_COOKIE_ORIGIN")?;
let oidc_enabled = issuer.is_some() || client_id.is_some() || client_secret.is_some();
if oidc_enabled && browser_origin.is_none() {
return Err("OIDC requires SESSION_COOKIE_ORIGIN".into());
}
let oidc = if oidc_enabled {
Some(OidcConfig {
issuer: issuer.ok_or("SESSION_OIDC_ISSUER is required with OIDC")?,
client_id: client_id.ok_or("SESSION_OIDC_CLIENT_ID is required with OIDC")?,
client_secret,
})
} else {
None
};
Ok(Self {
host: config::required("HOST")?,
port: config::required("PORT")?,
tenant_id: config::required("TENANT_ID")?,
directory_mode,
browser_origin,
oidc,
hmac_secret: config::required("HMAC_SECRET_KEY")?,
jwks_url: config::required("JWKS_PUBLIC_KEY_URL")?,
jwt_issuer: config::optional("JWT_ISSUER")?,
jwt_audience: config::optional("JWT_AUDIENCE")?,
cors_origins: config::optional::<String>("CORS_ORIGINS")?
.map(|value| value.split(',').map(str::trim).filter(|o| !o.is_empty()).map(str::to_owned).collect())
.unwrap_or_default(),
})
}
#[must_use]
pub fn bind_addr(&self) -> (&str, u16) {
(&self.host, self.port)
}
pub fn module_auth(&self) -> Result<HmacJwksAuth> {
let jwks = RemoteJwks::new(RemoteJwksConfig::new(self.jwks_url.clone()))?;
let mut validation = Validation::new(Algorithm::RS256);
validation.algorithms = vec![
Algorithm::RS256, Algorithm::RS384, Algorithm::RS512,
Algorithm::PS256, Algorithm::PS384, Algorithm::PS512,
Algorithm::ES256, Algorithm::ES384, Algorithm::EdDSA,
];
validation.validate_nbf = true;
validation.leeway = 0;
validation.validate_aud = false;
if let Some(issuer) = &self.jwt_issuer {
validation.set_issuer(&[issuer]);
}
if let Some(audience) = &self.jwt_audience {
validation.validate_aud = true;
validation.set_audience(&[audience]);
}
Ok(HmacJwksAuth::new(self.hmac_secret.clone().into_bytes(), jwks, validation)?)
}
}