actix_middleware_rfc7662/
lib.rs

1//! Actix-web extractor which validates OAuth2 tokens through an
2//! [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) token
3//! introspection endpoint.
4//!
5//! To protect a resource, you add the `RequireAuthorization` extractor.
6//! This extractor must be configured with a token introspection url
7//! before it can be used.
8//!
9//! The extractor takes an implementation of the
10//! `AuthorizationRequirements` trait, which is used to analyze the
11//! introspection response to determine if the request is authorized.
12//!
13//! # Example
14//! ```
15//! # use std::future::Future;
16//! # use actix_web::{ get, HttpResponse, HttpServer, Responder };
17//! # use actix_middleware_rfc7662::{AnyScope, RequireAuthorization, RequireAuthorizationConfig, StandardToken};
18//!
19//! #[get("/protected/api")]
20//! async fn handle_read(_auth: RequireAuthorization<AnyScope>) -> impl Responder {
21//!     HttpResponse::Ok().body("Success!\n")
22//! }
23//!
24//! fn setup_server() -> std::io::Result<impl Future> {
25//!     let oauth_config = RequireAuthorizationConfig::<StandardToken>::new(
26//!         "client_id".to_string(),
27//!         Some("client_secret".to_string()),
28//!         "https://example.com/oauth/authorize".parse().expect("invalid url"),
29//!         "https://example.com/oauth/introspect".parse().expect("invalid url"),
30//!     );
31//!
32//!     Ok(HttpServer::new(move || {
33//!         actix_web::App::new()
34//!             .app_data(oauth_config.clone())
35//!             .service(handle_read)
36//!     })
37//!     .bind("127.0.0.1:8182".to_string())?
38//!     .run())
39//! }
40//! ```
41
42use actix_web::{dev, FromRequest, HttpRequest};
43use futures_util::future::LocalBoxFuture;
44use oauth2::basic::BasicErrorResponseType;
45use oauth2::url::Url;
46use oauth2::{
47    reqwest, AccessToken, AuthUrl, ClientId, ClientSecret, IntrospectionUrl, StandardErrorResponse,
48    StandardRevocableToken, StandardTokenResponse, TokenIntrospectionResponse,
49};
50use std::future::ready;
51use std::marker::PhantomData;
52use std::sync::Arc;
53
54// Re-exports
55pub use oauth2::{
56    basic::BasicTokenType, EmptyExtraTokenFields as StandardToken, ExtraTokenFields,
57    StandardTokenIntrospectionResponse,
58};
59
60mod error;
61
62#[cfg(feature = "indieauth")]
63pub mod indieauth;
64
65pub use error::Error;
66
67const BEARER_TOKEN_PREFIX: &str = "Bearer ";
68
69pub type IntrospectionResponse<T> = StandardTokenIntrospectionResponse<T, BasicTokenType>;
70
71pub trait AuthorizationRequirements<T>
72where
73    T: ExtraTokenFields,
74{
75    fn authorized(introspection: &IntrospectionResponse<T>) -> Result<bool, Error>;
76}
77
78pub trait RequireScope {
79    fn scope() -> &'static str;
80}
81
82impl<T, S> AuthorizationRequirements<T> for S
83where
84    S: RequireScope,
85    T: ExtraTokenFields,
86{
87    fn authorized(introspection: &IntrospectionResponse<T>) -> Result<bool, Error> {
88        Ok(introspection
89            .scopes()
90            .map(|s| s.iter().find(|s| s.as_ref() == S::scope()).is_some())
91            .unwrap_or(false))
92    }
93}
94
95pub struct AnyScope;
96
97impl<T> AuthorizationRequirements<T> for AnyScope
98where
99    T: ExtraTokenFields,
100{
101    fn authorized(_: &IntrospectionResponse<T>) -> Result<bool, Error> {
102        Ok(true)
103    }
104}
105
106pub struct RequireAuthorization<R, T = StandardToken>
107where
108    R: AuthorizationRequirements<T>,
109    T: ExtraTokenFields,
110{
111    introspection: IntrospectionResponse<T>,
112    _auth_marker: PhantomData<R>,
113}
114
115impl<R, T> RequireAuthorization<R, T>
116where
117    R: AuthorizationRequirements<T>,
118    T: ExtraTokenFields,
119{
120    pub fn introspection(&self) -> &IntrospectionResponse<T> {
121        &self.introspection
122    }
123}
124
125impl<R, T> FromRequest for RequireAuthorization<R, T>
126where
127    R: AuthorizationRequirements<T> + 'static,
128    T: ExtraTokenFields + 'static + Clone,
129{
130    type Error = Error;
131    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
132
133    fn from_request(req: &actix_web::HttpRequest, _: &mut dev::Payload) -> Self::Future {
134        let my_req2 = req.clone();
135
136        let verifier = if let Some(verifier) = my_req2.app_data::<RequireAuthorizationConfig<T>>() {
137            verifier.clone()
138        } else {
139            return Box::pin(ready(Err(Error::ConfigurationError)));
140        };
141
142        let my_req = req.clone();
143
144        Box::pin(async move {
145            verifier
146                .verify_request(my_req)
147                .await
148                .and_then(|introspection| {
149                    if R::authorized(&introspection)? {
150                        Ok(RequireAuthorization {
151                            introspection,
152                            _auth_marker: PhantomData::default(),
153                        })
154                    } else {
155                        Err(Error::AccessDenied)
156                    }
157                })
158        })
159    }
160}
161
162#[derive(Clone)]
163struct RequireAuthorizationConfigInner<T>
164where
165    T: ExtraTokenFields,
166{
167    client: oauth2::Client<
168        StandardErrorResponse<BasicErrorResponseType>,
169        StandardTokenResponse<T, BasicTokenType>,
170        BasicTokenType,
171        StandardTokenIntrospectionResponse<T, BasicTokenType>,
172        StandardRevocableToken,
173        StandardErrorResponse<BasicErrorResponseType>,
174    >,
175}
176
177#[derive(Clone)]
178pub struct RequireAuthorizationConfig<T>(Arc<RequireAuthorizationConfigInner<T>>)
179where
180    T: ExtraTokenFields;
181
182impl<T> RequireAuthorizationConfig<T>
183where
184    T: ExtraTokenFields,
185{
186    pub fn new(
187        client_id: String,
188        client_secret: Option<String>,
189        auth_url: Url,
190        introspection_url: Url,
191    ) -> Self {
192        let client = oauth2::Client::new(
193            ClientId::new(client_id),
194            client_secret.map(|s| ClientSecret::new(s)),
195            AuthUrl::from_url(auth_url),
196            None,
197        )
198        .set_introspection_uri(IntrospectionUrl::from_url(introspection_url));
199        RequireAuthorizationConfig(Arc::new(RequireAuthorizationConfigInner { client }))
200    }
201
202    async fn verify_request(&self, req: HttpRequest) -> Result<IntrospectionResponse<T>, Error> {
203        let access_token = req
204            .headers()
205            .get("Authorization")
206            .and_then(|value| value.to_str().ok())
207            .filter(|value| value.starts_with(BEARER_TOKEN_PREFIX))
208            .map(|value| AccessToken::new(value.split_at(BEARER_TOKEN_PREFIX.len()).1.to_string()))
209            .ok_or(Error::MissingToken)?;
210
211        self.0
212            .client
213            .introspect(&access_token)
214            .map_err(|e| {
215                log::error!("OAuth2 client configuration error: {}", e);
216                Error::ConfigurationError
217            })?
218            .request_async(reqwest::async_http_client)
219            .await
220            .map_err(|e| {
221                log::warn!("Error from token introspection service: {}", e);
222                Error::IntrospectionServerError
223            })
224            .and_then(|resp| {
225                if resp.active() {
226                    Ok(resp)
227                } else {
228                    Err(Error::InvalidToken)
229                }
230            })
231    }
232}