#![expect(
clippy::print_stdout,
clippy::exit,
clippy::expect_used,
reason = "CLI examples can be more lax"
)]
use loopauth::{CliTokenClient, RequestScope, TlsCertificate};
const DEFAULT_SCOPES: &str = "email,profile";
const FAILURE_EXIT_CODE: i32 = 1;
const SIGINT_EXIT_CODE: i32 = 130;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
if std::env::args().any(|a| a == "--setup-guide") {
println!("{}", TlsCertificate::SETUP_GUIDE);
return;
}
let client_id = require_env("LOOPAUTH_CLIENT_ID");
let auth_url = url::Url::parse(&require_env("LOOPAUTH_AUTH_URL"))
.expect("LOOPAUTH_AUTH_URL must be a valid URL");
let token_url = url::Url::parse(&require_env("LOOPAUTH_TOKEN_URL"))
.expect("LOOPAUTH_TOKEN_URL must be a valid URL");
let client_secret = std::env::var("LOOPAUTH_CLIENT_SECRET").ok();
let scopes = parse_scopes(
&std::env::var("LOOPAUTH_SCOPES").unwrap_or_else(|_| DEFAULT_SCOPES.to_string()),
);
let port_hint = std::env::var("LOOPAUTH_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok());
let certificate = load_certificate();
let mut builder = CliTokenClient::builder()
.client_id(client_id)
.auth_url(auth_url)
.token_url(token_url)
.with_openid_scope()
.without_jwks_validation()
.use_https_with(certificate)
.add_scopes(scopes)
.on_url(|url| {
tracing::info!("opening: {url}");
tracing::info!("waiting for browser callback... (Ctrl+C to cancel)");
});
if let Some(secret) = client_secret {
builder = builder.client_secret(secret);
}
if let Some(port) = port_hint {
builder = builder.port_hint(port);
}
let auth = builder.build();
tracing::info!("starting HTTPS authorization flow");
match auth.run_authorization_flow().await {
Ok(tokens) => {
println!("\n=== Authentication successful ===");
println!("access_token : {}", tokens.access_token());
if let Some(rt) = tokens.refresh_token() {
println!("refresh_token: {rt}");
}
if let Some(oidc) = tokens.oidc() {
println!("\n=== OIDC claims ===");
println!("sub : {}", oidc.claims().sub());
if let Some(email) = oidc.claims().email() {
println!("email : {email}");
}
if let Some(name) = oidc.claims().name() {
println!("name : {name}");
}
}
}
Err(loopauth::AuthError::Cancelled) => {
tracing::info!("cancelled");
std::process::exit(SIGINT_EXIT_CODE);
}
Err(e) => {
tracing::error!("authentication failed: {e}");
std::process::exit(FAILURE_EXIT_CODE);
}
}
}
fn load_certificate() -> TlsCertificate {
if let Ok(tls_dir) = std::env::var("LOOPAUTH_TLS_DIR") {
tracing::info!("using managed TLS certificates in {tls_dir}");
return TlsCertificate::ensure_localhost(&tls_dir).unwrap_or_else(|e| {
tracing::error!("TLS certificate setup failed: {e}");
if matches!(e, loopauth::TlsCertificateError::MkcertNotFound) {
println!("\n{}", TlsCertificate::SETUP_GUIDE_MANAGED);
}
std::process::exit(FAILURE_EXIT_CODE);
});
}
if let (Ok(cert_file), Ok(key_file)) = (
std::env::var("LOOPAUTH_CERT_FILE"),
std::env::var("LOOPAUTH_KEY_FILE"),
) {
tracing::info!("loading TLS certificate from {cert_file} + {key_file}");
return TlsCertificate::from_pem_files(&cert_file, &key_file).unwrap_or_else(|e| {
tracing::error!("failed to load TLS certificate: {e}");
tracing::info!("run with --setup-guide for certificate setup instructions");
std::process::exit(FAILURE_EXIT_CODE);
});
}
tracing::error!("set LOOPAUTH_TLS_DIR (recommended) or LOOPAUTH_CERT_FILE + LOOPAUTH_KEY_FILE");
std::process::exit(FAILURE_EXIT_CODE);
}
fn require_env(name: &str) -> String {
std::env::var(name).unwrap_or_else(|_| {
tracing::error!("{name} ENV var not set");
std::process::exit(FAILURE_EXIT_CODE);
})
}
fn parse_scopes(s: &str) -> Vec<RequestScope> {
s.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(RequestScope::from)
.collect()
}