Skip to content

Commit

Permalink
Add initial async backend
Browse files Browse the repository at this point in the history
  • Loading branch information
FrancisMurillo committed May 14, 2023
1 parent d726954 commit a62474c
Show file tree
Hide file tree
Showing 12 changed files with 1,314 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions oxide-auth-async-actix/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "oxide-auth-async-actix"
version = "0.1.0"
authors = ["Andreas Molzer <[email protected]>"]
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"
4 changes: 4 additions & 0 deletions oxide-auth-async-actix/Changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 0.2.0

- Now compatible to `actix = "4"`.
- No functional changes.
20 changes: 20 additions & 0 deletions oxide-auth-async-actix/Readme.md
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions oxide-auth-async-actix/examples/async-actix-example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "async-actix-example"
version = "0.0.0"
authors = ["Andreas Molzer <[email protected]>"]
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"
235 changes: 235 additions & 0 deletions oxide-auth-async-actix/examples/async-actix-example/src/main.rs
Original file line number Diff line number Diff line change
@@ -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 = "<html>
This page should be accessed via an oauth token from the client in the example. Click
<a href=\"http://localhost:8020/authorize?response_type=code&client_id=LocalClient\">
here</a> to begin the authorization process.
</html>
";

struct State {
endpoint: Generic<
ClientMap,
AuthMap<RandomGenerator>,
TokenMap<RandomGenerator>,
Vacant,
Vec<Scope>,
fn() -> OAuthResponse,
>,
}

enum Extras {
AuthGet,
AuthPost(String),
ClientCredentials,
Nothing,
}

async fn get_authorize(
(req, state): (OAuthRequest, web::Data<Addr<State>>),
) -> Result<OAuthResponse, WebError> {
// 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<Addr<State>>),
) -> Result<OAuthResponse, WebError> {
// 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<Addr<State>>)) -> Result<OAuthResponse, WebError> {
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<Addr<State>>),
) -> Result<OAuthResponse, WebError> {
state.send(Refresh(req).wrap(Extras::Nothing)).await?
}

async fn index(
(req, state): (OAuthResource, web::Data<Addr<State>>),
) -> Result<OAuthResponse, WebError> {
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::<url::Url>()
.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<OAuthRequest, Error = WebError> + 'a
where
S: OwnerSolicitor<OAuthRequest> + '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<Self>;
}

impl<Op> Handler<OAuthMessage<Op, Extras>> for State
where
Op: OAuthOperation,
{
type Result = ResponseFuture<Result<Op::Item, Op::Error>>;

fn handle(&mut self, msg: OAuthMessage<Op, Extras>, 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),
}
}
}
Loading

0 comments on commit a62474c

Please sign in to comment.