From a62474c87a6d19396532b473835945c5f656690e Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Fri, 12 May 2023 15:54:15 +0800 Subject: [PATCH] Add initial async backend --- Cargo.toml | 2 + oxide-auth-async-actix/Cargo.toml | 28 + oxide-auth-async-actix/Changes.md | 4 + oxide-auth-async-actix/Readme.md | 20 + .../examples/async-actix-example/Cargo.toml | 21 + .../examples/async-actix-example/src/main.rs | 235 +++++++++ .../async-actix-example/src/support.rs | 107 ++++ oxide-auth-async-actix/src/lib.rs | 482 ++++++++++++++++++ oxide-auth-async-actix/src/operations.rs | 94 ++++ oxide-auth-async/src/frontends/mod.rs | 1 + oxide-auth-async/src/frontends/simple.rs | 319 ++++++++++++ oxide-auth-async/src/lib.rs | 1 + 12 files changed, 1314 insertions(+) create mode 100644 oxide-auth-async-actix/Cargo.toml create mode 100644 oxide-auth-async-actix/Changes.md create mode 100644 oxide-auth-async-actix/Readme.md create mode 100644 oxide-auth-async-actix/examples/async-actix-example/Cargo.toml create mode 100644 oxide-auth-async-actix/examples/async-actix-example/src/main.rs create mode 100644 oxide-auth-async-actix/examples/async-actix-example/src/support.rs create mode 100644 oxide-auth-async-actix/src/lib.rs create mode 100644 oxide-auth-async-actix/src/operations.rs create mode 100644 oxide-auth-async/src/frontends/mod.rs create mode 100644 oxide-auth-async/src/frontends/simple.rs diff --git a/Cargo.toml b/Cargo.toml index 8bbc2323..0555f576 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "oxide-auth-async", "oxide-auth-actix", "oxide-auth-actix/examples/actix-example", + "oxide-auth-async-actix", + "oxide-auth-async-actix/examples/async-actix-example", "oxide-auth-axum", "oxide-auth-iron", "oxide-auth-poem", diff --git a/oxide-auth-async-actix/Cargo.toml b/oxide-auth-async-actix/Cargo.toml new file mode 100644 index 00000000..d83f36d9 --- /dev/null +++ b/oxide-auth-async-actix/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "oxide-auth-async-actix" +version = "0.1.0" +authors = ["Andreas Molzer "] +repository = "https://github.com/HeroicKatora/oxide-auth.git" + +description = "oxide-auth-actix but uses the oxide-auth-async as a endpoint backend to allow async operations." +readme = "Readme.md" +keywords = ["oauth", "server", "oauth2"] +categories = ["web-programming::http-server", "authentication"] +license = "MIT OR Apache-2.0" +edition = "2018" + +[dependencies] +actix = { version = "0.13", default-features = false } +actix-web = { version = "4.2.1", default-features = false } +async-trait = "0.1.59" +futures = "0.3" +oxide-auth = { version = "0.5.0", path = "../oxide-auth" } +oxide-auth-async = { version = "0.1.0", path = "../oxide-auth-async" } +serde_urlencoded = "0.7" +url = "2" + +[dev-dependencies] +base64 = "0.13" +chrono = { version = "0.4", default-features = false, features = ["clock"] } +serde = "1.0" +serde_json = "1.0" diff --git a/oxide-auth-async-actix/Changes.md b/oxide-auth-async-actix/Changes.md new file mode 100644 index 00000000..0e95a616 --- /dev/null +++ b/oxide-auth-async-actix/Changes.md @@ -0,0 +1,4 @@ +## 0.2.0 + +- Now compatible to `actix = "4"`. +- No functional changes. diff --git a/oxide-auth-async-actix/Readme.md b/oxide-auth-async-actix/Readme.md new file mode 100644 index 00000000..c431b3c9 --- /dev/null +++ b/oxide-auth-async-actix/Readme.md @@ -0,0 +1,20 @@ +# oxide-auth-async-actix + +Just `oxide-auth-actix` but uses `oxide-auth-async` as a backing +endpoint to allow for `async` operations in the trait methods + +## Additional + +[![Crates.io Status](https://img.shields.io/crates/v/oxide-auth-async-actix.svg)](https://crates.io/crates/oxide-auth-actix) +[![Docs.rs Status](https://docs.rs/oxide-auth-async-actix/badge.svg)](https://docs.rs/oxide-auth-actix/) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-MIT) +[![License](https://img.shields.io/badge/license-Apache-blue.svg)](https://raw.githubusercontent.com/HeroicKatora/oxide-auth/dev-v0.4.0/docs/LICENSE-APACHE) +[![CI Status](https://api.cirrus-ci.com/github/HeroicKatora/oxide-auth.svg)](https://cirrus-ci.com/github/HeroicKatora/oxide-auth) + +Licensed under either of + * MIT license ([LICENSE-MIT] or http://opensource.org/licenses/MIT) + * Apache License, Version 2.0 ([LICENSE-APACHE] or http://www.apache.org/licenses/LICENSE-2.0) +at your option. + +[LICENSE-MIT]: docs/LICENSE-MIT +[LICENSE-APACHE]: docs/LICENSE-APACHE diff --git a/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml b/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml new file mode 100644 index 00000000..7cbff535 --- /dev/null +++ b/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "async-actix-example" +version = "0.0.0" +authors = ["Andreas Molzer "] +edition = "2018" + +[dependencies] +actix = "0.13" +actix-web = "4.2.1" +actix-web-actors = "4.2.0" +env_logger = "0.9" +futures = "0.3" +oxide-auth = { version = "0.5.0", path = "./../../../oxide-auth" } +oxide-auth-async = { version = "0.1.0", path = "./../../../oxide-auth-async" } +oxide-auth-async-actix = { version = "0.1.0", path = "./../../" } +reqwest = { version = "0.11.10", features = ["blocking"] } +serde = "1.0" +serde_json = "1.0" +url = "2" +serde_urlencoded = "0.7" +tokio = "1.16.1" diff --git a/oxide-auth-async-actix/examples/async-actix-example/src/main.rs b/oxide-auth-async-actix/examples/async-actix-example/src/main.rs new file mode 100644 index 00000000..a0d77e95 --- /dev/null +++ b/oxide-auth-async-actix/examples/async-actix-example/src/main.rs @@ -0,0 +1,235 @@ +mod support; + +use actix::{Actor, Addr, Context, Handler, ResponseFuture}; +use actix_web::{ + middleware::{Logger, NormalizePath, TrailingSlash}, + web::{self, Data}, + App, HttpRequest, HttpServer, rt, +}; +use oxide_auth::{ + endpoint::{OwnerConsent, Solicitation, QueryParameter}, + frontends::simple::endpoint::{FnSolicitor, Vacant}, + primitives::prelude::{AuthMap, Client, ClientMap, RandomGenerator, Scope, TokenMap}, +}; +use oxide_auth_async::{ + endpoint::{Endpoint, OwnerSolicitor}, + frontends::simple::{ErrorInto, Generic}, +}; +use oxide_auth_async_actix::{ + Authorize, OAuthMessage, OAuthOperation, OAuthRequest, OAuthResource, OAuthResponse, Refresh, + Resource, Token, WebError, +}; +use std::thread; + +static DENY_TEXT: &str = " +This page should be accessed via an oauth token from the client in the example. Click + +here to begin the authorization process. + +"; + +struct State { + endpoint: Generic< + ClientMap, + AuthMap, + TokenMap, + Vacant, + Vec, + fn() -> OAuthResponse, + >, +} + +enum Extras { + AuthGet, + AuthPost(String), + ClientCredentials, + Nothing, +} + +async fn get_authorize( + (req, state): (OAuthRequest, web::Data>), +) -> Result { + // GET requests should not mutate server state and are extremely + // vulnerable accidental repetition as well as Cross-Site Request + // Forgery (CSRF). + state.send(Authorize(req).wrap(Extras::AuthGet)).await? +} + +async fn post_authorize( + (r, req, state): (HttpRequest, OAuthRequest, web::Data>), +) -> Result { + // Some authentication should be performed here in production cases + state + .send(Authorize(req).wrap(Extras::AuthPost(r.query_string().to_owned()))) + .await? +} + +async fn token((req, state): (OAuthRequest, web::Data>)) -> Result { + let grant_type = req.body().and_then(|body| body.unique_value("grant_type")); + // Different grant types determine which flow to perform. + match grant_type.as_deref() { + // Each flow will validate the grant_type again, so we can let one case handle + // any incorrect or unsupported options. + _ => state.send(Token(req).wrap(Extras::Nothing)).await?, + } +} + +async fn refresh( + (req, state): (OAuthRequest, web::Data>), +) -> Result { + state.send(Refresh(req).wrap(Extras::Nothing)).await? +} + +async fn index( + (req, state): (OAuthResource, web::Data>), +) -> Result { + match state + .send(Resource(req.into_request()).wrap(Extras::Nothing)) + .await? + { + Ok(_grant) => Ok(OAuthResponse::ok() + .content_type("text/plain")? + .body("Hello world!")), + Err(Ok(e)) => Ok(e.body(DENY_TEXT)), + Err(Err(e)) => Err(e), + } +} + +async fn start_browser() -> () { + let _ = thread::spawn(support::open_in_browser); +} + +// Example of a main function of an actix-web server supporting oauth. +#[actix_web::main] +pub async fn main() -> std::io::Result<()> { + std::env::set_var( + "RUST_LOG", + "actix_example=info,actix_web=info,actix_http=info,actix_service=info", + ); + env_logger::init(); + + // Start, then open in browser, don't care about this finishing. + rt::spawn(start_browser()); + + let state = State::preconfigured().start(); + + // Create the main server instance + let server = HttpServer::new(move || { + App::new() + .app_data(Data::new(state.clone())) + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .wrap(Logger::default()) + .service( + web::resource("/authorize") + .route(web::get().to(get_authorize)) + .route(web::post().to(post_authorize)), + ) + .route("/token", web::post().to(token)) + .route("/refresh", web::post().to(refresh)) + .route("/", web::get().to(index)) + }) + .bind("localhost:8020") + .expect("Failed to bind to socket") + .run(); + + let client = support::dummy_client(); + + futures::try_join!(server, client).map(|_| ()) +} + +impl State { + pub fn preconfigured() -> Self { + State { + endpoint: Generic { + // A registrar with one pre-registered client + registrar: vec![Client::confidential( + "LocalClient", + "http://localhost:8021/endpoint" + .parse::() + .unwrap() + .into(), + "default-scope".parse().unwrap(), + "SecretSecret".as_bytes(), + )] + .into_iter() + .collect(), + // Authorization tokens are 16 byte random keys to a memory hash map. + authorizer: AuthMap::new(RandomGenerator::new(16)), + // Bearer tokens are also random generated but 256-bit tokens, since they live longer + // and this example is somewhat paranoid. + // + // We could also use a `TokenSigner::ephemeral` here to create signed tokens which can + // be read and parsed by anyone, but not maliciously created. However, they can not be + // revoked and thus don't offer even longer lived refresh tokens. + issuer: TokenMap::new(RandomGenerator::new(16)), + + solicitor: Vacant, + + // A single scope that will guard resources for this endpoint + scopes: vec!["default-scope".parse().unwrap()], + + response: OAuthResponse::ok, + }, + } + } + + pub fn with_solicitor<'a, S: Send + Sync>( + &'a mut self, solicitor: S, + ) -> impl Endpoint + 'a + where + S: OwnerSolicitor + 'static, + { + ErrorInto::new(Generic { + authorizer: &mut self.endpoint.authorizer, + registrar: &mut self.endpoint.registrar, + issuer: &mut self.endpoint.issuer, + solicitor, + scopes: &mut self.endpoint.scopes, + response: OAuthResponse::ok, + }) + } +} + +impl Actor for State { + type Context = Context; +} + +impl Handler> for State +where + Op: OAuthOperation, +{ + type Result = ResponseFuture>; + + fn handle(&mut self, msg: OAuthMessage, ctx: &mut Self::Context) -> Self::Result { + let (op, ex) = msg.into_inner(); + + match ex { + Extras::AuthGet => { + let solicitor = FnSolicitor(move |_: &mut OAuthRequest, pre_grant: Solicitation| { + // This will display a page to the user asking for his permission to proceed. The submitted form + // will then trigger the other authorization handler which actually completes the flow. + OwnerConsent::InProgress( + OAuthResponse::ok() + .content_type("text/html") + .unwrap() + .body(&crate::support::consent_page_html("/authorize".into(), pre_grant)), + ) + }); + + op.run(self.with_solicitor(solicitor)) + } + Extras::AuthPost(query_string) => { + let solicitor = FnSolicitor(move |_: &mut OAuthRequest, _: Solicitation| { + if query_string.contains("allow") { + OwnerConsent::Authorized("dummy user".to_owned()) + } else { + OwnerConsent::Denied + } + }); + + op.run(self.with_solicitor(solicitor)) + } + _ => op.run(&mut self.endpoint), + } + } +} diff --git a/oxide-auth-async-actix/examples/async-actix-example/src/support.rs b/oxide-auth-async-actix/examples/async-actix-example/src/support.rs new file mode 100644 index 00000000..fc7a7b0c --- /dev/null +++ b/oxide-auth-async-actix/examples/async-actix-example/src/support.rs @@ -0,0 +1,107 @@ +#[rustfmt::skip] +#[path = "../../../../examples/support/generic.rs"] +mod generic; + +use std::collections::HashMap; + +pub use self::generic::{consent_page_html, open_in_browser, Client, ClientConfig, ClientError}; + +use actix_web::{ + App, dev, + web::{self, Data}, + HttpServer, HttpResponse, Responder, + middleware::{Logger, NormalizePath, TrailingSlash}, +}; + +pub fn dummy_client() -> dev::Server { + let client = Client::new(ClientConfig { + client_id: "LocalClient".into(), + client_secret: Some("SecretSecret".to_owned()), + protected_url: "http://localhost:8020/".into(), + token_url: "http://localhost:8020/token".into(), + refresh_url: "http://localhost:8020/refresh".into(), + redirect_uri: "http://localhost:8021/endpoint".into(), + }); + + HttpServer::new(move || { + App::new() + .app_data(Data::new(client.clone())) + .wrap(Logger::default()) + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .route("/endpoint", web::get().to(endpoint_impl)) + .route("/refresh", web::post().to(refresh)) + .route("/", web::get().to(get_with_token)) + }) + .bind("localhost:8021") + .expect("Failed to start dummy client") + .run() +} + +async fn endpoint_impl( + (query, state): (web::Query>, web::Data), +) -> impl Responder { + if let Some(cause) = query.get("error") { + return HttpResponse::BadRequest() + .body(format!("Error during owner authorization: {:?}", cause)); + } + + let code = match query.get("code") { + None => return HttpResponse::BadRequest().body("Missing code"), + Some(code) => code.clone(), + }; + + let auth_handle = tokio::task::spawn_blocking(move || { + let res = state.authorize(&code); + res + }); + let auth_result = auth_handle.await.unwrap(); + + match auth_result { + Ok(()) => HttpResponse::Found().append_header(("Location", "/")).finish(), + Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)), + } +} + +async fn refresh(state: web::Data) -> impl Responder { + let refresh_handle = tokio::task::spawn_blocking(move || { + let res = state.refresh(); + res + }); + let refresh_result = refresh_handle.await.unwrap(); + + match refresh_result { + Ok(()) => HttpResponse::Found().append_header(("Location", "/")).finish(), + Err(err) => HttpResponse::InternalServerError().body(format!("{}", err)), + } +} + +async fn get_with_token(state: web::Data) -> impl Responder { + let html = state.as_html(); + + let protected_page_handle = tokio::task::spawn_blocking(move || { + let res = state.retrieve_protected_page(); + res + }); + let protected_page_result = protected_page_handle.await.unwrap(); + + let protected_page = match protected_page_result { + Ok(page) => page, + Err(err) => return HttpResponse::InternalServerError().body(format!("{}", err)), + }; + + let display_page = format!( + " +
+ Used token to access + http://localhost:8020/. + Its contents are: +
{}
+
+
", html, protected_page); + + HttpResponse::Ok().content_type("text/html").body(display_page) +} diff --git a/oxide-auth-async-actix/src/lib.rs b/oxide-auth-async-actix/src/lib.rs new file mode 100644 index 00000000..41310590 --- /dev/null +++ b/oxide-auth-async-actix/src/lib.rs @@ -0,0 +1,482 @@ +//! Bindings and utilities for creating an oauth endpoint with actix. +//! +//! Use the provided methods to use code grant methods in an asynchronous fashion, or use an +//! `AsActor<_>` to create an actor implementing endpoint functionality via messages. +#![warn(missing_docs)] + +use actix::{MailboxError, Message}; +use actix_web::{ + body::BoxBody, + dev::Payload, + http::{ + header::{self, HeaderMap, InvalidHeaderValue}, + StatusCode, + }, + web::Form, + web::Query, + FromRequest, HttpRequest, HttpResponse, HttpResponseBuilder, Responder, ResponseError, +}; +use async_trait::async_trait; +use futures::future::{self, FutureExt, LocalBoxFuture, Ready}; +use oxide_auth::{ + endpoint::{NormalizedParameter, OAuthError, QueryParameter, WebRequest, WebResponse}, + frontends::simple::endpoint::Error, +}; +use oxide_auth_async::{ + endpoint::{Endpoint}, + primitives::{Authorizer}, +}; +use std::{borrow::Cow, convert::TryFrom, error, fmt}; +use url::Url; + +mod operations; + +pub use operations::{Authorize, Refresh, Resource, Token}; + +/// Describes an operation that can be performed in the presence of an `Endpoint` +/// +/// This trait can be implemented by any type, but is very useful in Actor scenarios, where an +/// Actor can provide an endpoint to an operation sent as a message. +/// +/// Here's how any Endpoint type can be turned into an Actor that responds to OAuthMessages: +/// ```rust,ignore +/// use actix::{Actor, Context, Handler}; +/// use oxide_auth::endpoint::Endpoint; +/// use oxide_auth_actix::OAuthOperation; +/// +/// pub struct MyEndpoint { +/// // Define your endpoint... +/// } +/// +/// impl Endpoint for MyEndpoint { +/// // Implement your endpoint... +/// } +/// +/// // Implement Actor +/// impl Actor for MyEndpoint { +/// type Context = Context; +/// } +/// +/// // Handle incoming OAuthMessages +/// impl Handler> for MyEndpoint +/// where +/// Op: OAuthOperation, +/// { +/// type Result = Result; +/// +/// fn handle(&mut self, msg: OAuthMessage, _: &mut Self::Context) -> Self::Result { +/// let (op, _) = msg.into_inner(); +/// +/// op.run(self) +/// } +/// } +/// ``` +/// +/// By additionally specifying a type for Extras, more advanced patterns can be used +/// ```rust,ignore +/// type Ext = Option; +/// +/// // Handle incoming OAuthMessages +/// impl Handler> for MyEndpoint +/// where +/// Op: OAuthOperation, +/// { +/// type Result = Result; +/// +/// fn handle(&mut self, msg: OAuthMessage, _: &mut Self::Context) -> Self::Result { +/// let (op, ext) = msg.into_inner(); +/// +/// op.run(self.with_my_custom_solicitor(ext)) +/// } +/// } +/// ``` +#[async_trait] +pub trait OAuthOperation: Sized + 'static { + /// The success-type produced by an OAuthOperation + type Item: 'static; + + /// The error type produced by an OAuthOperation + type Error: fmt::Debug + 'static; + + /// Performs the oxide operation with the provided endpoint + async fn run(self, endpoint: E) -> Result + where + E: Endpoint + Send + Sync, + E::Error: Send, + WebError: From; + + /// Turn an OAuthOperation into a Message to send to an actor + fn wrap(self, extras: Extras) -> OAuthMessage { + OAuthMessage(self, extras) + } +} + +/// A message type to easily send `OAuthOperation`s to an actor +pub struct OAuthMessage(Operation, Extras); + +#[derive(Clone, Debug)] +/// Type implementing `WebRequest` as well as `FromRequest` for use in route handlers +/// +/// This type consumes the body of the HttpRequest upon extraction, so be careful not to use it in +/// places you also expect an application payload +pub struct OAuthRequest { + auth: Option, + query: Option, + body: Option, +} + +impl OAuthResponse { + /// Get the headers from `OAuthResponse` + pub fn get_headers(&self) -> HeaderMap { + self.headers.clone() + } + + /// Get the body from `OAuthResponse` + pub fn get_body(&self) -> Option { + self.body.clone() + } +} + +/// Type implementing `WebRequest` as well as `FromRequest` for use in guarding resources +/// +/// This is useful over [OAuthRequest] since [OAuthResource] doesn't consume the body of the +/// request upon extraction +pub struct OAuthResource { + auth: Option, +} + +#[derive(Clone, Debug)] +/// Type implementing `WebResponse` and `Responder` for use in route handlers +pub struct OAuthResponse { + status: StatusCode, + headers: HeaderMap, + body: Option, +} + +#[derive(Debug)] +/// The error type for Oxide Auth operations +pub enum WebError { + /// Errors occuring in Endpoint operations + Endpoint(OAuthError), + + /// Errors occuring when producing Headers + Header(InvalidHeaderValue), + + /// Errors with the request encoding + Encoding, + + /// Request body could not be parsed as a form + Form, + + /// Request query was absent or could not be parsed + Query, + + /// Request was missing a body + Body, + + /// The Authorization header was invalid + Authorization, + + /// Processing part of the request was canceled + Canceled, + + /// An actor's mailbox was full + Mailbox, + + /// General internal server error + InternalError(Option), +} + +impl OAuthRequest { + /// Create a new OAuthRequest from an HttpRequest and Payload + pub async fn new(req: HttpRequest, mut payload: Payload) -> Result { + let query = Query::extract(&req) + .await + .ok() + .map(|q: Query| q.into_inner()); + let body = Form::from_request(&req, &mut payload) + .await + .ok() + .map(|b: Form| b.into_inner()); + + let mut all_auth = req.headers().get_all(header::AUTHORIZATION); + let optional = all_auth.next(); + + let auth = if all_auth.next().is_some() { + return Err(WebError::Authorization); + } else { + optional.and_then(|hv| hv.to_str().ok().map(str::to_owned)) + }; + + Ok(OAuthRequest { auth, query, body }) + } + + /// Fetch the authorization header from the request + pub fn authorization_header(&self) -> Option<&str> { + self.auth.as_deref() + } + + /// Fetch the query for this request + pub fn query(&self) -> Option<&NormalizedParameter> { + self.query.as_ref() + } + + /// Fetch the query mutably + pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> { + self.query.as_mut() + } + + /// Fetch the body of the request + pub fn body(&self) -> Option<&NormalizedParameter> { + self.body.as_ref() + } +} + +impl OAuthResource { + /// Create a new OAuthResource from an HttpRequest + pub fn new(req: &HttpRequest) -> Result { + let mut all_auth = req.headers().get_all(header::AUTHORIZATION); + let optional = all_auth.next(); + + let auth = if all_auth.next().is_some() { + return Err(WebError::Authorization); + } else { + optional.and_then(|hv| hv.to_str().ok().map(str::to_owned)) + }; + + Ok(OAuthResource { auth }) + } + + /// Turn this OAuthResource into an OAuthRequest for processing + pub fn into_request(self) -> OAuthRequest { + OAuthRequest { + query: None, + body: None, + auth: self.auth, + } + } +} + +impl OAuthResponse { + /// Create a simple response with no body and a '200 OK' HTTP Status + pub fn ok() -> Self { + OAuthResponse { + status: StatusCode::OK, + headers: HeaderMap::new(), + body: None, + } + } + + /// Set the `ContentType` header on a response + pub fn content_type(mut self, content_type: &str) -> Result { + self.headers + .insert(header::CONTENT_TYPE, TryFrom::try_from(content_type)?); + Ok(self) + } + + /// Set the bodyfor the response + pub fn body(mut self, body: &str) -> Self { + self.body = Some(body.to_owned()); + self + } +} + +impl OAuthMessage { + /// Produce an OAuthOperation from a wrapping OAuthMessage + pub fn into_inner(self) -> (Operation, Extras) { + (self.0, self.1) + } +} + +impl WebRequest for OAuthRequest { + type Error = WebError; + type Response = OAuthResponse; + + fn query(&mut self) -> Result, Self::Error> { + self.query + .as_ref() + .map(|q| Cow::Borrowed(q as &dyn QueryParameter)) + .ok_or(WebError::Query) + } + + fn urlbody(&mut self) -> Result, Self::Error> { + self.body + .as_ref() + .map(|b| Cow::Borrowed(b as &dyn QueryParameter)) + .ok_or(WebError::Body) + } + + fn authheader(&mut self) -> Result>, Self::Error> { + Ok(self.auth.as_deref().map(Cow::Borrowed)) + } +} + +impl WebResponse for OAuthResponse { + type Error = WebError; + + fn ok(&mut self) -> Result<(), Self::Error> { + self.status = StatusCode::OK; + Ok(()) + } + + fn redirect(&mut self, url: Url) -> Result<(), Self::Error> { + self.status = StatusCode::FOUND; + let location = String::from(url); + self.headers + .insert(header::LOCATION, TryFrom::try_from(location)?); + Ok(()) + } + + fn client_error(&mut self) -> Result<(), Self::Error> { + self.status = StatusCode::BAD_REQUEST; + Ok(()) + } + + fn unauthorized(&mut self, kind: &str) -> Result<(), Self::Error> { + self.status = StatusCode::UNAUTHORIZED; + self.headers + .insert(header::WWW_AUTHENTICATE, TryFrom::try_from(kind)?); + Ok(()) + } + + fn body_text(&mut self, text: &str) -> Result<(), Self::Error> { + self.body = Some(text.to_owned()); + self.headers + .insert(header::CONTENT_TYPE, TryFrom::try_from("text/plain")?); + Ok(()) + } + + fn body_json(&mut self, json: &str) -> Result<(), Self::Error> { + self.body = Some(json.to_owned()); + self.headers + .insert(header::CONTENT_TYPE, TryFrom::try_from("application/json")?); + Ok(()) + } +} + +impl Message for OAuthMessage +where + Operation: OAuthOperation + 'static, +{ + type Result = Result; +} + +impl FromRequest for OAuthRequest { + type Error = WebError; + type Future = LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + Self::new(req.clone(), payload.take()).boxed_local() + } +} + +impl FromRequest for OAuthResource { + type Error = WebError; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + future::ready(Self::new(req)) + } +} + +impl Responder for OAuthResponse { + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let mut builder = HttpResponseBuilder::new(self.status); + for (k, v) in self.headers.into_iter() { + builder.insert_header((k, v.to_owned())); + } + + if let Some(body) = self.body { + builder.body(body) + } else { + builder.finish() + } + } +} + +impl From for OAuthRequest { + fn from(o: OAuthResource) -> Self { + o.into_request() + } +} + +impl Default for OAuthResponse { + fn default() -> Self { + OAuthResponse { + status: StatusCode::OK, + headers: HeaderMap::new(), + body: None, + } + } +} + +impl From> for WebError { + fn from(e: Error) -> Self { + match e { + Error::Web(e) => e, + Error::OAuth(e) => e.into(), + } + } +} + +impl From for WebError { + fn from(e: InvalidHeaderValue) -> Self { + WebError::Header(e) + } +} + +impl From for WebError { + fn from(e: MailboxError) -> Self { + match e { + MailboxError::Closed => WebError::Mailbox, + MailboxError::Timeout => WebError::Canceled, + } + } +} + +impl From for WebError { + fn from(e: OAuthError) -> Self { + WebError::Endpoint(e) + } +} + +impl fmt::Display for WebError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + WebError::Endpoint(ref e) => write!(f, "Endpoint, {}", e), + WebError::Header(ref e) => write!(f, "Couldn't set header, {}", e), + WebError::Encoding => write!(f, "Error decoding request"), + WebError::Form => write!(f, "Request is not a form"), + WebError::Query => write!(f, "No query present"), + WebError::Body => write!(f, "No body present"), + WebError::Authorization => write!(f, "Request has invalid Authorization headers"), + WebError::Canceled => write!(f, "Operation canceled"), + WebError::Mailbox => write!(f, "An actor's mailbox was full"), + WebError::InternalError(None) => write!(f, "An internal server error occured"), + WebError::InternalError(Some(ref e)) => write!(f, "An internal server error occured: {}", e), + } + } +} + +impl error::Error for WebError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match *self { + WebError::Endpoint(ref e) => e.source(), + WebError::Header(ref e) => e.source(), + WebError::Encoding + | WebError::Form + | WebError::Authorization + | WebError::Query + | WebError::Body + | WebError::Canceled + | WebError::Mailbox + | WebError::InternalError(_) => None, + } + } +} + +impl ResponseError for WebError { + // Default to 500 for now +} diff --git a/oxide-auth-async-actix/src/operations.rs b/oxide-auth-async-actix/src/operations.rs new file mode 100644 index 00000000..299ac93a --- /dev/null +++ b/oxide-auth-async-actix/src/operations.rs @@ -0,0 +1,94 @@ +use async_trait::async_trait; +use crate::{OAuthRequest, OAuthResponse, OAuthOperation, WebError}; +use oxide_auth::primitives::grant::Grant; +use oxide_auth_async::{ + endpoint::{ + Endpoint, access_token::AccessTokenFlow, authorization::AuthorizationFlow, + resource::ResourceFlow, refresh::RefreshFlow, + }, +}; + +/// Authorization-related operations +pub struct Authorize(pub OAuthRequest); + +#[async_trait] +impl OAuthOperation for Authorize { + type Item = OAuthResponse; + type Error = WebError; + + async fn run(self, endpoint: E) -> Result + where + E: Endpoint + Send + Sync, + E::Error: Send, + WebError: From, + { + AuthorizationFlow::prepare(endpoint)? + .execute(self.0) + .await + .map_err(WebError::from) + } +} + +/// Token-related operations +pub struct Token(pub OAuthRequest); + +#[async_trait] +impl OAuthOperation for Token { + type Item = OAuthResponse; + type Error = WebError; + + async fn run(self, endpoint: E) -> Result + where + E: Endpoint + Send + Sync, + E::Error: Send, + WebError: From, + { + AccessTokenFlow::prepare(endpoint)? + .execute(self.0) + .await + .map_err(WebError::from) + } +} + +/// Refresh-related operations +pub struct Refresh(pub OAuthRequest); + +#[async_trait] +impl OAuthOperation for Refresh { + type Item = OAuthResponse; + type Error = WebError; + + async fn run(self, endpoint: E) -> Result + where + E: Endpoint + Send + Sync, + E::Error: Send, + WebError: From, + { + RefreshFlow::prepare(endpoint)? + .execute(self.0) + .await + .map_err(WebError::from) + } +} + +/// Resource-related operations +pub struct Resource(pub OAuthRequest); + +#[async_trait] +impl OAuthOperation for Resource { + type Item = Grant; + type Error = Result; + + async fn run(self, endpoint: E) -> Result + where + E: Endpoint + Send + Sync, + E::Error: Send, + WebError: From, + { + ResourceFlow::prepare(endpoint) + .map_err(|e| Err(WebError::from(e)))? + .execute(self.0) + .await + .map_err(|r| r.map_err(WebError::from)) + } +} diff --git a/oxide-auth-async/src/frontends/mod.rs b/oxide-auth-async/src/frontends/mod.rs new file mode 100644 index 00000000..b252f36b --- /dev/null +++ b/oxide-auth-async/src/frontends/mod.rs @@ -0,0 +1 @@ +pub mod simple; diff --git a/oxide-auth-async/src/frontends/simple.rs b/oxide-auth-async/src/frontends/simple.rs new file mode 100644 index 00000000..a2ffdb6d --- /dev/null +++ b/oxide-auth-async/src/frontends/simple.rs @@ -0,0 +1,319 @@ +use async_trait::async_trait; +use oxide_auth::{ + endpoint::{Scopes, WebRequest, Template, OAuthError, OwnerConsent, Solicitation}, + frontends::simple::endpoint::Error, + primitives::scope::Scope, +}; + +use crate::{ + endpoint::{ + Extension, Endpoint, OwnerSolicitor, access_token::AccessTokenFlow, + authorization::AuthorizationFlow, refresh::RefreshFlow, resource::ResourceFlow, + }, + primitives::{Registrar, Authorizer, Issuer}, +}; + +use std::marker::PhantomData; + +pub struct Generic< + R: Send + Sync, + A: Send + Sync, + I: Send + Sync, + S: Send + Sync = Vacant, + C: Send + Sync = Vacant, + L: Send + Sync = Vacant, +> { + /// The registrar implementation, or `Vacant` if it is not necesary. + pub registrar: R, + + /// The authorizer implementation, or `Vacant` if it is not necesary. + pub authorizer: A, + + /// The issuer implementation, or `Vacant` if it is not necesary. + pub issuer: I, + + /// A solicitor implementation fit for the request types, or `Vacant` if it is not necesary. + pub solicitor: S, + + /// Determine scopes for the request types, or `Vacant` if this does not protect resources. + pub scopes: C, + + /// Creates responses, or `Vacant` if `Default::default` is applicable. + pub response: L, +} + +pub struct ErrorInto(E, PhantomData); + +impl ErrorInto { + /// Create a new ErrorInto wrapping the supplied endpoint. + pub fn new(endpoint: E) -> Self { + ErrorInto(endpoint, PhantomData) + } +} + +pub struct Vacant; + +pub struct FnSolicitor(pub F); + +impl + Generic +{ + pub fn with_solicitor(self, new_solicitor: N) -> Generic { + Generic { + registrar: self.registrar, + authorizer: self.authorizer, + issuer: self.issuer, + solicitor: new_solicitor, + scopes: self.scopes, + response: self.response, + } + } + + pub fn with_scopes(self, new_scopes: S) -> Generic { + Generic { + registrar: self.registrar, + authorizer: self.authorizer, + issuer: self.issuer, + solicitor: self.solicitor, + scopes: new_scopes, + response: self.response, + } + } + + pub fn authorization_flow(self) -> AuthorizationFlow + where + Self: Endpoint, + W::Error: Send + Sync, + R: Registrar, + A: Authorizer, + { + match AuthorizationFlow::prepare(self) { + Ok(flow) => flow, + Err(_) => unreachable!(), + } + } + + pub fn access_token_flow(self) -> AccessTokenFlow + where + Self: Endpoint, + W::Error: Send + Sync, + R: Registrar, + A: Authorizer, + I: Issuer, + { + match AccessTokenFlow::prepare(self) { + Ok(flow) => flow, + Err(_) => unreachable!(), + } + } + + /// Create a token refresh flow. + /// + /// Opposed to `RefreshFlow::prepare` this statically ensures that the construction succeeds. + pub fn refresh_flow(self) -> RefreshFlow + where + Self: Endpoint, + W::Error: Send + Sync, + R: Registrar, + I: Issuer, + { + match RefreshFlow::prepare(self) { + Ok(flow) => flow, + Err(_) => unreachable!(), + } + } + + /// Create a resource access flow. + /// + /// Opposed to `ResourceFlow::prepare` this statically ensures that the construction succeeds. + pub fn resource_flow(self) -> ResourceFlow + where + Self: Endpoint, + W::Error: Send + Sync, + I: Issuer, + { + match ResourceFlow::prepare(self) { + Ok(flow) => flow, + Err(_) => unreachable!(), + } + } + + /// Check, statically, that this is an endpoint for some request. + /// + /// This is mainly a utility method intended for compilation and integration tests. + pub fn assert(self) -> Self + where + Self: Endpoint, + { + self + } +} + +#[async_trait] +impl Endpoint for ErrorInto +where + E: Endpoint, + E::Error: Into, + W: WebRequest, +{ + type Error = Error; + + fn registrar(&self) -> Option<&(dyn Registrar + Sync)> { + self.0.registrar() + } + + fn authorizer_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> { + self.0.authorizer_mut() + } + + fn issuer_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> { + self.0.issuer_mut() + } + + fn owner_solicitor(&mut self) -> Option<&mut (dyn OwnerSolicitor + Send)> { + self.0.owner_solicitor() + } + + fn scopes(&mut self) -> Option<&mut dyn Scopes> { + self.0.scopes() + } + + fn response(&mut self, request: &mut W, kind: Template<'_>) -> Result { + self.0.response(request, kind).map_err(Into::into) + } + + fn error(&mut self, err: OAuthError) -> Self::Error { + self.0.error(err).into() + } + + fn web_error(&mut self, err: W::Error) -> Self::Error { + self.0.web_error(err).into() + } + + fn extension(&mut self) -> Option<&mut (dyn Extension + Send)> { + self.0.extension() + } +} + +pub trait OptRegistrar { + fn opt_ref(&self) -> Option<&(dyn Registrar + Sync)>; +} + +impl OptRegistrar for T { + fn opt_ref(&self) -> Option<&(dyn Registrar + Sync)> { + Some(self) + } +} + +impl OptRegistrar for Vacant { + fn opt_ref(&self) -> Option<&(dyn Registrar + Sync)> { + Option::None + } +} + +pub trait OptAuthorizer { + fn opt_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)>; +} + +impl OptAuthorizer for T { + fn opt_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> { + Some(self) + } +} + +impl OptAuthorizer for Vacant { + fn opt_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> { + Option::None + } +} + +pub trait OptIssuer { + fn opt_mut(&mut self) -> Option<&mut (dyn Issuer + Send)>; +} + +impl OptIssuer for T { + fn opt_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> { + Some(self) + } +} + +impl OptIssuer for Vacant { + fn opt_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> { + Option::None + } +} + +pub trait ResponseCreator { + fn create(&mut self, request: &mut W, kind: Template) -> W::Response; +} + +impl ResponseCreator for F +where + F: FnMut() -> W::Response, +{ + fn create(&mut self, _: &mut W, _: Template) -> W::Response { + self() + } +} + +impl ResponseCreator for Vacant +where + W::Response: Default, +{ + fn create(&mut self, _: &mut W, _: Template) -> W::Response { + Default::default() + } +} + +impl< + W: Send + Sync, + R: Send + Sync, + A: Send + Sync, + I: Send + Sync, + O: Send + Sync, + C: Send + Sync, + L: Send + Sync, + > Endpoint for Generic +where + W: WebRequest, + R: OptRegistrar, + A: OptAuthorizer, + I: OptIssuer, + O: OwnerSolicitor, + C: Scopes, + L: ResponseCreator, +{ + type Error = Error; + + fn registrar(&self) -> Option<&(dyn Registrar + Sync)> { + self.registrar.opt_ref() + } + + fn authorizer_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> { + self.authorizer.opt_mut() + } + + fn issuer_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> { + self.issuer.opt_mut() + } + + fn owner_solicitor(&mut self) -> Option<&mut (dyn OwnerSolicitor + Send)> { + Some(&mut self.solicitor) + } + + fn scopes(&mut self) -> Option<&mut dyn Scopes> { + Some(&mut self.scopes) + } + + fn response(&mut self, request: &mut W, kind: Template) -> Result { + Ok(self.response.create(request, kind)) + } + + fn error(&mut self, err: OAuthError) -> Self::Error { + Error::OAuth(err) + } + + fn web_error(&mut self, err: W::Error) -> Self::Error { + Error::Web(err) + } +} diff --git a/oxide-auth-async/src/lib.rs b/oxide-auth-async/src/lib.rs index ea8f4010..b2ae858c 100644 --- a/oxide-auth-async/src/lib.rs +++ b/oxide-auth-async/src/lib.rs @@ -1,5 +1,6 @@ pub mod code_grant; pub mod endpoint; +pub mod frontends; pub mod primitives; #[cfg(test)]