use crate::{
cli::{
args::{AuthArgs, ProviderKind},
history::{HistoryKey, HistoryManager, file::FileHistoryManager},
},
config::cli_config::Profile,
services::authentication::urls::IdentityProvider,
};
use dialoguer::{Input, Select, theme::ColorfulTheme};
use miette::{IntoDiagnostic, Result};
#[derive(Debug, PartialEq)]
pub struct AuthParams {
pub provider: IdentityProvider,
pub client_id: String,
pub scopes: Vec<String>,
}
impl AuthParams {
pub fn new(profile: &Profile, args: AuthArgs, default_scope_prompt: &str) -> Result<Self> {
let theme = ColorfulTheme::default();
let manager = FileHistoryManager;
let provider_kind = match args.provider.or(profile.provider.clone()) {
Some(p) => p,
None => Self::prompt_provider(&theme)?,
};
let provider = match provider_kind {
ProviderKind::Microsoft => {
let tenant_id = match args.tenant_id.or(profile.tenant_id.clone()) {
Some(t) => t,
None => Self::prompt_or_history(
&theme,
"Enter Tenant ID",
HistoryKey::Tenant,
None,
&manager,
)?,
};
IdentityProvider::Microsoft { tenant_id }
}
ProviderKind::Auth0 => {
let domain = match args.domain.or(profile.domain.clone()) {
Some(d) => d,
None => Self::prompt_or_history(
&theme,
"Enter Auth0 Domain (e.g. my-org.eu.auth0.com)",
HistoryKey::Domain,
None,
&manager,
)?,
};
let audience = match args.audience.or(profile.audience.clone()) {
Some(a) => a,
None => Self::prompt_or_history(
&theme,
"Enter Auth0 Audience (e.g. api://ez-token)",
HistoryKey::Audience,
None,
&manager,
)?,
};
IdentityProvider::Auth0 { domain, audience }
}
};
let client_id = match args.client_id.or(profile.client_id.clone()) {
Some(c) => c,
None => Self::prompt_or_history(
&theme,
"Enter Client ID",
HistoryKey::Client,
None,
&manager,
)?,
};
let scopes_str = match args.scopes.or(profile.default_scopes.clone()) {
Some(s) => s,
None => Self::prompt_or_history(
&theme,
&format!("Enter Scopes (e.g. {})", default_scope_prompt),
HistoryKey::Scopes,
Some(default_scope_prompt),
&manager,
)?,
};
let scopes = scopes_str
.split_whitespace()
.map(|s| s.to_string())
.collect();
Ok(Self {
provider,
client_id,
scopes,
})
}
fn prompt_or_history(
theme: &ColorfulTheme,
prompt: &str,
key: HistoryKey,
default: Option<&str>,
manager: &dyn HistoryManager,
) -> Result<String> {
let mut history = manager.load(key);
let mut input = Input::with_theme(theme)
.with_prompt(prompt)
.history_with(&mut history);
if let Some(d) = default {
input = input.default(d.to_string());
}
let result = input.interact_text().into_diagnostic()?;
manager.save(key, &history);
Ok(result)
}
fn prompt_provider(theme: &ColorfulTheme) -> Result<ProviderKind> {
let options = vec!["Microsoft Entra ID (Azure AD)", "Auth0"];
let selection = Select::with_theme(theme)
.with_prompt("Select Identity Provider")
.items(&options)
.default(0)
.interact()
.into_diagnostic()?;
match selection {
0 => Ok(ProviderKind::Microsoft),
_ => Ok(ProviderKind::Auth0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_profile() -> Profile {
Profile {
provider: None,
tenant_id: None,
domain: None,
audience: None,
client_id: None,
default_scopes: None,
}
}
#[test]
fn test_cli_args_override_profile_microsoft() {
let profile = Profile {
provider: Some(ProviderKind::Auth0),
tenant_id: Some("profile-tenant".to_string()),
client_id: Some("profile-client".to_string()),
default_scopes: Some("profile.read".to_string()),
..empty_profile()
};
let args = AuthArgs {
provider: Some(ProviderKind::Microsoft),
tenant_id: Some("cli-tenant".to_string()),
client_id: Some("cli-client".to_string()),
scopes: Some("cli.read cli.write".to_string()),
..AuthArgs::default()
};
let params = AuthParams::new(&profile, args, "default.scope").unwrap();
assert_eq!(
params,
AuthParams {
provider: IdentityProvider::Microsoft {
tenant_id: "cli-tenant".to_string(),
},
client_id: "cli-client".to_string(),
scopes: vec!["cli.read".to_string(), "cli.write".to_string()],
}
);
}
#[test]
fn test_profile_fallback_when_cli_args_missing_auth0() {
let profile = Profile {
provider: Some(ProviderKind::Auth0),
domain: Some("my-org.auth0.com".to_string()),
audience: Some("api://my-api".to_string()),
client_id: Some("profile-client".to_string()),
default_scopes: Some("openid profile".to_string()),
..empty_profile()
};
let args = AuthArgs::default();
let params = AuthParams::new(&profile, args, "default.scope").unwrap();
assert_eq!(
params,
AuthParams {
provider: IdentityProvider::Auth0 {
domain: "my-org.auth0.com".to_string(),
audience: "api://my-api".to_string(),
},
client_id: "profile-client".to_string(),
scopes: vec!["openid".to_string(), "profile".to_string()],
}
);
}
#[test]
fn test_mixed_resolution() {
let profile = Profile {
provider: Some(ProviderKind::Microsoft),
tenant_id: Some("common".to_string()),
..empty_profile()
};
let args = AuthArgs {
client_id: Some("cli-client".to_string()),
scopes: Some("api://ez/.default".to_string()),
..AuthArgs::default()
};
let params = AuthParams::new(&profile, args, "default").unwrap();
assert_eq!(
params,
AuthParams {
provider: IdentityProvider::Microsoft {
tenant_id: "common".to_string(),
},
client_id: "cli-client".to_string(),
scopes: vec!["api://ez/.default".to_string()],
}
);
}
#[test]
fn test_scopes_whitespace_splitting() {
let profile = empty_profile();
let args = AuthArgs {
provider: Some(ProviderKind::Microsoft),
tenant_id: Some("common".to_string()),
client_id: Some("12345".to_string()),
scopes: Some("scope1 scope2\tscope3".to_string()),
..AuthArgs::default()
};
let params = AuthParams::new(&profile, args, "default").unwrap();
assert_eq!(
params.scopes,
vec![
"scope1".to_string(),
"scope2".to_string(),
"scope3".to_string()
]
);
}
}