diff options
| -rw-r--r-- | Cargo.toml | 14 | ||||
| -rw-r--r-- | examples/demo/main.rs | 53 | ||||
| -rw-r--r-- | src/demo/main.rs | 47 | ||||
| -rw-r--r-- | src/indieauth.rs | 21 | ||||
| -rw-r--r-- | src/lib.rs | 106 |
5 files changed, 151 insertions, 90 deletions
@@ -1,20 +1,24 @@ [package] name = "actix-middleware-rfc7662" -version = "0.1.0" +version = "0.2.0" edition = "2021" description = "Actix-web extractor which validates OAuth2 tokens through an RFC 7662 token introspection endpoint." license = "MIT" repository = "https://github.com/jesterpm/actix-middleware-rfc7662" authors = ["Jesse Morgan <jesse@jesterpm.net>"] -[[bin]] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[[example]] name = "demo" -path = "src/demo/main.rs" +required-features = ["indieauth"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +indieauth = ["serde"] [dependencies] log = "0.4" actix-web = "4" oauth2 = "4" -futures-util = "0.3"
\ No newline at end of file +futures-util = "0.3" +serde = {version = "1", features = ["derive"], optional = true }
\ No newline at end of file diff --git a/examples/demo/main.rs b/examples/demo/main.rs new file mode 100644 index 0000000..7fddcb2 --- /dev/null +++ b/examples/demo/main.rs @@ -0,0 +1,53 @@ +use actix_middleware_rfc7662::indieauth::IndieAuthToken; +use actix_middleware_rfc7662::{ + AnyScope, RequireAuthorization, RequireAuthorizationConfig, RequireScope, +}; +use actix_web::{get, HttpResponse, HttpServer, Responder}; + +#[get("/read")] +async fn handle_read(auth: RequireAuthorization<AnyScope, IndieAuthToken>) -> impl Responder { + HttpResponse::Ok().body(format!( + "Hello {}!\n", + auth.introspection().extra_fields().me() + )) +} + +struct WriteScope; +impl RequireScope for WriteScope { + fn scope() -> &'static str { + "write" + } +} + +#[get("/write")] +async fn handle_write(_auth: RequireAuthorization<WriteScope, IndieAuthToken>) -> impl Responder { + HttpResponse::Ok().body("Success!\n") +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let bind = std::env::var("BIND").unwrap_or_else(|_| "127.0.0.1:8182".to_string()); + + let oauth_config = RequireAuthorizationConfig::<IndieAuthToken>::new( + std::env::var("CLIENT_ID").expect("Missing CLIENT_ID"), + std::env::var("CLIENT_SECRET").ok(), + std::env::var("AUTH_ENDPOINT") + .expect("Missing AUTH_ENDPOINT") + .parse() + .expect("AUTH_ENDPOINT: invalid url"), + std::env::var("INTROSPECT_ENDPOINT") + .expect("Missing INTROSPECT_ENDPOINT") + .parse() + .expect("INTROSPECT_ENDPOINT: invalid url"), + ); + + HttpServer::new(move || { + actix_web::App::new() + .app_data(oauth_config.clone()) + .service(handle_read) + .service(handle_write) + }) + .bind(bind)? + .run() + .await +} diff --git a/src/demo/main.rs b/src/demo/main.rs deleted file mode 100644 index f598b84..0000000 --- a/src/demo/main.rs +++ /dev/null @@ -1,47 +0,0 @@ -use actix_middleware_rfc7662::{ - AnyScope, RequireAuthorization, RequireAuthorizationConfig, RequireScope, -}; -use actix_web::{get, HttpResponse, HttpServer, Responder}; - -#[get("/read")] -async fn handle_read(_auth: RequireAuthorization<AnyScope>) -> impl Responder { - HttpResponse::Ok().body("Success!\n") -} - -struct WriteScope; -impl RequireScope for WriteScope { - fn scope() -> &'static str { - "write" - } -} - -#[get("/write")] -async fn handle_write(_auth: RequireAuthorization<WriteScope>) -> impl Responder { - HttpResponse::Ok().body("Success!\n") -} - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - let bind = std::env::var("BIND").unwrap_or_else(|_| "127.0.0.1:8182".to_string()); - - let oauth_config = RequireAuthorizationConfig::new( - "cid1".to_string(), - Some("cs1".to_string()), - "https://cadmium.jesterpm.net/oauth/authorize" - .parse() - .expect("invalid url"), - "https://cadmium.jesterpm.net/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(bind)? - .run() - .await -} diff --git a/src/indieauth.rs b/src/indieauth.rs new file mode 100644 index 0000000..0f04b84 --- /dev/null +++ b/src/indieauth.rs @@ -0,0 +1,21 @@ +//! Extras for working with IndieAuth endpoints. + +use oauth2::ExtraTokenFields; +use serde::{Deserialize, Serialize}; + +/// An IndieAuth access token and introspection reponse has an additional +/// `me` field. +/// +/// See https://indieauth.spec.indieweb.org/#access-token-verification +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct IndieAuthToken { + me: String, +} + +impl IndieAuthToken { + pub fn me(&self) -> &str { + &self.me + } +} + +impl ExtraTokenFields for IndieAuthToken {} @@ -12,106 +12,128 @@ //! //! # Example //! ``` +//! # use std::future::Future; //! # use actix_web::{ get, HttpResponse, HttpServer, Responder }; -//! # use actix_middleware_rfc7662::{AnyScope, RequireAuthorization, RequireAuthorizationConfig}; +//! # use actix_middleware_rfc7662::{AnyScope, RequireAuthorization, RequireAuthorizationConfig, StandardToken}; //! //! #[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( +//! fn setup_server() -> std::io::Result<impl Future> { +//! let oauth_config = RequireAuthorizationConfig::<StandardToken>::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 || { +//! Ok(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 +//! .run()) //! } //! ``` use actix_web::{dev, FromRequest, HttpRequest}; use futures_util::future::LocalBoxFuture; -use oauth2::basic::{BasicErrorResponseType, BasicTokenType}; +use oauth2::basic::BasicErrorResponseType; use oauth2::url::Url; -use oauth2::*; +use oauth2::{ + reqwest, AccessToken, AuthUrl, ClientId, ClientSecret, IntrospectionUrl, StandardErrorResponse, + StandardRevocableToken, StandardTokenResponse, TokenIntrospectionResponse, +}; use std::future::ready; use std::marker::PhantomData; use std::sync::Arc; +// Re-exports +pub use oauth2::{ + basic::BasicTokenType, EmptyExtraTokenFields as StandardToken, ExtraTokenFields, + StandardTokenIntrospectionResponse, +}; + mod error; -use error::Error; + +#[cfg(feature = "indieauth")] +pub mod indieauth; + +pub use error::Error; const BEARER_TOKEN_PREFIX: &str = "Bearer "; -pub type IntrospectionResponse = - StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>; +pub type IntrospectionResponse<T> = StandardTokenIntrospectionResponse<T, BasicTokenType>; -pub trait AuthorizationRequirements { - fn authorized(introspection: &IntrospectionResponse) -> Result<bool, Error>; +pub trait AuthorizationRequirements<T> +where + T: ExtraTokenFields, +{ + fn authorized(introspection: &IntrospectionResponse<T>) -> Result<bool, Error>; } pub trait RequireScope { fn scope() -> &'static str; } -impl<T> AuthorizationRequirements for T +impl<T, S> AuthorizationRequirements<T> for S where - T: RequireScope, + S: RequireScope, + T: ExtraTokenFields, { - fn authorized(introspection: &IntrospectionResponse) -> Result<bool, Error> { + fn authorized(introspection: &IntrospectionResponse<T>) -> Result<bool, Error> { Ok(introspection .scopes() - .map(|s| s.iter().find(|s| s.as_ref() == T::scope()).is_some()) + .map(|s| s.iter().find(|s| s.as_ref() == S::scope()).is_some()) .unwrap_or(false)) } } pub struct AnyScope; -impl AuthorizationRequirements for AnyScope { - fn authorized(_: &IntrospectionResponse) -> Result<bool, Error> { +impl<T> AuthorizationRequirements<T> for AnyScope +where + T: ExtraTokenFields, +{ + fn authorized(_: &IntrospectionResponse<T>) -> Result<bool, Error> { Ok(true) } } -pub struct RequireAuthorization<T> +pub struct RequireAuthorization<R, T = StandardToken> where - T: AuthorizationRequirements, + R: AuthorizationRequirements<T>, + T: ExtraTokenFields, { - introspection: IntrospectionResponse, - _auth_marker: PhantomData<T>, + introspection: IntrospectionResponse<T>, + _auth_marker: PhantomData<R>, } -impl<T> RequireAuthorization<T> +impl<R, T> RequireAuthorization<R, T> where - T: AuthorizationRequirements, + R: AuthorizationRequirements<T>, + T: ExtraTokenFields, { - pub fn introspection(&self) -> &IntrospectionResponse { + pub fn introspection(&self) -> &IntrospectionResponse<T> { &self.introspection } } -impl<T> FromRequest for RequireAuthorization<T> +impl<R, T> FromRequest for RequireAuthorization<R, T> where - T: AuthorizationRequirements + 'static, + R: AuthorizationRequirements<T> + 'static, + T: ExtraTokenFields + 'static + Clone, { 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>() { + let my_req2 = req.clone(); + + let verifier = if let Some(verifier) = my_req2.app_data::<RequireAuthorizationConfig<T>>() { verifier.clone() } else { return Box::pin(ready(Err(Error::ConfigurationError))); @@ -124,7 +146,7 @@ where .verify_request(my_req) .await .and_then(|introspection| { - if T::authorized(&introspection)? { + if R::authorized(&introspection)? { Ok(RequireAuthorization { introspection, _auth_marker: PhantomData::default(), @@ -138,21 +160,29 @@ where } #[derive(Clone)] -struct RequireAuthorizationConfigInner { +struct RequireAuthorizationConfigInner<T> +where + T: ExtraTokenFields, +{ client: oauth2::Client< StandardErrorResponse<BasicErrorResponseType>, - StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>, + StandardTokenResponse<T, BasicTokenType>, BasicTokenType, - StandardTokenIntrospectionResponse<EmptyExtraTokenFields, BasicTokenType>, + StandardTokenIntrospectionResponse<T, BasicTokenType>, StandardRevocableToken, StandardErrorResponse<BasicErrorResponseType>, >, } #[derive(Clone)] -pub struct RequireAuthorizationConfig(Arc<RequireAuthorizationConfigInner>); +pub struct RequireAuthorizationConfig<T>(Arc<RequireAuthorizationConfigInner<T>>) +where + T: ExtraTokenFields; -impl RequireAuthorizationConfig { +impl<T> RequireAuthorizationConfig<T> +where + T: ExtraTokenFields, +{ pub fn new( client_id: String, client_secret: Option<String>, @@ -169,7 +199,7 @@ impl RequireAuthorizationConfig { RequireAuthorizationConfig(Arc::new(RequireAuthorizationConfigInner { client })) } - async fn verify_request(&self, req: HttpRequest) -> Result<IntrospectionResponse, Error> { + async fn verify_request(&self, req: HttpRequest) -> Result<IntrospectionResponse<T>, Error> { let access_token = req .headers() .get("Authorization") |
