#![expect(
clippy::print_stdout,
clippy::exit,
reason = "CLI examples can be more lax"
)]
use loopauth::{CliTokenClientBuilder, RequestScope, oidc::OpenIdConfiguration};
use url::Url;
const DEFAULT_SCOPES: &str = "openid,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();
let client_id = require_env("LOOPAUTH_CLIENT_ID");
let issuer_url = Url::parse(&require_env("LOOPAUTH_ISSUER_URL")).unwrap_or_else(|e| {
tracing::error!("LOOPAUTH_ISSUER_URL is not a valid URL: {e}");
std::process::exit(FAILURE_EXIT_CODE);
});
let client_secret = std::env::var("LOOPAUTH_CLIENT_SECRET").ok();
let raw_scopes =
std::env::var("LOOPAUTH_SCOPES").unwrap_or_else(|_| DEFAULT_SCOPES.to_string());
let scopes = parse_scopes(&raw_scopes);
let port_hint = std::env::var("LOOPAUTH_PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok());
if !raw_scopes.split(',').map(str::trim).any(|s| s == "openid") {
tracing::error!("LOOPAUTH_SCOPES must include 'openid'");
std::process::exit(FAILURE_EXIT_CODE);
}
tracing::info!(issuer = issuer_url.as_str(), "fetching OIDC configuration");
let open_id_configuration = OpenIdConfiguration::fetch(issuer_url)
.await
.unwrap_or_else(|e| {
tracing::error!("OIDC discovery failed: {e}");
std::process::exit(FAILURE_EXIT_CODE);
});
let mut builder = CliTokenClientBuilder::from_open_id_configuration(&open_id_configuration)
.client_id(client_id)
.add_scopes(scopes)
.with_open_id_configuration_jwks_validator(&open_id_configuration)
.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 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 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()
}