Skip to content

Commit

Permalink
feat: bootstrap baffao bff oauth implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelgautier committed Mar 11, 2024
1 parent 7718e2d commit 1109cd6
Show file tree
Hide file tree
Showing 21 changed files with 586 additions and 1 deletion.
14 changes: 14 additions & 0 deletions .gitignore
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
6 changes: 6 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"rust-analyzer.linkedProjects": [
"./baffao-core/Cargo.toml",
"./baffao-core/Cargo.toml"
]
}
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[workspace]
resolver = "2"

members = [
"baffao-core",
"baffao-proxy",
]
8 changes: 7 additions & 1 deletion README.md
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)
16 changes: 16 additions & 0 deletions baffao-core/Cargo.toml
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"
15 changes: 15 additions & 0 deletions baffao-core/src/cookies.rs
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()
}
3 changes: 3 additions & 0 deletions baffao-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod oauth;
pub mod cookies;
pub mod settings;
33 changes: 33 additions & 0 deletions baffao-core/src/oauth/authorize.rs
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(),
))
}
45 changes: 45 additions & 0 deletions baffao-core/src/oauth/callback.rs
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()))
}
69 changes: 69 additions & 0 deletions baffao-core/src/oauth/client.rs
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()))
}
}
3 changes: 3 additions & 0 deletions baffao-core/src/oauth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod authorize;
pub mod callback;
pub mod client;
98 changes: 98 additions & 0 deletions baffao-core/src/settings.rs
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,
}
20 changes: 20 additions & 0 deletions baffao-proxy/Cargo.toml
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"] }
1 change: 1 addition & 0 deletions baffao-proxy/config/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
local.*
29 changes: 29 additions & 0 deletions baffao-proxy/config/default.toml
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 = "/"
Loading

0 comments on commit 1109cd6

Please sign in to comment.