BLOG-85 Implement OIDC authentication #93

Merged
squid merged 8 commits from BLOG-85_oidc_login into main 2025-07-30 03:46:50 +08:00
7 changed files with 139 additions and 69 deletions
Showing only changes of commit a9655edff6 - Show all commits

View File

@ -1,18 +1,33 @@
use openidconnect::reqwest; use openidconnect::reqwest;
use crate::configuration::oidc::OidcConfiguration; use crate::configuration::{
db::DbConfiguration, oidc::OidcConfiguration, server::ServerConfiguration,
session::SessionConfiguration, storage::StorageConfiguration,
};
pub mod db;
pub mod oidc; pub mod oidc;
pub mod server;
pub mod session;
pub mod storage;
#[derive(Clone)] #[derive(Clone)]
pub struct Configuration { pub struct Configuration {
pub oidc_configuration: OidcConfiguration, pub db: DbConfiguration,
pub oidc: OidcConfiguration,
pub server: ServerConfiguration,
pub session: SessionConfiguration,
pub storage: StorageConfiguration,
} }
impl Configuration { impl Configuration {
pub async fn new(http_client: reqwest::Client) -> Self { pub async fn new(http_client: reqwest::Client) -> Self {
Self { Self {
oidc_configuration: OidcConfiguration::new(http_client).await, db: DbConfiguration::new(),
oidc: OidcConfiguration::new(http_client).await,
server: ServerConfiguration::new(),
session: SessionConfiguration::new(),
storage: StorageConfiguration::new(),
} }
} }
} }

View File

@ -0,0 +1,44 @@
use std::env;
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
#[derive(Clone)]
pub struct DbConfiguration {
pub database_url: String,
}
impl DbConfiguration {
pub fn new() -> Self {
let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = env::var("DATABASE_PORT").unwrap_or_else(|_| "5432".to_string());
let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string());
let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string());
let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string());
let encoded_password =
percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC)
.to_string();
let database_url = format!(
"postgres://{}:{}@{}:{}/{}",
user, encoded_password, host, port, dbname
);
Self { database_url }
}
pub async fn create_connection(&self) -> Pool<Postgres> {
let db_pool = PgPoolOptions::new()
.max_connections(5)
.connect(&self.database_url)
.await
.expect("Failed to create database connection pool");
sqlx::migrate!("../migrations")
.run(&db_pool)
.await
.expect("Failed to run database migrations");
db_pool
}
}

View File

@ -0,0 +1,17 @@
#[derive(Clone)]
pub struct ServerConfiguration {
pub host: String,
pub port: u16,
}
impl ServerConfiguration {
pub fn new() -> Self {
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = std::env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse::<u16>()
.unwrap();
Self { host, port }
}
}

View File

@ -0,0 +1,36 @@
use actix_session::storage::RedisSessionStore;
use actix_web::cookie::Key;
#[derive(Clone)]
pub struct SessionConfiguration {
pub session_key: Key,
pub redis_url: String,
}
impl SessionConfiguration {
pub fn new() -> Self {
let session_key_hex = std::env::var("SESSION_KEY").expect("SESSION_KEY must be set");
let session_key_bytes =
hex::decode(session_key_hex).expect("Invalid SESSION_KEY format, must be hex encoded");
if session_key_bytes.len() != 64 {
panic!("SESSION_KEY must be 64 bytes long");
}
let session_key = Key::from(&session_key_bytes);
let redis_url =
std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.1:6379".to_string());
Self {
session_key,
redis_url,
}
}
pub async fn create_session_store(&self) -> RedisSessionStore {
RedisSessionStore::new(self.redis_url.clone())
.await
.expect("Failed to create Redis session store")
}
}

View File

@ -0,0 +1,13 @@
use std::env;
#[derive(Clone)]
pub struct StorageConfiguration {
pub storage_path: String,
}
impl StorageConfiguration {
pub fn new() -> Self {
let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string());
Self { storage_path }
}
}

View File

