-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: bootstrap baffao bff oauth implementation
- Loading branch information
1 parent
7718e2d
commit b6d941d
Showing
20 changed files
with
580 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Generated by Cargo | ||
# will have compiled files and executables | ||
debug/ | ||
target/ | ||
|
||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries | ||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html | ||
Cargo.lock | ||
|
||
# These are backup files generated by rustfmt | ||
**/*.rs.bk | ||
|
||
# MSVC Windows builds of rustc generate these, which store debugging information | ||
*.pdb |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
[workspace] | ||
resolver = "2" | ||
|
||
members = [ | ||
"baffao-core", | ||
"baffao-proxy", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,7 @@ | ||
# Baffao : The BAckend For Frontend Authx Oriented | ||
# Baffao: The BAckend For Frontend Authentication and Authorization Oriented | ||
|
||
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. | ||
|
||
## References | ||
|
||
- [IETF OAuth 2.0 for Browser-Based Apps](https://github.com/oauth-wg/oauth-browser-based-apps) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "baffao-core" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
anyhow = "1.0.80" | ||
axum-extra = { version = "0.9.2", features = ["cookie-private"] } | ||
config = "0.14.0" | ||
cookie = "0.18.0" | ||
jsonwebtoken = "9.2.0" | ||
oauth2 = "4.4.2" | ||
reqwest = "0.11.24" | ||
serde = "1.0.197" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
use cookie::Cookie; | ||
use crate::settings; | ||
|
||
pub fn new_cookie( | ||
config: settings::CookieConfig, | ||
value: String, | ||
) -> Cookie<'static> { | ||
Cookie::build((config.name, value)) | ||
.domain(config.domain) | ||
.path("/") | ||
.secure(config.secure) | ||
.http_only(config.http_only) | ||
.same_site(config.same_site) | ||
.build() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub mod oauth; | ||
pub mod cookies; | ||
pub mod settings; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
use anyhow::{Error, Ok}; | ||
use axum_extra::extract::CookieJar; | ||
use serde::Deserialize; | ||
|
||
use crate::{cookies::new_cookie, oauth::client::OAuthClient, settings::CookiesConfig}; | ||
|
||
#[derive(Debug, Deserialize)] | ||
pub struct AuthorizationQuery { | ||
pub scope: Option<String>, | ||
} | ||
|
||
pub fn oauth2_authorize( | ||
jar: CookieJar, | ||
query: Option<AuthorizationQuery>, | ||
client: OAuthClient, | ||
CookiesConfig { | ||
csrf: csrf_cookie, .. | ||
}: CookiesConfig, | ||
) -> Result<(CookieJar, String), Error> { | ||
let (url, csrf_token) = client.get_authorization_url( | ||
query | ||
.map(|q| q.scope.unwrap_or_default()) | ||
.unwrap_or_default() | ||
.split_whitespace() | ||
.map(|s| s.to_string()) | ||
.collect(), | ||
); | ||
|
||
Ok(( | ||
jar.add(new_cookie(csrf_cookie, csrf_token.secret().to_string())), | ||
url.to_string(), | ||
)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
use anyhow::{Error, Ok}; | ||
use axum_extra::extract::CookieJar; | ||
use serde::Deserialize; | ||
|
||
use crate::{cookies::new_cookie, oauth::client::OAuthClient, settings::CookiesConfig}; | ||
|
||
#[derive(Debug, Deserialize, Default)] | ||
pub struct AuthorizationCallbackQuery { | ||
pub code: String, | ||
pub state: String, | ||
} | ||
|
||
pub async fn oauth2_callback( | ||
jar: CookieJar, | ||
query: AuthorizationCallbackQuery, | ||
client: OAuthClient, | ||
CookiesConfig { | ||
csrf: csrf_cookie, | ||
access_token: access_token_cookie, | ||
refresh_token: refresh_token_cookie, | ||
.. | ||
}: CookiesConfig, | ||
) -> Result<(CookieJar, String), Error> { | ||
let pkce_code = jar | ||
.get(csrf_cookie.name.as_str()) | ||
.map(|cookie| cookie.value().to_string()) | ||
.unwrap_or_default(); | ||
let (access_token, refresh_token, _expires) = client | ||
.exchange_code(query.code, pkce_code, query.state.clone()) | ||
.await | ||
.unwrap(); | ||
|
||
let mut new_jar = jar.remove(csrf_cookie.name).add(new_cookie( | ||
access_token_cookie, | ||
access_token.secret().to_string(), | ||
)); | ||
if let Some(refresh_token) = refresh_token { | ||
new_jar = new_jar.add(new_cookie( | ||
refresh_token_cookie, | ||
refresh_token.secret().to_string(), | ||
)); | ||
} | ||
|
||
Ok((new_jar, "/".to_string())) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
use std::time::Duration; | ||
|
||
use anyhow::Context; | ||
use oauth2::{ | ||
basic::{BasicClient, BasicTokenType}, reqwest::async_http_client, AccessToken, AuthType, AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, RefreshToken, Scope, StandardErrorResponse, TokenResponse, TokenUrl | ||
}; | ||
use reqwest::Url; | ||
|
||
use crate::settings::OAuthConfig; | ||
|
||
#[derive(Debug)] | ||
pub struct OAuthClient { | ||
config: OAuthConfig, | ||
client: BasicClient, | ||
} | ||
|
||
impl Clone for OAuthClient { | ||
fn clone(&self) -> Self { | ||
OAuthClient { | ||
config: self.config.clone(), | ||
client: self.client.clone(), | ||
} | ||
} | ||
} | ||
|
||
impl OAuthClient { | ||
pub fn new(config: OAuthConfig) -> Self { | ||
let client = BasicClient::new( | ||
ClientId::new(config.client_id.clone()), | ||
Some(ClientSecret::new(config.client_secret.clone())), | ||
AuthUrl::new(config.authorization_url.clone()).unwrap(), | ||
Some(TokenUrl::new(config.token_url.clone()).unwrap()), | ||
) | ||
.set_auth_type(AuthType::RequestBody) | ||
.set_redirect_uri(RedirectUrl::new(config.authorization_redirect_uri.clone()).unwrap()); | ||
|
||
Self { config, client } | ||
} | ||
|
||
pub fn get_authorization_url(&self, scope: Vec<String>) -> (Url, CsrfToken) { | ||
let mut request = self.client.authorize_url(CsrfToken::new_random); | ||
if !scope.is_empty() { | ||
request = request.add_scope(Scope::new(scope.join(" "))); | ||
} | ||
|
||
let (auth_url, csrf_token) = request.url(); | ||
(auth_url, csrf_token) | ||
} | ||
|
||
pub async fn exchange_code( | ||
&self, | ||
code: String, | ||
csrf_token: String, | ||
state: String, | ||
) -> Result<(AccessToken, Option<RefreshToken>, Option<Duration>), anyhow::Error> { | ||
if state != csrf_token { | ||
return Err(anyhow::anyhow!("Invalid state")); | ||
} | ||
|
||
let code = AuthorizationCode::new(code); | ||
let token = self.client | ||
.exchange_code(code) | ||
.request_async(async_http_client) | ||
.await | ||
.context("Failed to exchange code")?; | ||
|
||
Ok((token.access_token().clone(), token.refresh_token().cloned(), token.expires_in())) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
pub mod authorize; | ||
pub mod callback; | ||
pub mod client; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
use axum_extra::extract::cookie::SameSite; | ||
use reqwest::Url; | ||
use serde::Deserialize; | ||
|
||
#[derive(Debug, Deserialize, Clone)] | ||
#[allow(unused)] | ||
pub struct ServerConfig { | ||
pub host: String, | ||
pub port: u16, | ||
pub base_url: String, | ||
pub cookies: CookiesConfig, | ||
} | ||
|
||
impl ServerConfig { | ||
pub fn base_url(&self) -> String { | ||
format!("{}:{}", self.host, self.port) | ||
} | ||
|
||
pub fn scheme(&self) -> String { | ||
Url::parse(&self.base_url).unwrap().scheme().to_string() | ||
} | ||
|
||
pub fn domain(&self) -> String { | ||
Url::parse(&self.base_url).unwrap().domain().unwrap().to_string() | ||
} | ||
} | ||
|
||
#[derive(Debug, Deserialize, Clone)] | ||
#[allow(unused)] | ||
pub struct OAuthConfig { | ||
pub client_id: String, | ||
pub client_secret: String, | ||
pub metadata_url: Option<String>, | ||
pub authorization_redirect_uri: String, | ||
pub authorization_url: String, | ||
pub token_url: String, | ||
pub userinfo_url: Option<String>, | ||
pub redirect_uri: Option<String>, | ||
} | ||
|
||
#[derive(Debug, Deserialize, Clone)] | ||
#[allow(unused)] | ||
pub struct JwtConfig { | ||
pub secret: String, | ||
pub issuer: String, | ||
} | ||
|
||
#[derive(Debug, Deserialize, Clone)] | ||
#[allow(unused)] | ||
pub struct CookieConfig { | ||
pub name: String, | ||
pub domain: String, | ||
pub secure: bool, | ||
pub http_only: bool, | ||
#[serde(deserialize_with = "deserialize_same_site")] | ||
pub same_site: SameSite, | ||
} | ||
|
||
fn deserialize_same_site<'de, D>(deserializer: D) -> Result<SameSite, D::Error> | ||
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
let s: String = serde::Deserialize::deserialize(deserializer)?; | ||
match s.to_lowercase().as_str() { | ||
"lax" => Ok(SameSite::Lax), | ||
"strict" => Ok(SameSite::Strict), | ||
"none" => Ok(SameSite::None), | ||
_ => Err(serde::de::Error::custom("invalid value for SameSite")), | ||
} | ||
} | ||
|
||
impl CookieConfig { | ||
pub fn to_string_with_value(&self, value: String) -> String { | ||
let mut cookie = format!( | ||
"{}={}; Domain={}; Path=/; SameSite={}", | ||
self.name, value, self.domain, self.same_site | ||
); | ||
|
||
if self.secure { | ||
cookie = format!("{}; Secure", cookie) | ||
} | ||
|
||
if self.http_only { | ||
cookie = format!("{}; HttpOnly", cookie) | ||
} | ||
|
||
cookie | ||
} | ||
} | ||
|
||
#[derive(Debug, Deserialize, Clone)] | ||
#[allow(unused)] | ||
pub struct CookiesConfig { | ||
pub csrf: CookieConfig, | ||
pub access_token: CookieConfig, | ||
pub refresh_token: CookieConfig, | ||
pub id_token: CookieConfig, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "baffao-proxy" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
axum = "0.7.4" | ||
axum-extra = { version = "0.9.2", features = ["typed-header", "cookie"] } | ||
baffao-core = { path = "../baffao-core" } | ||
config = "0.14.0" | ||
oauth2 = "4.4.2" | ||
serde = { version = "1.0", features = ["derive"] } | ||
tokio = { "version" = "1.36.0", features = ["full"] } | ||
tower = { version = "0.4", features = ["util", "timeout"] } | ||
tower-http = { version = "0.5.0", features = ["add-extension", "trace"] } | ||
tracing = "0.1" | ||
tracing-subscriber = { version = "0.3", features = ["env-filter"] } | ||
uuid = { version = "1.0", features = ["serde", "v4"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
local.* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
[server] | ||
[server.cookies] | ||
|
||
[server.cookies.csrf] | ||
name = "baffao.csrf_token" | ||
secure = true | ||
http_only = true | ||
same_site = "Strict" | ||
|
||
[server.cookies.access_token] | ||
name = "baffao.access_token" | ||
secure = true | ||
http_only = true | ||
same_site = "Strict" | ||
|
||
[server.cookies.refresh_token] | ||
name = "baffao.refresh_token" | ||
secure = true | ||
http_only = true | ||
same_site = "Strict" | ||
|
||
[server.cookies.id_token] | ||
name = "baffao.id_token" | ||
secure = true | ||
http_only = true | ||
same_site = "Strict" | ||
|
||
[oauth] | ||
redirect_uri = "/" |
Oops, something went wrong.