Skip to content

Commit b6d941d

Browse files
feat: bootstrap baffao bff oauth implementation
1 parent 7718e2d commit b6d941d

20 files changed

Lines changed: 580 additions & 1 deletion

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
debug/
4+
target/
5+
6+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
7+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8+
Cargo.lock
9+
10+
# These are backup files generated by rustfmt
11+
**/*.rs.bk
12+
13+
# MSVC Windows builds of rustc generate these, which store debugging information
14+
*.pdb

Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[workspace]
2+
resolver = "2"
3+
4+
members = [
5+
"baffao-core",
6+
"baffao-proxy",
7+
]

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
# Baffao : The BAckend For Frontend Authx Oriented
1+
# Baffao: The BAckend For Frontend Authentication and Authorization Oriented
2+
3+
Baffao is a lightweight component which implement the Backend For Frontend (BFF) pattern and provides authentication and authorization features for web applications. More specifically, it provides a more secure and efficient way to perform OAuth2 and OpenID Connect flows.
4+
5+
## References
6+
7+
- [IETF OAuth 2.0 for Browser-Based Apps](https://github.com/oauth-wg/oauth-browser-based-apps)

baffao-core/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "baffao-core"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
anyhow = "1.0.80"
10+
axum-extra = { version = "0.9.2", features = ["cookie-private"] }
11+
config = "0.14.0"
12+
cookie = "0.18.0"
13+
jsonwebtoken = "9.2.0"
14+
oauth2 = "4.4.2"
15+
reqwest = "0.11.24"
16+
serde = "1.0.197"

baffao-core/src/cookies.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use cookie::Cookie;
2+
use crate::settings;
3+
4+
pub fn new_cookie(
5+
config: settings::CookieConfig,
6+
value: String,
7+
) -> Cookie<'static> {
8+
Cookie::build((config.name, value))
9+
.domain(config.domain)
10+
.path("/")
11+
.secure(config.secure)
12+
.http_only(config.http_only)
13+
.same_site(config.same_site)
14+
.build()
15+
}

baffao-core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod oauth;
2+
pub mod cookies;
3+
pub mod settings;

baffao-core/src/oauth/authorize.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
use anyhow::{Error, Ok};
2+
use axum_extra::extract::CookieJar;
3+
use serde::Deserialize;
4+
5+
use crate::{cookies::new_cookie, oauth::client::OAuthClient, settings::CookiesConfig};
6+
7+
#[derive(Debug, Deserialize)]
8+
pub struct AuthorizationQuery {
9+
pub scope: Option<String>,
10+
}
11+
12+
pub fn oauth2_authorize(
13+
jar: CookieJar,
14+
query: Option<AuthorizationQuery>,
15+
client: OAuthClient,
16+
CookiesConfig {
17+
csrf: csrf_cookie, ..
18+
}: CookiesConfig,
19+
) -> Result<(CookieJar, String), Error> {
20+
let (url, csrf_token) = client.get_authorization_url(
21+
query
22+
.map(|q| q.scope.unwrap_or_default())
23+
.unwrap_or_default()
24+
.split_whitespace()
25+
.map(|s| s.to_string())
26+
.collect(),
27+
);
28+
29+
Ok((
30+
jar.add(new_cookie(csrf_cookie, csrf_token.secret().to_string())),
31+
url.to_string(),
32+
))
33+
}

baffao-core/src/oauth/callback.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use anyhow::{Error, Ok};
2+
use axum_extra::extract::CookieJar;
3+
use serde::Deserialize;
4+
5+
use crate::{cookies::new_cookie, oauth::client::OAuthClient, settings::CookiesConfig};
6+
7+
#[derive(Debug, Deserialize, Default)]
8+
pub struct AuthorizationCallbackQuery {
9+
pub code: String,
10+
pub state: String,
11+
}
12+
13+
pub async fn oauth2_callback(
14+
jar: CookieJar,
15+
query: AuthorizationCallbackQuery,
16+
client: OAuthClient,
17+
CookiesConfig {
18+
csrf: csrf_cookie,
19+
access_token: access_token_cookie,
20+
refresh_token: refresh_token_cookie,
21+
..
22+
}: CookiesConfig,
23+
) -> Result<(CookieJar, String), Error> {
24+
let pkce_code = jar
25+
.get(csrf_cookie.name.as_str())
26+
.map(|cookie| cookie.value().to_string())
27+
.unwrap_or_default();
28+
let (access_token, refresh_token, _expires) = client
29+
.exchange_code(query.code, pkce_code, query.state.clone())
30+
.await
31+
.unwrap();
32+
33+
let mut new_jar = jar.remove(csrf_cookie.name).add(new_cookie(
34+
access_token_cookie,
35+
access_token.secret().to_string(),
36+
));
37+
if let Some(refresh_token) = refresh_token {
38+
new_jar = new_jar.add(new_cookie(
39+
refresh_token_cookie,
40+
refresh_token.secret().to_string(),
41+
));
42+
}
43+
44+
Ok((new_jar, "/".to_string()))
45+
}

baffao-core/src/oauth/client.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use std::time::Duration;
2+
3+
use anyhow::Context;
4+
use oauth2::{
5+
basic::{BasicClient, BasicTokenType}, reqwest::async_http_client, AccessToken, AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, RefreshToken, Scope, StandardErrorResponse, TokenResponse, TokenUrl
6+
};
7+
use reqwest::Url;
8+
9+
use crate::settings::OAuthConfig;
10+
11+
#[derive(Debug)]
12+
pub struct OAuthClient {
13+
config: OAuthConfig,
14+
client: BasicClient,
15+
}
16+
17+
impl Clone for OAuthClient {
18+
fn clone(&self) -> Self {
19+
OAuthClient {
20+
config: self.config.clone(),
21+
client: self.client.clone(),
22+
}
23+
}
24+
}
25+
26+
impl OAuthClient {
27+
pub fn new(config: OAuthConfig) -> Self {
28+
let client = BasicClient::new(
29+
ClientId::new(config.client_id.clone()),
30+
Some(ClientSecret::new(config.client_secret.clone())),
31+
AuthUrl::new(config.authorization_url.clone()).unwrap(),
32+
Some(TokenUrl::new(config.token_url.clone()).unwrap()),
33+
)
34+
.set_auth_type(AuthType::RequestBody)
35+
.set_redirect_uri(RedirectUrl::new(config.authorization_redirect_uri.clone()).unwrap());
36+
37+
Self { config, client }
38+
}
39+
40+
pub fn get_authorization_url(&self, scope: Vec<String>) -> (Url, CsrfToken) {
41+
let mut request = self.client.authorize_url(CsrfToken::new_random);
42+
if !scope.is_empty() {
43+
request = request.add_scope(Scope::new(scope.join(" ")));
44+
}
45+
46+
let (auth_url, csrf_token) = request.url();
47+
(auth_url, csrf_token)
48+
}
49+
50+
pub async fn exchange_code(
51+
&self,
52+
code: String,
53+
csrf_token: String,
54+
state: String,
55+
) -> Result<(AccessToken, Option<RefreshToken>, Option<Duration>), anyhow::Error> {
56+
if state != csrf_token {
57+
return Err(anyhow::anyhow!("Invalid state"));
58+
}
59+
60+
let code = AuthorizationCode::new(code);
61+
let token = self.client
62+
.exchange_code(code)
63+
.request_async(async_http_client)
64+
.await
65+
.context("Failed to exchange code")?;
66+
67+
Ok((token.access_token().clone(), token.refresh_token().cloned(), token.expires_in()))
68+
}
69+
}

baffao-core/src/oauth/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod authorize;
2+
pub mod callback;
3+
pub mod client;

0 commit comments

Comments
 (0)