summaryrefslogtreecommitdiff
path: root/src/lib.rs
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2022-03-20 17:39:10 -0700
committerJesse Morgan <jesse@jesterpm.net>2022-03-20 17:39:10 -0700
commit237accf0a1313ff5d73fe18096dd7229e0f8ba62 (patch)
treea7b8660ab4555706cf0f0c42f758f7afc46524eb /src/lib.rs
Initial commit
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs202
1 files changed, 202 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..670a397
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,202 @@
+//! Actix-web extractor which validates OAuth2 tokens through an
+//! [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) token
+//! introspection endpoint.
+//!
+//! To protect a resource, you add the `RequireAuthorization` extractor.
+//! This extractor must be configured with a token introspection url
+//! before it can be used.
+//!
+//! The extractor takes an implementation of the
+//! `AuthorizationRequirements` trait, which is used to analyze the
+//! introspection response to determine if the request is authorized.
+//!
+//! # Example
+//! ```
+//! # use actix_web::{ get, HttpResponse, HttpServer, Responder };
+//! # use actix_middleware_rfc7662::{AnyScope, RequireAuthorization, RequireAuthorizationConfig};
+//!
+//! #[get("/protected/api")]
+//! async fn handle_read(_auth: RequireAuthorization<AnyScope>) -> impl Responder {
+//! HttpResponse::Ok().body("Success!\n")
+//! }
+//!
+//! #[actix_web::main]
+//! async fn main() -> std::io::Result<()> {
+//! let oauth_config = RequireAuthorizationConfig::new(
+//! "client_id".to_string(),
+//! Some("client_secret".to_string()),
+//! "https://example.com/oauth/authorize".parse().expect("invalid url"),
+//! "https://example.com/oauth/introspect".parse().expect("invalid url"),
+//! );
+//!
+//! HttpServer::new(move || {
+//! actix_web::App::new()
+//! .app_data(oauth_config.clone())
+//! .service(handle_read)
+//! .service(handle_write)
+//! })
+//! .bind("127.0.0.1:8182".to_string())?
+//! .run()
+//! .await
+//! }
+//! ```
+
+use actix_web::{dev, FromRequest, HttpRequest};
+use futures_util::future::LocalBoxFuture;
+use oauth2::basic::{BasicErrorResponseType, BasicTokenType};
+use oauth2::url::Url;
+use oauth2::*;
+use std::future::ready;
+use std::marker::PhantomData;
+use std::sync::Arc;
+
+mod error;
+use error::Error;
+
+const BEARER_TOKEN_PREFIX: &str = "Bearer ";
+
+pub type IntrospectionResponse =
+ StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>;
+
+pub trait AuthorizationRequirements {
+ fn authorized(introspection: &IntrospectionResponse) -> Result<bool, Error>;
+}
+
+pub trait RequireScope {
+ fn scope() -> &'static str;
+}
+
+impl<T> AuthorizationRequirements for T
+where
+ T: RequireScope,
+{
+ fn authorized(introspection: &IntrospectionResponse) -> Result<bool, Error> {
+ Ok(introspection
+ .scopes()
+ .map(|s| s.iter().find(|s| s.as_ref() == T::scope()).is_some())
+ .unwrap_or(false))
+ }
+}
+
+pub struct AnyScope;
+
+impl AuthorizationRequirements for AnyScope {
+ fn authorized(_: &IntrospectionResponse) -> Result<bool, Error> {
+ Ok(true)
+ }
+}
+
+pub struct RequireAuthorization<T>
+where
+ T: AuthorizationRequirements,
+{
+ introspection: IntrospectionResponse,
+ _auth_marker: PhantomData<T>,
+}
+
+impl<T> RequireAuthorization<T>
+where
+ T: AuthorizationRequirements,
+{
+ pub fn introspection(&self) -> &IntrospectionResponse {
+ &self.introspection
+ }
+}
+
+impl<T> FromRequest for RequireAuthorization<T>
+where
+ T: AuthorizationRequirements + 'static,
+{
+ type Error = Error;
+ type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
+
+ fn from_request(req: &actix_web::HttpRequest, _: &mut dev::Payload) -> Self::Future {
+ let verifier = if let Some(verifier) = req.app_data::<RequireAuthorizationConfig>() {
+ verifier.clone()
+ } else {
+ return Box::pin(ready(Err(Error::ConfigurationError)));
+ };
+
+ let my_req = req.clone();
+
+ Box::pin(async move {
+ verifier
+ .verify_request(my_req)
+ .await
+ .and_then(|introspection| {
+ if T::authorized(&introspection)? {
+ Ok(RequireAuthorization {
+ introspection,
+ _auth_marker: PhantomData::default(),
+ })
+ } else {
+ Err(Error::AccessDenied)
+ }
+ })
+ })
+ }
+}
+
+#[derive(Clone)]
+struct RequireAuthorizationConfigInner {
+ client: oauth2::Client<
+ StandardErrorResponse<BasicErrorResponseType>,
+ StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
+ BasicTokenType,
+ StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>,
+ StandardRevocableToken,
+ StandardErrorResponse<BasicErrorResponseType>,
+ >,
+}
+
+#[derive(Clone)]
+pub struct RequireAuthorizationConfig(Arc<RequireAuthorizationConfigInner>);
+
+impl RequireAuthorizationConfig {
+ pub fn new(
+ client_id: String,
+ client_secret: Option<String>,
+ auth_url: Url,
+ introspection_url: Url,
+ ) -> Self {
+ let client = oauth2::Client::new(
+ ClientId::new(client_id),
+ client_secret.map(|s| ClientSecret::new(s)),
+ AuthUrl::from_url(auth_url),
+ None,
+ )
+ .set_introspection_uri(IntrospectionUrl::from_url(introspection_url));
+ RequireAuthorizationConfig(Arc::new(RequireAuthorizationConfigInner { client }))
+ }
+
+ async fn verify_request(&self, req: HttpRequest) -> Result<IntrospectionResponse, Error> {
+ let access_token = req
+ .headers()
+ .get("Authorization")
+ .and_then(|value| value.to_str().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(Error::MissingToken)?;
+
+ self.0
+ .client
+ .introspect(&access_token)
+ .map_err(|e| {
+ log::error!("OAuth2 client configuration error: {}", e);
+ Error::ConfigurationError
+ })?
+ .request_async(reqwest::async_http_client)
+ .await
+ .map_err(|e| {
+ log::warn!("Error from token introspection service: {}", e);
+ Error::IntrospectionServerError
+ })
+ .and_then(|resp| {
+ if resp.active() {
+ Ok(resp)
+ } else {
+ Err(Error::InvalidToken)
+ }
+ })
+ }
+}