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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ $ TOKEN=$(cat DEFAULT.token) \
```

You should see `X-Factor-Client-Id: DEFAULT` in the response from the server.
Note that in this case we are generating an oidc token an validating it
Note that in this case we are generating an oidc token and validating it
locally.

## Governance and Code of Conduct
Expand Down Expand Up @@ -171,3 +171,16 @@ the environment. Identity data looks like:
If a matching token is supplied, then the header `X-Factor-Client-Id` will be
set to the value of the matching client id. To reject, requests that don't
match, use the flag `--reject-unknown`

`factor issuer`

Prints out the issuer and subject for the application. If factor is already
running it will dynamically print out the current issuer, otherwise loads the
identity provider to determine the issuer.

The output will be something like:

```
issuer=http://localhost:5000
subject=local
```
12 changes: 7 additions & 5 deletions src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use log::{error, info, trace, warn};
pub use providers::*;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::{sync::watch, time::interval};
use tokio::{fs::write, sync::watch, time::interval};

use super::{dirs, env, server::Service};

Expand Down Expand Up @@ -65,7 +65,10 @@ pub trait IdentityProvider: Send + Sync {
async fn ensure_audience(&self, audience: &str) -> Result<()>;
async fn get_sub(&self) -> Result<String>;
async fn get_token(&self, audience: &str) -> Result<String>;
async fn get_iss_and_jwks(&self) -> Result<Option<(String, String)>>;
async fn get_iss(&self) -> Result<String>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a good separation.

async fn get_jwks(&self) -> Result<Option<String>> {
Ok(None)
}
}

pub struct IdentitySyncService {
Expand Down Expand Up @@ -113,9 +116,8 @@ impl IdentitySyncService {
}

async fn write_issuer(&self) -> Result<()> {
if let Some((issuer, _)) = self.provider.get_iss_and_jwks().await? {
env::set_var_file("ISSUER", &issuer, &self.issuer_path)?;
}
let issuer = self.provider.get_iss().await?;
write(&self.issuer_path, issuer.as_bytes()).await?;
Ok(())
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/identity/providers/auth0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,8 @@ struct Auth0Response {

#[async_trait]
impl IdentityProvider for Provider {
async fn get_iss_and_jwks(&self) -> Result<Option<(String, String)>> {
// No jwks management
Ok(None)
async fn get_iss(&self) -> Result<String> {
self.config.issuer.clone().context("Issuer not configured")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

async fn configure_app_identity(&self, name: &str) -> Result<ProviderConfig> {
Expand Down
5 changes: 2 additions & 3 deletions src/identity/providers/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ impl Provider {

#[async_trait]
impl IdentityProvider for Provider {
async fn get_iss_and_jwks(&self) -> Result<Option<(String, String)>> {
// No jwks management
Ok(None)
async fn get_iss(&self) -> Result<String> {
Ok("dummy-issuer".to_string())
}
async fn configure_app_identity(&self, name: &str) -> Result<ProviderConfig> {
let mut config = self.config.clone();
Expand Down
74 changes: 67 additions & 7 deletions src/identity/providers/k8s.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::sync::Arc;

use anyhow::{Context, Result};
use async_trait::async_trait;
use http::uri::Uri;
Expand All @@ -11,25 +13,26 @@ use kube::{
Client,
};
use log::trace;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use tokio::sync::OnceCell;
use tokio::sync::{OnceCell, RwLock};

use crate::identity::{IdentityProvider, ProviderConfig};

#[serde_as]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
#[serde_as(as = "Option<DisplayFromStr>")]
pub cluster_url: Option<Uri>,
pub namespace: Option<String>,
pub default_namespace: Option<String>,
pub service_account_name: Option<String>,
pub kubeconfig_path: Option<String>,
}

#[derive(Clone)]
pub struct Provider {
config: Config,
issuer: Arc<RwLock<Option<String>>>,
client: OnceCell<Client>,
}

Expand All @@ -41,6 +44,7 @@ impl Provider {
pub fn new(config: Config) -> Result<Self> {
Ok(Self {
config,
issuer: Arc::new(RwLock::new(None)),
client: OnceCell::new(),
})
}
Expand Down Expand Up @@ -87,13 +91,68 @@ impl Provider {
})
.await
}

/// # Errors
///
/// This method returns an [`anyhow::Error`] if:
///
/// - Configuration error in `self.config.kubeconfig_path`:
/// - the config file does not exist
/// - the config file is not valid YAML
/// - the config file is missing a context
/// - see [`kube::config::Kubeconfig::read_from`]
/// - see [`kube::config::Config::from_custom_kubeconfig`]
///
/// - The cluster URL cannot be inferred from the Kubernetes configuration.
///
/// - See [`kube::Config::infer`]
pub async fn get_cluster_url(&self) -> Result<Uri> {
if let Some(path) = &self.config.kubeconfig_path {
let kube_config = kube::config::Kubeconfig::read_from(path)
.context("Failed to read kubeconfig from path")?;
let client_config = kube::config::Config::from_custom_kubeconfig(
kube_config,
&KubeConfigOptions::default(),
)
.await
.context("Failed to create config from kubeconfig")?;
Ok(client_config.cluster_url)
} else {
let config = kube::Config::infer()
.await
.context("Failed to infer Kubernetes config")?;
Ok(config.cluster_url)
}
}
}

#[derive(Debug, Deserialize)]
struct OidcDiscovery {
issuer: String,
// Add other fields if needed
}

#[async_trait]
impl IdentityProvider for Provider {
async fn get_iss_and_jwks(&self) -> Result<Option<(String, String)>> {
// No jwks management
Ok(None)
async fn get_iss(&self) -> Result<String> {
if let Some(issuer) = &*self.issuer.read().await {
return Ok(issuer.clone());
}

let cluster_url = self.get_cluster_url().await?;

// Convert http::Uri to url::Url
let cluster_url =
Url::parse(&cluster_url.to_string()).context("Failed to parse cluster URL")?;

let discovery_url = cluster_url.join(".well-known/openid-configuration")?;
let response = reqwest::get(discovery_url).await?;
let oidc_discovery: OidcDiscovery = response.json().await?;

*self.issuer.write().await = Some(oidc_discovery.issuer.clone());

// Return the issuer URL
Ok(oidc_discovery.issuer)
}

async fn configure_app_identity(&self, name: &str) -> Result<ProviderConfig> {
Expand All @@ -120,6 +179,7 @@ impl IdentityProvider for Provider {

// Create new config with service account name
let mut config = self.config.clone();
config.default_namespace = Some(client.default_namespace().to_string());
config.service_account_name = Some(sa_name);

Ok(ProviderConfig::k8s(config))
Expand Down Expand Up @@ -171,7 +231,7 @@ impl IdentityProvider for Provider {
.context("Service account name not configured")?;
let namespace = self
.config
.namespace
.default_namespace
.as_ref()
.context("Namespace not configured")?;
Ok(format!("system:serviceaccount:{namespace}:{sa_name}"))
Expand Down
8 changes: 5 additions & 3 deletions src/identity/providers/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ struct Claims {

#[async_trait]
impl IdentityProvider for Provider {
async fn get_iss_and_jwks(&self) -> Result<Option<(String, String)>> {
async fn get_iss(&self) -> Result<String> {
// if iss is still an env var, expand it now
let iss = env::expand(&self.config.iss)?;
env::expand(&self.config.iss)
}
async fn get_jwks(&self) -> Result<Option<String>> {
let json = serde_json::to_string_pretty(&self.jwks)?;
Ok(Some((iss, json)))
Ok(Some(json))
}

async fn configure_app_identity(&self, name: &str) -> Result<ProviderConfig> {
Expand Down
7 changes: 2 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,11 +310,8 @@ fn handle_issuer(app_config: &AppConfig) -> Result<(), anyhow::Error> {
let issuer = if let Ok(iss) = identity::get_stored_issuer() {
iss
} else {
info!("Could not get stored issuer, falling back to generating a token");
// Generate a token with a dummy audience to extract issuer
let token = rt.block_on(provider.get_token("dummy-audience"))?;
let claims = identity::get_claims(&token)?;
claims.iss
info!("Could not get stored issuer, falling back to provider");
rt.block_on(provider.get_iss())?
};

// Get subject directly
Expand Down
12 changes: 4 additions & 8 deletions src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,8 @@ pub struct AuthProxy {
}

impl AuthProxy {
async fn handle_well_known(
&self,
session: &mut Session,
iss: String,
jwks: String,
) -> anyhow::Result<()> {
async fn handle_well_known(&self, session: &mut Session, jwks: String) -> anyhow::Result<()> {
let iss = self.provider.get_iss().await?;
let path = session.req_header().uri.path();
let resp_str = if path.starts_with("/.well-known/openid-configuration") {
// Generate the OIDC discovery response
Expand Down Expand Up @@ -180,8 +176,8 @@ impl ProxyHttp for AuthProxy {

async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
if session.req_header().uri.path().starts_with("/.well-known/") {
if let Ok(Some((iss, jwks))) = self.provider.get_iss_and_jwks().await {
if let Err(e) = self.handle_well_known(session, iss, jwks).await {
if let Ok(Some(jwks)) = self.provider.get_jwks().await {
if let Err(e) = self.handle_well_known(session, jwks).await {
info!("Failed to handle well_known {e:?}");
let _ = session.respond_error(500).await;
return Ok(true);
Expand Down
Loading