#![expect(
clippy::print_stdout,
clippy::exit,
clippy::expect_used,
reason = "CLI examples can be more lax"
)]
use loopauth::{CliTokenClient, TlsCertificate, TokenResponseFields};
const SLACK_AUTH_URL: &str = "https://slack.com/oauth/v2/authorize";
const SLACK_TOKEN_URL: &str = "https://slack.com/api/oauth.v2.access";
const DEFAULT_SCOPES: &str = "channels:history,channels:read,groups:history,groups:read";
const DEFAULT_PORT: u16 = 8443;
const FAILURE_EXIT_CODE: i32 = 1;
const SIGINT_EXIT_CODE: i32 = 130;
#[derive(serde::Deserialize)]
struct SlackV2TokenResponse {
authed_user: SlackAuthedUser,
}
#[derive(serde::Deserialize)]
struct SlackAuthedUser {
access_token: String,
refresh_token: Option<String>,
expires_in: Option<u64>,
scope: Option<String>,
}
impl From<SlackV2TokenResponse> for TokenResponseFields {
fn from(resp: SlackV2TokenResponse) -> Self {
let scope = resp.authed_user.scope.map(|s| s.replace(',', " "));
Self::new(resp.authed_user.access_token)
.with_refresh_token(resp.authed_user.refresh_token)
.with_expires_in(resp.authed_user.expires_in)
.with_token_type(Some("Bearer".to_string()))
.with_scope(scope)
}
}
#[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 client_secret = require_env("LOOPAUTH_CLIENT_SECRET");
let user_scopes =
std::env::var("LOOPAUTH_SCOPES").unwrap_or_else(|_| DEFAULT_SCOPES.to_string());
let port: u16 = std::env::var("LOOPAUTH_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(DEFAULT_PORT);
let auth_url = url::Url::parse(SLACK_AUTH_URL).expect("Slack auth URL is valid");
let token_url = url::Url::parse(SLACK_TOKEN_URL).expect("Slack token URL is valid");
let tls_dir = require_env("LOOPAUTH_TLS_DIR");
tracing::info!("using managed TLS certificates in {tls_dir}");
let cert = 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);
});
let client = CliTokenClient::builder()
.client_id(client_id)
.client_secret(client_secret)
.auth_url(auth_url)
.token_url(token_url)
.token_response_type::<SlackV2TokenResponse>()
.use_https_with(cert)
.require_port(port)
.on_auth_url(move |params| {
params.append("user_scope", &user_scopes);
})
.on_url(|url| {
tracing::info!("opening: {url}");
tracing::info!("waiting for browser callback... (Ctrl+C to cancel)");
})
.build();
tracing::info!("starting Slack OAuth v2 authorization flow");
match client.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}");
}
let scopes: Vec<String> = tokens.scopes().iter().map(ToString::to_string).collect();
if !scopes.is_empty() {
println!("scopes : {}", scopes.join(", "));
}
if let Some(expires) = tokens.expires_at()
&& let Ok(remaining) = expires.duration_since(std::time::SystemTime::now())
{
println!("expires in : {}s", remaining.as_secs());
}
}
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);
})
}