Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DRAFT] Add async/await support for oxide-auth-async #165

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
27 changes: 27 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,27 @@
[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"
anyhow = "1.0.71"
env_logger = "0.9"
futures = "0.3"
oxide-auth = { version = "0.5.0", path = "./../../../oxide-auth" }
oxide-auth-actix = { version = "0.2.0", path = "./../../../oxide-auth-actix" }
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"
sqlx = { version = "0.6.3", features = ["sqlite", "offline", "runtime-actix-native-tls"] }
url = "2"
serde_urlencoded = "0.7"
tokio = "1.16.1"
async-trait = "0.1.68"
once_cell = "1.17.1"
chrono = "0.4.24"
282 changes: 282 additions & 0 deletions oxide-auth-async-actix/examples/async-actix-example/src/endpoint.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
use anyhow::Result;
use async_trait::async_trait;
use chrono::{Duration, offset::Utc};
use once_cell::sync::Lazy;
use oxide_auth::{
endpoint::{OAuthError, Scopes, Template},
primitives::{
grant::Grant,
issuer::{IssuedToken, RefreshedToken, TokenType},
scope::Scope,
registrar::{
BoundClient, ClientType, ClientUrl, EncodedClient, PasswordPolicy, PreGrant,
RegisteredClient, RegistrarError, RegisteredUrl,
},
},
};
use oxide_auth_async::{
endpoint::{Endpoint, OwnerSolicitor},
primitives::{Authorizer, Issuer, Registrar},
};
use oxide_auth_async_actix::{OAuthRequest, OAuthResponse, WebError};

use std::{sync::Arc, borrow::Cow};
use sqlx::{self, sqlite::SqlitePool, FromRow};
use url::Url;

pub struct DbEndpoint {
pool: Arc<SqlitePool>,
solicitor: Option<Box<dyn OwnerSolicitor<OAuthRequest> + Send + Sync>>,
}

#[derive(FromRow)]
pub struct App {
id: i32,
uid: String,
secret: String,
}

impl App {
fn token(&self) -> String {
format!("token{}", self.id)
}
}

static REDIRECT_URI: Lazy<RegisteredUrl> =
Lazy::new(|| "http://localhost:8021".parse::<Url>().unwrap().into());

static DEFAULT_SCOPES: Lazy<Scope> = Lazy::new(|| "read write".parse::<Scope>().unwrap());

impl DbEndpoint {
pub async fn create() -> Result<Self> {
let pool = SqlitePool::connect("sqlite::memory:").await?;

let mut conn = pool.acquire().await?;

sqlx::query(
"CREATE TABLE IF NOT EXISTS apps (
id INTEGER PRIMARY KEY NOT NULL,
uid VARCHAR(250) NOT NULL,
secret VARCHAR(250) NOT NULL
);",
)
.execute(&mut conn)
.await?;
sqlx::query(
"INSERT INTO apps (uid, secret)
VALUES (?, ?);",
)
.bind("clienta")
.bind("secreta")
.execute(&mut conn)
.await?;

drop(conn);

Ok(Self {
pool: Arc::new(pool),
solicitor: None,
})
}

pub fn with_solicitor<S>(&self, solicitor: S) -> Self
where
S: OwnerSolicitor<OAuthRequest> + Send + Sync + 'static,
{
Self {
pool: self.pool.clone(),
solicitor: Some(Box::new(solicitor)),
}
}

async fn find_app_by_uid(&self, uid: &str) -> Result<Option<App>> {
let mut conn = self.pool.acquire().await?;

let app_opt = sqlx::query_as::<_, App>("SELECT * FROM apps WHERE uid = ?")
.bind(uid)
.fetch_optional(&mut conn)
.await?;

Ok(app_opt)
}

pub async fn find_client_by_id(&self, client_id: &str) -> Result<Option<EncodedClient>> {
let app_opt = self.find_app_by_uid(client_id).await?;

Ok(app_opt.map(|app| EncodedClient {
client_id: app.uid,
redirect_uri: Lazy::force(&REDIRECT_URI).clone(),
additional_redirect_uris: Default::default(),
default_scope: Lazy::force(&DEFAULT_SCOPES).clone(),
encoded_client: ClientType::Confidential {
passdata: app.secret.into_bytes(),
},
}))
}
}

