use clap::Parser; use oauth2::basic::BasicErrorResponseType; use oauth2::http::Response; use oauth2::url::Url; use oauth2::{ basic::BasicTokenType, reqwest, AccessToken, AuthUrl, ClientId, ClientSecret, IntrospectionUrl, StandardErrorResponse, StandardRevocableToken, StandardTokenIntrospectionResponse, StandardTokenResponse, TokenIntrospectionResponse, }; use std::collections::HashSet; use std::io::Write; use std::process::Command; use std::{env, io, process}; #[cfg(feature = "indieauth")] mod indieauth; mod error; use error::Error; /// A CGI wrapper to validate OAuth2 bearer tokens before executing another /// script. #[derive(Parser, Debug)] struct Args { /// The OAuth2 client id to authenticate with the introspection endpoint. /// This may also be provided through the OAUTH2_CLIENT_ID environment /// variable. #[arg(long = "client-id")] client_id: Option, /// The OAuth2 client secret. /// This may also be provided through the OAUTH2_CLIENT_SECRET environment /// variable. #[arg(long = "secret")] client_secret: Option, /// Scopes that must be present for the request to succeed. #[arg(long = "scope")] scope: Vec, /// The URL of the Authorization endpoint. auth_url: Url, /// The URL of the token introspection endpoint. introspection_url: Url, /// The command to run if authorized. command: String, /// Arguments to pass to the command. args: Vec, } const BEARER_TOKEN_PREFIX: &str = "Bearer "; #[cfg(feature = "indieauth")] type TokenType = indieauth::IndieAuthToken; #[cfg(not(feature = "indieauth"))] type TokenType = oauth2::EmptyExtraTokenFields; type Client = oauth2::Client< StandardErrorResponse, StandardTokenResponse, BasicTokenType, StandardTokenIntrospectionResponse, StandardRevocableToken, StandardErrorResponse, >; fn handle_request() -> Result>, Error> { let args = Args::parse(); let client_id = args .client_id .or_else(|| env::var("OAUTH2_CLIENT_ID").ok()) .map(ClientId::new) .expect("Missing required argument --client-id"); let client_secret = args .client_secret .or_else(|| env::var("OAUTH2_CLIENT_SECRET").ok()) .map(ClientSecret::new); let client = Client::new( client_id, client_secret, AuthUrl::from_url(args.auth_url), None, ) .set_introspection_uri(IntrospectionUrl::from_url(args.introspection_url)); let access_token = env::var("HTTP_AUTHORIZATION") .ok() .filter(|value| value.starts_with(BEARER_TOKEN_PREFIX)) .map(|value| { AccessToken::new( value.split_at(BEARER_TOKEN_PREFIX.len()).1.to_string(), ) }) .ok_or_else(|| { log::info!("Authorization failed due to missing or malformed Authorization header"); Error::MissingToken })?; let resp = client .introspect(&access_token) .map_err(|e| { log::error!("OAuth2 client configuration error: {}", e); Error::ConfigurationError })? .request(reqwest::http_client) .map_err(|e| { log::warn!("Error from token introspection service: {:?}", e); Error::IntrospectionServerError }) .and_then(|resp| { if resp.active() { Ok(resp) } else { log::info!("Authorization failed due to invalid token"); Err(Error::InvalidToken) } })?; let authorized_scopes: HashSet<&str> = resp .scopes() .iter() .flat_map(|s| s.iter()) .map(|s| s.as_ref()) .collect(); // Check if all required scopes are granted by the token. if !args .scope .iter() .all(|s| authorized_scopes.contains(s.as_str())) { log::info!("Authorization failed due to missing required scope(s)"); return Err(Error::AccessDenied); } let scopes = authorized_scopes.iter().fold(String::new(), |mut a, b| { a.push_str(b); a.push(' '); a }); let mut cmd = Command::new(args.command); cmd.args(args.args); cmd.env("OAUTH2_SCOPES", scopes.trim_end()); #[cfg(feature = "indieauth")] cmd.env("INDIEAUTH_ME", resp.extra_fields().me()); let status = cmd.status()?.code().unwrap_or(0); process::exit(status) } fn main() { env_logger::init(); let resp = handle_request().unwrap_or_else(|e| e.into()); println!( "Status: {} {}", resp.status().as_str(), resp.status().canonical_reason().unwrap_or("") ); for (name, value) in resp.headers().iter() { print!("{}: ", name); io::stdout().write_all(value.as_bytes()).expect("IO Error"); println!(); } if let Some(body) = resp.body() { println!("Content-Type: application/json"); println!(); println!("{}", body) } else { println!(); } }