use ones_oidc::{
read_private_key, OpenIdconnectClient, OnesOidcConfig,
get_well_known_application_by_client_identifier, load_device_config,
DEFAULT_DEVICE_CONFIG_PATH, DEFAULT_PRIVATE_KEY_PATH,
};
use openidconnect::{core::CoreProviderMetadata, OAuth2TokenResponse};
use std::env;
use std::process;
#[derive(Debug)]
struct CliArgs {
program_name: String,
command: String,
subcommand: Option<String>,
identifier: Option<String>,
device_config_path: String,
private_key_path: String,
}
impl CliArgs {
fn parse() -> Result<Self, String> {
let args: Vec<String> = env::args().collect();
let program_name = args[0].clone();
if args.len() < 2 {
return Err("No command provided".to_string());
}
let mut command = String::new();
let mut subcommand = None;
let mut identifier = None;
let mut device_config_path = DEFAULT_DEVICE_CONFIG_PATH.to_string();
let mut private_key_path = DEFAULT_PRIVATE_KEY_PATH.to_string();
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--device-config" => {
if i + 1 >= args.len() {
return Err("--device-config requires a path argument".to_string());
}
device_config_path = args[i + 1].clone();
i += 2;
}
"--device-private-key" => {
if i + 1 >= args.len() {
return Err("--device-private-key requires a path argument".to_string());
}
private_key_path = args[i + 1].clone();
i += 2;
}
arg if !arg.starts_with("--") => {
if command.is_empty() {
command = arg.to_string();
} else if command == "well-known" && identifier.is_none() {
identifier = Some(arg.to_string());
} else if subcommand.is_none() {
subcommand = Some(arg.to_string());
} else if identifier.is_none() {
identifier = Some(arg.to_string());
}
i += 1;
}
_ => {
return Err(format!("Unknown argument: {}", args[i]));
}
}
}
if command.is_empty() {
return Err("No command provided".to_string());
}
Ok(CliArgs {
program_name,
command,
subcommand,
identifier,
device_config_path,
private_key_path,
})
}
}
#[tokio::main]
async fn main() {
let cli_args = match CliArgs::parse() {
Ok(args) => args,
Err(_) => {
let args: Vec<String> = env::args().collect();
print_help(&args[0]);
process::exit(1);
}
};
let result = match cli_args.command.as_str() {
"help" | "--help" | "-h" => {
print_help(&cli_args.program_name);
process::exit(0);
}
"device" => {
match cli_args.subcommand.as_deref() {
Some("access-token") => handle_device_access_token(&cli_args).await,
Some("config") => handle_device_config(&cli_args).await,
Some("help") | Some("--help") | Some("-h") => {
print_device_help(&cli_args.program_name);
process::exit(0);
}
Some(subcmd) => {
eprintln!("Unknown device subcommand: {}", subcmd);
print_device_help(&cli_args.program_name);
process::exit(1);
}
None => {
print_device_help(&cli_args.program_name);
process::exit(1);
}
}
}
"well-known" => {
match cli_args.identifier.as_deref() {
Some(identifier) => handle_well_known(&cli_args, identifier).await,
None => {
print_well_known_help(&cli_args.program_name);
process::exit(1);
}
}
}
_ => {
eprintln!("Unknown command: {}", cli_args.command);
print_help(&cli_args.program_name);
process::exit(1);
}
};
match result {
Ok(output) => println!("{}", output),
Err(e) => {
eprintln!("Error: {}", e);
process::exit(1);
}
}
}
async fn handle_device_access_token(cli_args: &CliArgs) -> Result<String, Box<dyn std::error::Error>> {
let device_config = load_device_config(&cli_args.device_config_path)?;
let private_key = read_private_key(&cli_args.private_key_path)?;
let client_id = device_config.client_id.clone();
let issuer_url = device_config.get_issuer_url()?;
let provider_metadata = CoreProviderMetadata::discover_async(
issuer_url.clone(),
async_http_client,
).await?;
let config = OnesOidcConfig::default();
let client = OpenIdconnectClient::with_config(
client_id,
issuer_url,
provider_metadata,
private_key,
config,
);
let token_response = client.device_access_token().await?;
let output = serde_json::json!({
"access_token": token_response.access_token().secret(),
"token_type": "Bearer",
"expires_in": token_response.expires_in().map(|d| d.as_secs()),
"scope": token_response.scopes().map(|scopes| {
scopes.iter().map(|s| s.to_string()).collect::<Vec<_>>().join(" ")
})
});
Ok(serde_json::to_string_pretty(&output)?)
}
async fn handle_device_config(cli_args: &CliArgs) -> Result<String, Box<dyn std::error::Error>> {
let device_config = load_device_config(&cli_args.device_config_path)?;
let output = serde_json::json!({
"client_id": device_config.client_id,
"domain": device_config.domain,
"host": device_config.host,
"id": device_config.id,
"initiated_on": device_config.initiated_on,
"is_managed": device_config.is_managed,
"key_security": device_config.key_security,
"key_type": device_config.key_type,
"location": device_config.location,
"role": device_config.role,
"title": device_config.title,
"config_path": cli_args.device_config_path,
"private_key_path": cli_args.private_key_path
});
Ok(serde_json::to_string_pretty(&output)?)
}
async fn handle_well_known(cli_args: &CliArgs, identifier: &str) -> Result<String, Box<dyn std::error::Error>> {
let device_config = load_device_config(&cli_args.device_config_path)?;
let private_key = read_private_key(&cli_args.private_key_path)?;
let client_id = device_config.client_id.clone();
let issuer_url = device_config.get_issuer_url()?;
let provider_metadata = CoreProviderMetadata::discover_async(
issuer_url.clone(),
async_http_client,
).await?;
let config = OnesOidcConfig::default();
let client = OpenIdconnectClient::with_config(
client_id,
issuer_url,
provider_metadata.clone(),
private_key,
config,
);
let token_response = client.device_access_token().await?;
let device_access_token = token_response.access_token().secret();
let app = get_well_known_application_by_client_identifier(
&provider_metadata,
device_access_token,
identifier,
).await?;
let output = serde_json::json!({
"id": app.id,
"name": app.name,
"type": app.r#type,
"description": app.description,
"hostname": app.hostname,
"client_identifier": app.client_identifier
});
Ok(serde_json::to_string_pretty(&output)?)
}
async fn async_http_client(
request: openidconnect::HttpRequest,
) -> Result<openidconnect::HttpResponse, openidconnect::reqwest::Error<reqwest::Error>> {
use openidconnect::reqwest::async_http_client;
async_http_client(request).await
}
fn print_help(program_name: &str) {
println!("ONES OIDC CLI Tool");
println!();
println!("USAGE:");
println!(" {} <COMMAND> [OPTIONS]", program_name);
println!();
println!("COMMANDS:");
println!(" device Device authentication operations");
println!(" well-known Get well-known application information");
println!(" help Show this help message");
println!();
println!("GLOBAL OPTIONS:");
println!(" --device-config <PATH> Device configuration file");
println!(" (default: {})", DEFAULT_DEVICE_CONFIG_PATH);
println!(" --device-private-key <PATH> Device private key file");
println!(" (default: {})", DEFAULT_PRIVATE_KEY_PATH);
println!(" -h, --help Show help information");
println!();
println!("EXAMPLES:");
println!(" {} device access-token", program_name);
println!(" {} device config", program_name);
println!(" {} well-known <identifier>", program_name);
println!(" {} --device-config /custom/path/device.yml device access-token", program_name);
println!(" {} help", program_name);
}
fn print_device_help(program_name: &str) {
println!("Device authentication operations");
println!();
println!("USAGE:");
println!(" {} [GLOBAL_OPTIONS] device <SUBCOMMAND>", program_name);
println!();
println!("SUBCOMMANDS:");
println!(" access-token Get device access token from IDP");
println!(" config Show device configuration structure");
println!(" help Show this help message");
println!();
println!("GLOBAL OPTIONS:");
println!(" --device-config <PATH> Device configuration file");
println!(" (default: {})", DEFAULT_DEVICE_CONFIG_PATH);
println!(" --device-private-key <PATH> Device private key file");
println!(" (default: {})", DEFAULT_PRIVATE_KEY_PATH);
println!();
println!("EXAMPLES:");
println!(" {} device access-token", program_name);
println!(" {} device config", program_name);
println!(" {} --device-config /opt/device.yml device access-token", program_name);
}
fn print_well_known_help(program_name: &str) {
println!("Get well-known application information");
println!();
println!("USAGE:");
println!(" {} [GLOBAL_OPTIONS] well-known <IDENTIFIER>", program_name);
println!();
println!("ARGUMENTS:");
println!(" <IDENTIFIER> Application client identifier to look up");
println!();
println!("GLOBAL OPTIONS:");
println!(" --device-config <PATH> Device configuration file");
println!(" (default: {})", DEFAULT_DEVICE_CONFIG_PATH);
println!(" --device-private-key <PATH> Device private key file");
println!(" (default: {})", DEFAULT_PRIVATE_KEY_PATH);
println!();
println!("EXAMPLES:");
println!(" {} well-known my-app-id", program_name);
println!(" {} well-known 12345678-1234-5678-9012-123456789abc", program_name);
println!(" {} --device-config /opt/device.yml well-known my-app", program_name);
}