//! 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) -> 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; pub trait AuthorizationRequirements { fn authorized(introspection: &IntrospectionResponse) -> Result; } pub trait RequireScope { fn scope() -> &'static str; } impl AuthorizationRequirements for T where T: RequireScope, { fn authorized(introspection: &IntrospectionResponse) -> Result { 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 { Ok(true) } } pub struct RequireAuthorization where T: AuthorizationRequirements, { introspection: IntrospectionResponse, _auth_marker: PhantomData, } impl RequireAuthorization where T: AuthorizationRequirements, { pub fn introspection(&self) -> &IntrospectionResponse { &self.introspection } } impl FromRequest for RequireAuthorization where T: AuthorizationRequirements + 'static, { type Error = Error; type Future = LocalBoxFuture<'static, Result>; fn from_request(req: &actix_web::HttpRequest, _: &mut dev::Payload) -> Self::Future { let verifier = if let Some(verifier) = req.app_data::() { 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, StandardTokenResponse, BasicTokenType, StandardTokenIntrospectionResponse, StandardRevocableToken, StandardErrorResponse, >, } #[derive(Clone)] pub struct RequireAuthorizationConfig(Arc); impl RequireAuthorizationConfig { pub fn new( client_id: String, client_secret: Option, 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 { 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) } }) } }