diff options
Diffstat (limited to 'src/main.rs')
-rw-r--r-- | src/main.rs | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ec91ec1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,183 @@ +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<String>, + + /// The OAuth2 client secret. + /// This may also be provided through the OAUTH2_CLIENT_SECRET environment + /// variable. + #[arg(long = "secret")] + client_secret: Option<String>, + /// Scopes that must be present for the request to succeed. + #[arg(long = "scope")] + scope: Vec<String>, + + /// 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<String>, +} + +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<BasicErrorResponseType>, + StandardTokenResponse<TokenType, BasicTokenType>, + BasicTokenType, + StandardTokenIntrospectionResponse<TokenType, BasicTokenType>, + StandardRevocableToken, + StandardErrorResponse<BasicErrorResponseType>, +>; + +fn handle_request() -> Result<Response<Option<String>>, 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!(); + } +} |