From 35400c176052cc999be73a3929e904b57a668f7f Mon Sep 17 00:00:00 2001 From: Francis Murillo Date: Sun, 14 May 2023 16:31:12 +0800 Subject: [PATCH] Add initial actix async example with a sqlx database --- .../examples/async-actix-example/Cargo.toml | 6 + .../async-actix-example/src/endpoint.rs | 282 ++++++++++++++++++ .../examples/async-actix-example/src/main.rs | 79 ++--- 3 files changed, 307 insertions(+), 60 deletions(-) create mode 100644 oxide-auth-async-actix/examples/async-actix-example/src/endpoint.rs diff --git a/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml b/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml index 7cbff535..096428b8 100644 --- a/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml +++ b/oxide-auth-async-actix/examples/async-actix-example/Cargo.toml @@ -8,14 +8,20 @@ edition = "2018" 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" diff --git a/oxide-auth-async-actix/examples/async-actix-example/src/endpoint.rs b/oxide-auth-async-actix/examples/async-actix-example/src/endpoint.rs new file mode 100644 index 00000000..847596bc --- /dev/null +++ b/oxide-auth-async-actix/examples/async-actix-example/src/endpoint.rs @@ -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, + solicitor: Option + 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 = + Lazy::new(|| "http://localhost:8021".parse::().unwrap().into()); + +static DEFAULT_SCOPES: Lazy = Lazy::new(|| "read write".parse::().unwrap()); + +impl DbEndpoint { + pub async fn create() -> Result { + 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(&self, solicitor: S) -> Self + where + S: OwnerSolicitor + Send + Sync + 'static, + { + Self { + pool: self.pool.clone(), + solicitor: Some(Box::new(solicitor)), + } + } + + async fn find_app_by_uid(&self, uid: &str) -> Result> { + 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> { + 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 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 + Send)> { + if let Some(solicitor) = self.solicitor.as_deref_mut() { + Some(solicitor) + } else { + None + } + } + + fn scopes(&mut self) -> Option<&mut dyn Scopes> { + None + } + + fn response( + &mut self, _request: &mut OAuthRequest, _kind: Template<'_>, + ) -> Result { + 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, 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, + ) -> Result { + 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 { + 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 { + 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, ()> { + 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 { + 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 { + Err(()) + } + + async fn recover_token(&mut self, _: &str) -> Result, ()> { + Ok(None) + } + + async fn recover_refresh(&mut self, _: &str) -> Result, ()> { + Ok(None) + } +} 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 index a0d77e95..a76b4271 100644 --- a/oxide-auth-async-actix/examples/async-actix-example/src/main.rs +++ b/oxide-auth-async-actix/examples/async-actix-example/src/main.rs @@ -1,3 +1,4 @@ +mod endpoint; mod support; use actix::{Actor, Addr, Context, Handler, ResponseFuture}; @@ -9,18 +10,16 @@ use actix_web::{ 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::endpoint::OwnerSolicitor; use oxide_auth_async_actix::{ Authorize, OAuthMessage, OAuthOperation, OAuthRequest, OAuthResource, OAuthResponse, Refresh, Resource, Token, WebError, }; use std::thread; +use endpoint::DbEndpoint; + static DENY_TEXT: &str = " This page should be accessed via an oauth token from the client in the example. Click @@ -29,14 +28,7 @@ here to begin the authorization process. "; struct State { - endpoint: Generic< - ClientMap, - AuthMap, - TokenMap, - Vacant, - Vec, - fn() -> OAuthResponse, - >, + endpoint: DbEndpoint, } enum Extras { @@ -111,7 +103,7 @@ pub async fn main() -> std::io::Result<()> { // Start, then open in browser, don't care about this finishing. rt::spawn(start_browser()); - let state = State::preconfigured().start(); + let state = State::preconfigured().await.start(); // Create the main server instance let server = HttpServer::new(move || { @@ -138,55 +130,17 @@ pub async fn main() -> std::io::Result<()> { } impl State { - pub fn preconfigured() -> Self { + pub async 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, - }, + endpoint: DbEndpoint::create().await.unwrap(), } } - pub fn with_solicitor<'a, S: Send + Sync>( - &'a mut self, solicitor: S, - ) -> impl Endpoint + 'a + pub fn with_solicitor<'a, S: Send + Sync>(&'a mut self, solicitor: S) -> DbEndpoint where - S: OwnerSolicitor + 'static, + S: OwnerSolicitor + Send + Sync + '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, - }) + self.endpoint.with_solicitor(solicitor) } } @@ -206,8 +160,6 @@ where 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") @@ -229,7 +181,14 @@ where op.run(self.with_solicitor(solicitor)) } - _ => op.run(&mut self.endpoint), + Extras::ClientCredentials => { + let solicitor = FnSolicitor(move |_: &mut OAuthRequest, solicitation: Solicitation| { + OwnerConsent::Authorized(solicitation.pre_grant().client_id.clone()) + }); + + op.run(self.with_solicitor(solicitor)) + } + _ => op.run(self.with_solicitor(Vacant)), } } }