Skip to content

Commit 35400c1

Browse files
Add initial actix async example with a sqlx database
1 parent a62474c commit 35400c1

File tree

3 files changed

+307
-60
lines changed

3 files changed

+307
-60
lines changed

oxide-auth-async-actix/examples/async-actix-example/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ edition = "2018"
88
actix = "0.13"
99
actix-web = "4.2.1"
1010
actix-web-actors = "4.2.0"
11+
anyhow = "1.0.71"
1112
env_logger = "0.9"
1213
futures = "0.3"
1314
oxide-auth = { version = "0.5.0", path = "./../../../oxide-auth" }
15+
oxide-auth-actix = { version = "0.2.0", path = "./../../../oxide-auth-actix" }
1416
oxide-auth-async = { version = "0.1.0", path = "./../../../oxide-auth-async" }
1517
oxide-auth-async-actix = { version = "0.1.0", path = "./../../" }
1618
reqwest = { version = "0.11.10", features = ["blocking"] }
1719
serde = "1.0"
1820
serde_json = "1.0"
21+
sqlx = { version = "0.6.3", features = ["sqlite", "offline", "runtime-actix-native-tls"] }
1922
url = "2"
2023
serde_urlencoded = "0.7"
2124
tokio = "1.16.1"
25+
async-trait = "0.1.68"
26+
once_cell = "1.17.1"
27+
chrono = "0.4.24"
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
use anyhow::Result;
2+
use async_trait::async_trait;
3+
use chrono::{Duration, offset::Utc};
4+
use once_cell::sync::Lazy;
5+
use oxide_auth::{
6+
endpoint::{OAuthError, Scopes, Template},
7+
primitives::{
8+
grant::Grant,
9+
issuer::{IssuedToken, RefreshedToken, TokenType},
10+
scope::Scope,
11+
registrar::{
12+
BoundClient, ClientType, ClientUrl, EncodedClient, PasswordPolicy, PreGrant,
13+
RegisteredClient, RegistrarError, RegisteredUrl,
14+
},
15+
},
16+
};
17+
use oxide_auth_async::{
18+
endpoint::{Endpoint, OwnerSolicitor},
19+
primitives::{Authorizer, Issuer, Registrar},
20+
};
21+
use oxide_auth_async_actix::{OAuthRequest, OAuthResponse, WebError};
22+
23+
use std::{sync::Arc, borrow::Cow};
24+
use sqlx::{self, sqlite::SqlitePool, FromRow};
25+
use url::Url;
26+
27+
pub struct DbEndpoint {
28+
pool: Arc<SqlitePool>,
29+
solicitor: Option<Box<dyn OwnerSolicitor<OAuthRequest> + Send + Sync>>,
30+
}
31+
32+
#[derive(FromRow)]
33+
pub struct App {
34+
id: i32,
35+
uid: String,
36+
secret: String,
37+
}
38+
39+
impl App {
40+
fn token(&self) -> String {
41+
format!("token{}", self.id)
42+
}
43+
}
44+
45+
static REDIRECT_URI: Lazy<RegisteredUrl> =
46+
Lazy::new(|| "http://localhost:8021".parse::<Url>().unwrap().into());
47+
48+
static DEFAULT_SCOPES: Lazy<Scope> = Lazy::new(|| "read write".parse::<Scope>().unwrap());
49+
50+
impl DbEndpoint {
51+
pub async fn create() -> Result<Self> {
52+
let pool = SqlitePool::connect("sqlite::memory:").await?;
53+
54+
let mut conn = pool.acquire().await?;
55+
56+
sqlx::query(
57+
"CREATE TABLE IF NOT EXISTS apps (
58+
id INTEGER PRIMARY KEY NOT NULL,
59+
uid VARCHAR(250) NOT NULL,
60+
secret VARCHAR(250) NOT NULL
61+
);",
62+
)
63+
.execute(&mut conn)
64+
.await?;
65+
sqlx::query(
66+
"INSERT INTO apps (uid, secret)
67+
VALUES (?, ?);",
68+
)
69+
.bind("clienta")
70+
.bind("secreta")
71+
.execute(&mut conn)
72+
.await?;
73+
74+
drop(conn);
75+
76+
Ok(Self {
77+
pool: Arc::new(pool),
78+
solicitor: None,
79+
})
80+
}
81+
82+
pub fn with_solicitor<S>(&self, solicitor: S) -> Self
83+
where
84+
S: OwnerSolicitor<OAuthRequest> + Send + Sync + 'static,
85+
{
86+
Self {
87+
pool: self.pool.clone(),
88+
solicitor: Some(Box::new(solicitor)),
89+
}
90+
}
91+
92+
async fn find_app_by_uid(&self, uid: &str) -> Result<Option<App>> {
93+
let mut conn = self.pool.acquire().await?;
94+
95+
let app_opt = sqlx::query_as::<_, App>("SELECT * FROM apps WHERE uid = ?")
96+
.bind(uid)
97+
.fetch_optional(&mut conn)
98+
.await?;
99+
100+
Ok(app_opt)
101+
}
102+
103+
pub async fn find_client_by_id(&self, client_id: &str) -> Result<Option<EncodedClient>> {
104+
let app_opt = self.find_app_by_uid(client_id).await?;
105+
106+
Ok(app_opt.map(|app| EncodedClient {
107+
client_id: app.uid,
108+
redirect_uri: Lazy::force(&REDIRECT_URI).clone(),
109+
additional_redirect_uris: Default::default(),
110+
default_scope: Lazy::force(&DEFAULT_SCOPES).clone(),
111+
encoded_client: ClientType::Confidential {
112+
passdata: app.secret.into_bytes(),
113+
},
114+
}))
115+
}
116+
}
117+
118+
impl Endpoint<OAuthRequest> for DbEndpoint {
119+
type Error = OAuthError;
120+
121+
fn registrar(&self) -> Option<&(dyn Registrar + Sync)> {
122+
Some(self)
123+
}
124+
125+
fn authorizer_mut(&mut self) -> Option<&mut (dyn Authorizer + Send)> {
126+
Some(self)
127+
}
128+
129+
fn issuer_mut(&mut self) -> Option<&mut (dyn Issuer + Send)> {
130+
Some(self)
131+
}
132+
133+
fn owner_solicitor(&mut self) -> Option<&mut (dyn OwnerSolicitor<OAuthRequest> + Send)> {
134+
if let Some(solicitor) = self.solicitor.as_deref_mut() {
135+
Some(solicitor)
136+
} else {
137+
None
138+
}
139+
}
140+
141+
fn scopes(&mut self) -> Option<&mut dyn Scopes<OAuthRequest>> {
142+
None
143+
}
144+
145+
fn response(
146+
&mut self, _request: &mut OAuthRequest, _kind: Template<'_>,
147+
) -> Result<OAuthResponse, Self::Error> {
148+
Ok(Default::default())
149+
}
150+
151+
fn error(&mut self, err: OAuthError) -> Self::Error {
152+
err.into()
153+
}
154+
155+
fn web_error(&mut self, _err: WebError) -> Self::Error {
156+
unreachable!()
157+
}
158+
}
159+
160+
#[async_trait]
161+
impl Registrar for DbEndpoint {
162+
async fn bound_redirect<'a>(&self, bound: ClientUrl<'a>) -> Result<BoundClient<'a>, RegistrarError> {
163+
let client = match self.find_client_by_id(&bound.client_id).await {
164+
Ok(Some(client)) => client,
165+
_ => return Err(RegistrarError::Unspecified),
166+
};
167+
168+
Ok(BoundClient {
169+
client_id: bound.client_id,
170+
redirect_uri: Cow::Owned(client.redirect_uri),
171+
})
172+
}
173+
174+
async fn negotiate<'a>(
175+
&self, bound: BoundClient<'a>, _scope: Option<Scope>,
176+
) -> Result<PreGrant, RegistrarError> {
177+
let client = match self.find_client_by_id(&bound.client_id).await {
178+
Ok(Some(client)) => client,
179+
_ => return Err(RegistrarError::Unspecified),
180+
};
181+
182+
Ok(PreGrant {
183+
client_id: bound.client_id.into_owned(),
184+
redirect_uri: bound.redirect_uri.into_owned(),
185+
scope: client.default_scope,
186+
})
187+
}
188+
189+
async fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
190+
let client = match self.find_client_by_id(client_id).await {
191+
Ok(Some(client)) => client,
192+
_ => return Err(RegistrarError::Unspecified),
193+
};
194+
195+
RegisteredClient::new(&client, &CheckSecret).check_authentication(passphrase)?;
196+
197+
Ok(())
198+
}
199+
}
200+
201+
#[derive(Clone, Debug, Default)]
202+
struct CheckSecret;
203+
204+
impl PasswordPolicy for CheckSecret {
205+
fn store(&self, _client_id: &str, _passphrase: &[u8]) -> Vec<u8> {
206+
unreachable!()
207+
}
208+
209+
fn check(&self, _client_id: &str, passphrase: &[u8], stored: &[u8]) -> Result<(), RegistrarError> {
210+
if stored == passphrase {
211+
Ok(())
212+
} else {
213+
Err(RegistrarError::Unspecified)
214+
}
215+
}
216+
}
217+
218+
#[async_trait]
219+
impl Authorizer for DbEndpoint {
220+
async fn authorize(&mut self, grant: Grant) -> Result<String, ()> {
221+
let Grant { client_id, .. } = grant;
222+
223+
let app = match self.find_app_by_uid(&client_id).await {
224+
Ok(Some(app)) => app,
225+
_ => return Err(()),
226+
};
227+
228+
Ok(app.token())
229+
}
230+
231+
async fn extract(&mut self, token: &str) -> Result<Option<Grant>, ()> {
232+
let id = match token.strip_prefix("token") {
233+
Some(id) => id,
234+
None => return Ok(None),
235+
};
236+
237+
let app = match self.find_app_by_uid(&id).await {
238+
Ok(Some(client)) => client,
239+
_ => return Ok(None),
240+
};
241+
242+
Ok(Some(Grant {
243+
owner_id: app.uid.clone(),
244+
client_id: app.uid.clone(),
245+
redirect_uri: Lazy::force(&REDIRECT_URI).clone().into(),
246+
scope: Lazy::force(&DEFAULT_SCOPES).clone(),
247+
until: Utc::now() + Duration::minutes(10),
248+
extensions: Default::default(),
249+
}))
250+
}
251+
}
252+
253+
#[async_trait]
254+
impl Issuer for DbEndpoint {
255+
async fn issue(&mut self, grant: Grant) -> Result<IssuedToken, ()> {
256+
let Grant { client_id, until, .. } = grant;
257+
258+
let app = match self.find_app_by_uid(&client_id).await {
259+
Ok(Some(app)) => app,
260+
_ => return Err(()),
261+
};
262+
263+
Ok(IssuedToken {
264+
token: format!("token{}", app.id),
265+
refresh: None,
266+
until,
267+
token_type: TokenType::Bearer,
268+
})
269+
}
270+
271+
async fn refresh(&mut self, _refresh: &str, _grant: Grant) -> Result<RefreshedToken, ()> {
272+
Err(())
273+
}
274+
275+
async fn recover_token(&mut self, _: &str) -> Result<Option<Grant>, ()> {
276+
Ok(None)
277+
}
278+
279+
async fn recover_refresh(&mut self, _: &str) -> Result<Option<Grant>, ()> {
280+
Ok(None)
281+
}
282+
}

0 commit comments

Comments
 (0)