ones-oidc 0.3.4

ONES OpenID Connect client for Rust
Documentation
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() {
                        // For well-known command, next argument is the identifier, not subcommand
                        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);
}