@ -49,11 +49,10 @@ pub struct Container {
impl Container { impl Container {
pub fn new( pub fn new(
db_pool: Pool<Postgres>, db_pool: Pool<Postgres>,
storage_path: &str,
http_client: reqwest::Client, http_client: reqwest::Client,
configuration: Configuration, configuration: Configuration,
) -> Self { ) -> Self {
let oidc_configuration = &configuration.oidc_configuration; let oidc_configuration = &configuration.oidc;
let auth_oidc_service = Arc::new(AuthOidcServiceImpl::new( let auth_oidc_service = Arc::new(AuthOidcServiceImpl::new(
oidc_configuration.provider_metadata.clone(), oidc_configuration.provider_metadata.clone(),
&oidc_configuration.client_id, &oidc_configuration.client_id,
@ -81,7 +80,7 @@ impl Container {
)); ));
let image_db_service = Arc::new(ImageDbServiceImpl::new(db_pool.clone())); let image_db_service = Arc::new(ImageDbServiceImpl::new(db_pool.clone()));
let image_storage = Arc::new(ImageStorageImpl::new(storage_path.into())); let image_storage = Arc::new(ImageStorageImpl::new(&configuration.storage.storage_path));
let image_repository = Arc::new(ImageRepositoryImpl::new( let image_repository = Arc::new(ImageRepositoryImpl::new(
image_db_service.clone(), image_db_service.clone(),
image_storage.clone(), image_storage.clone(),

View File

@ -4,7 +4,6 @@ use actix_session::{
use actix_web::{ use actix_web::{
App, Error, HttpServer, App, Error, HttpServer,
body::MessageBody, body::MessageBody,
cookie,
dev::{ServiceFactory, ServiceRequest, ServiceResponse}, dev::{ServiceFactory, ServiceRequest, ServiceResponse},
web, web,
}; };
@ -13,36 +12,30 @@ use image::framework::web::image_web_routes::configure_image_routes;
use openidconnect::reqwest; use openidconnect::reqwest;
use post::framework::web::post_web_routes::configure_post_routes; use post::framework::web::post_web_routes::configure_post_routes;
use server::{configuration::Configuration, container::Container}; use server::{configuration::Configuration, container::Container};
use sqlx::{Pool, Postgres, postgres::PgPoolOptions}; use sqlx::{Pool, Postgres};
use std::env;
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
env_logger::init(); env_logger::init();
let db_pool = init_database().await;
let storage_path = env::var("STORAGE_PATH").unwrap_or_else(|_| "static".to_string());
let http_client = reqwest::ClientBuilder::new() let http_client = reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none()) .redirect(reqwest::redirect::Policy::none())
.build() .build()
.expect("Failed to create HTTP client"); .expect("Failed to create HTTP client");
let session_store = init_session_store().await;
let session_key = init_session_key().await;
let host = env::var("HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
let port = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse::<u16>()
.unwrap();
let configuration = Configuration::new(http_client.clone()).await; let configuration = Configuration::new(http_client.clone()).await;
let host = configuration.server.host.clone();
let port = configuration.server.port;
let db_pool = configuration.db.create_connection().await;
let session_key = configuration.session.session_key.clone();
let session_store = configuration.session.create_session_store().await;
HttpServer::new(move || { HttpServer::new(move || {
create_app( create_app(
db_pool.clone(), db_pool.clone(),
storage_path.clone(),
http_client.clone(), http_client.clone(),
SessionMiddleware::builder(session_store.clone(), session_key.clone()), SessionMiddleware::builder(session_store.clone(), session_key.clone()),
configuration.clone(), configuration.clone(),
@ -53,55 +46,8 @@ async fn main() -> std::io::Result<()> {
.await .await
} }
async fn init_database() -> Pool<Postgres> {
let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = env::var("DATABASE_PORT").unwrap_or_else(|_| "5432".to_string());
let user = env::var("DATABASE_USER").unwrap_or_else(|_| "postgres".to_string());
let password = env::var("DATABASE_PASSWORD").unwrap_or_else(|_| "".to_string());
let dbname = env::var("DATABASE_NAME").unwrap_or_else(|_| "postgres".to_string());
let encoded_password =
percent_encoding::utf8_percent_encode(&password, percent_encoding::NON_ALPHANUMERIC)
.to_string();
let database_url = format!(
"postgres://{}:{}@{}:{}/{}",
user, encoded_password, host, port, dbname
);
let db_pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to create database connection pool");
sqlx::migrate!("../migrations")
.run(&db_pool)
.await
.expect("Failed to run database migrations");
db_pool
}
async fn init_session_store() -> RedisSessionStore {
let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.1:6379".to_string());
RedisSessionStore::new(redis_url)
.await
.expect("Failed to create Redis session store")
}
async fn init_session_key() -> cookie::Key {
let session_key_hex = env::var("SESSION_KEY").expect("SESSION_KEY must be set");
let session_key_bytes =
hex::decode(session_key_hex).expect("Invalid SESSION_KEY format, must be hex encoded");
if session_key_bytes.len() != 64 {
panic!("SESSION_KEY must be 64 bytes long");
}
cookie::Key::from(&session_key_bytes)
}
fn create_app( fn create_app(
db_pool: Pool<Postgres>, db_pool: Pool<Postgres>,
storage_path: String,
http_client: reqwest::Client, http_client: reqwest::Client,
session_middleware_builder: SessionMiddlewareBuilder<RedisSessionStore>, session_middleware_builder: SessionMiddlewareBuilder<RedisSessionStore>,
configuration: Configuration, configuration: Configuration,
@ -114,7 +60,7 @@ fn create_app(
Error = Error, Error = Error,
>, >,
> { > {
let container = Container::new(db_pool, &storage_path, http_client, configuration); let container = Container::new(db_pool, http_client, configuration);
App::new() App::new()
.wrap(session_middleware_builder.build()) .wrap(session_middleware_builder.build())