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
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,17 @@ 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`
`factor info`

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.
Prints out info for the current application. If factor is already running it
will dynamically print out the current data, otherwise loads the identity
provider to determine the values.

The output will be something like:

```
issuer=http://localhost:5000
subject=local
name=local
url=http://localhost:5000
iss=http://localhost:5000
sub=local
```
3 changes: 2 additions & 1 deletion example.factor-app
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
app = "local"
path = "."
# if the app is behind a gateway, specify the url here
# url = "https://example.com"

[id]
name = "local"
provider = "local"
iss = "$NGROK_URL" # if not using ngrok, set issuer to public hostname of app
secret = "..." # base64 encoded bootstrap secret -> openssl rand -base64 32
sub = "local"

Expand Down
96 changes: 96 additions & 0 deletions src/dirs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::path::PathBuf;

use anyhow::Result;
use directories::ProjectDirs;
use tokio::fs::write;

const QUALIFIER: &str = "dev";
const ORGANIZATION: &str = "twelve-factor";
Expand Down Expand Up @@ -33,3 +34,98 @@ pub fn home_dir() -> Result<PathBuf> {
.ok_or_else(|| anyhow::anyhow!("Could not find home directory"))
.map(|dirs| dirs.home_dir().to_path_buf())
}

const URL_FILENAME: &str = "url";
const ISS_FILENAME: &str = "issuer";

/// Get the stored url from the data directory
///
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - Url file doesn't exist or can't be read
pub fn get_stored_url() -> Result<String> {
let url_path = get_data_dir()?.join(URL_FILENAME);
std::fs::read_to_string(&url_path)
.map_err(|e| anyhow::anyhow!("Failed to read url file: {}", e))
}

/// Write the url for the app to the data directory
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - `write` returns an error, which can happen if:
/// - The file cannot be created or opened.
/// - There is an error writing to the file.
/// - See [`tokio::fs::write`].
pub async fn write_url(url: String) -> Result<()> {
let url_path = get_data_dir()?.join(URL_FILENAME);
write(&url_path, url.as_bytes()).await?;
Ok(())
}

/// Delete the url file from the data directory
///
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - `remove_file` returns an error, which can happen if:
/// - The file doesn't exist.
/// - The user lacks permissions to remove the file.
/// - Some other I/O error occurred.
/// - See [`tokio::fs::remove_file`].
pub async fn delete_url() -> Result<()> {
let data_dir = get_data_dir()?;
let url_path = data_dir.join(URL_FILENAME);
tokio::fs::remove_file(&url_path).await?;
Ok(())
}

/// Get the stored iss from the data directory
///
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - Iss file doesn't exist or can't be read
pub fn get_stored_iss() -> Result<String> {
let iss_path = get_data_dir()?.join(ISS_FILENAME);
std::fs::read_to_string(&iss_path)
.map_err(|e| anyhow::anyhow!("Failed to read iss file: {}", e))
}

/// Write the iss for the app to the data directory
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - `write` returns an error, which can happen if:
/// - The file cannot be created or opened.
/// - There is an error writing to the file.
/// - See [`tokio::fs::write`].
pub async fn write_iss(iss: String) -> Result<()> {
let iss_path = get_data_dir()?.join(ISS_FILENAME);
write(&iss_path, iss.as_bytes()).await?;
Ok(())
}

/// Delete the iss file from the data directory
///
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - `remove_file` returns an error, which can happen if:
/// - The file doesn't exist.
/// - The user lacks permissions to remove the file.
/// - Some other I/O error occurred.
/// - See [`tokio::fs::remove_file`].
pub async fn delete_iss() -> Result<()> {
let data_dir = get_data_dir()?;
let iss_path = data_dir.join(ISS_FILENAME);
tokio::fs::remove_file(&iss_path).await?;
Ok(())
}
11 changes: 9 additions & 2 deletions src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use std::{
};

use anyhow::Result;
use log::error;
use notify::{Event, RecursiveMode, Watcher};
use reqwest;
use serde::de::DeserializeOwned;
Expand Down Expand Up @@ -51,7 +52,10 @@ where

let string_value = var_with_on_change(key, string_callback)?;

let value = serde_json::from_str(&string_value).map_err(|_| VarError::NotPresent)?;
let value = serde_json::from_str(&string_value).map_err(|e| {
error!("Failed to parse JSON: {}", e);
VarError::NotPresent
})?;
Ok(value)
}

Expand All @@ -68,7 +72,10 @@ where
T: DeserializeOwned + Send + Sync + 'static,
{
let string_value = var(key)?;
let value = serde_json::from_str(&string_value).map_err(|_| VarError::NotPresent)?;
let value = serde_json::from_str(&string_value).map_err(|e| {
error!("Failed to parse JSON: {}", e);
VarError::NotPresent
})?;
Ok(value)
}

Expand Down
26 changes: 4 additions & 22 deletions src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ use log::{error, info, trace, warn};
pub use providers::*;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tokio::{fs::write, sync::watch, time::interval};
use tokio::{sync::watch, time::interval};

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

const ISSUER_FILENAME: &str = "issuer";

macro_rules! identity_providers {
($($variant:ident),*) => {
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, strum_macros::Display)]
Expand Down Expand Up @@ -76,7 +74,6 @@ pub struct IdentitySyncService {
path: PathBuf,
audience: String,
provider: Arc<dyn IdentityProvider + Send + Sync>,
issuer_path: PathBuf,
}

impl IdentitySyncService {
Expand All @@ -101,7 +98,6 @@ impl IdentitySyncService {
let key = format!("{}_{}", target_id_safe, "IDENTITY");
let filename = format!("{target_id}.token");
let path = PathBuf::from(path).join(filename);
let issuer_path = dirs::get_data_dir()?.join(ISSUER_FILENAME);

// make sure that we empty any existing old identity
env::set_var_file(&key, "", &path)?;
Expand All @@ -110,15 +106,13 @@ impl IdentitySyncService {
path,
audience: audience.to_string(),
provider,
issuer_path,
};
Ok(service)
}

async fn write_issuer(&self) -> Result<()> {
let issuer = self.provider.get_iss().await?;
write(&self.issuer_path, issuer.as_bytes()).await?;
Ok(())
dirs::write_iss(issuer).await
}
}

Expand All @@ -142,8 +136,8 @@ impl Service for IdentitySyncService {
tokio::select! {
_ = shutdown.changed() => {
// Clean up issuer file on shutdown
if let Err(e) = std::fs::remove_file(&self.issuer_path) {
warn!("Failed to remove issuer file: {}", e);
if let Err(e) = dirs::delete_iss().await {
error!("Failed to delete issuer file: {}", e);
}
break;
}
Expand Down Expand Up @@ -225,15 +219,3 @@ pub fn get_claims(jwt: &str) -> Result<Claims> {

Ok(claims)
}

/// Get the stored issuer from the data directory
///
/// # Errors
///
/// Returns an error if:
/// - Cannot access data directory
/// - Issuer file doesn't exist or can't be read
pub fn get_stored_issuer() -> Result<String> {
let issuer_path = dirs::get_data_dir()?.join(ISSUER_FILENAME);
std::fs::read_to_string(&issuer_path).map_err(|e| anyhow!("Failed to read issuer file: {}", e))
}
14 changes: 5 additions & 9 deletions src/identity/providers/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

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

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub iss: String,
pub secret: String,
pub sub: Option<String>,
}
Expand Down Expand Up @@ -106,7 +105,8 @@ impl Provider {
/// would indicate internal key corruption)
/// - The PEM-encoded key cannot be converted to JWT format
pub fn new(config: Config) -> Result<Self> {
let private_key = generate_rsa_key_from_secret(&config.secret)?;
let private_key = generate_rsa_key_from_secret(&config.secret)
.map_err(|e| anyhow!("Invalid secret: {}", e))?;
let private_key_pem = private_key.to_pkcs1_pem(LineEnding::CR)?.to_string();
let key = EncodingKey::from_rsa_pem(private_key_pem.as_bytes())?;
let public_key = private_key.to_public_key();
Expand All @@ -133,8 +133,7 @@ struct Claims {
#[async_trait]
impl IdentityProvider for Provider {
async fn get_iss(&self) -> Result<String> {
// if iss is still an env var, expand it now
env::expand(&self.config.iss)
dirs::get_stored_url()
}
async fn get_jwks(&self) -> Result<Option<String>> {
let json = serde_json::to_string_pretty(&self.jwks)?;
Expand All @@ -156,11 +155,8 @@ impl IdentityProvider for Provider {
async fn get_token(&self, audience: &str) -> Result<String> {
let sub = self.config.sub.as_ref().context("Sub not configured")?;

// if iss is still an env var, expand it now
let iss = env::expand(&self.config.iss)?;

let claims = Claims {
iss,
iss: self.get_iss().await?,
sub: sub.to_string(),
aud: audience.to_string(),
exp: (SystemTime::now() + std::time::Duration::from_secs(1800)) // Expiration time (30 minutes from now)
Expand Down
Loading
Loading