impl Endpoint<OAuthRequest> for DbEndpoint {
type Error = OAuthError;

fn registrar(&self) -> Option<&(dyn Registrar + Sync)> {
Some(self)
}

fn authorizer_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> {
Some(self)
}

fn issuer_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> {
Some(self)
}

fn owner_solicitor(&mut self) -> Option<&mut (dyn OwnerSolicitor<OAuthRequest> + Send)> {
if let Some(solicitor) = self.solicitor.as_deref_mut() {
Some(solicitor)
} else {
None
}
}

fn scopes(&mut self) -> Option<&mut dyn Scopes<OAuthRequest>> {
None
}

fn response(
&mut self, _request: &mut OAuthRequest, _kind: Template<'_>,
) -> Result<OAuthResponse, Self::Error> {
Ok(Default::default())
}

fn error(&mut self, err: OAuthError) -> Self::Error {
err.into()
}

fn web_error(&mut self, _err: WebError) -> Self::Error {
unreachable!()
}
}

#[async_trait]
impl Registrar for DbEndpoint {
async fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
let client = match self.find_client_by_id(&bound.client_id).await {
Ok(Some(client)) => client,
_ => return Err(RegistrarError::Unspecified),
};

Ok(BoundClient {
client_id: bound.client_id,
redirect_uri: Cow::Owned(client.redirect_uri),
})
}

async fn negotiate<'a>(
&self, bound: BoundClient<'a>, _scope: Option<Scope>,
) -> Result<PreGrant, RegistrarError> {
let client = match self.find_client_by_id(&bound.client_id).await {
Ok(Some(client)) => client,
_ => return Err(RegistrarError::Unspecified),
};

Ok(PreGrant {
client_id: bound.client_id.into_owned(),
redirect_uri: bound.redirect_uri.into_owned(),
scope: client.default_scope,
})
}

async fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
let client = match self.find_client_by_id(client_id).await {
Ok(Some(client)) => client,
_ => return Err(RegistrarError::Unspecified),
};

RegisteredClient::new(&client, &CheckSecret).check_authentication(passphrase)?;

Ok(())
}
}

#[derive(Clone, Debug, Default)]
struct CheckSecret;

impl PasswordPolicy for CheckSecret {
fn store(&self, _client_id: &str, _passphrase: &[u8]) -> Vec<u8> {
unreachable!()
}

fn check(&self, _client_id: &str, passphrase: &[u8], stored: &[u8]) -> Result<(), RegistrarError> {
if stored == passphrase {
Ok(())
} else {
Err(RegistrarError::Unspecified)
}
}
}

#[async_trait]
impl Authorizer for DbEndpoint {
async fn authorize(&mut self, grant: Grant) -> Result<String, ()> {
let Grant { client_id, .. } = grant;

let app = match self.find_app_by_uid(&client_id).await {
Ok(Some(app)) => app,
_ => return Err(()),
};

Ok(app.token())
}

async fn extract(&mut self, token: &str) -> Result<Option<Grant>, ()> {
let id = match token.strip_prefix("token") {
Some(id) => id,
None => return Ok(None),
};

let app = match self.find_app_by_uid(&id).await {
Ok(Some(client)) => client,
_ => return Ok(None),
};

Ok(Some(Grant {
owner_id: app.uid.clone(),
client_id: app.uid.clone(),
redirect_uri: Lazy::force(&REDIRECT_URI).clone().into(),
scope: Lazy::force(&DEFAULT_SCOPES).clone(),
until: Utc::now() + Duration::minutes(10),
extensions: Default::default(),
}))
}
}

#[async_trait]
impl Issuer for DbEndpoint {
async fn issue(&mut self, grant: Grant) -> Result<IssuedToken, ()> {
let Grant { client_id, until, .. } = grant;

let app = match self.find_app_by_uid(&client_id).await {
Ok(Some(app)) => app,
_ => return Err(()),
};

Ok(IssuedToken {
token: format!("token{}", app.id),
refresh: None,
until,
token_type: TokenType::Bearer,
})
}

async fn refresh(&mut self, _refresh: &str, _grant: Grant) -> Result<RefreshedToken, ()> {
Err(())
}

async fn recover_token(&mut self, _: &str) -> Result<Option<Grant>, ()> {
Ok(None)
}

async fn recover_refresh(&mut self, _: &str) -> Result<Option<Grant>, ()> {
Ok(None)
}
}
Loading