summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2024-01-27 21:45:33 -0800
committerJesse Morgan <jesse@jesterpm.net>2024-01-27 21:45:33 -0800
commit8df0ef0368dd3f6fdd2fccb5da458a1fe99f25e0 (patch)
tree55c873f7742411eb099f874b84d01327471bda62 /src/main.rs
Initial version of chkoauth2v0.1.0
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs183
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!();
+ }
+}