BLOG-94 Create user in DB when first login through OIDC #96
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@ -428,6 +428,7 @@ dependencies = [
|
||||
"log",
|
||||
"openidconnect",
|
||||
"serde",
|
||||
"sqlx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -10,3 +10,4 @@ async-trait.workspace = true
|
||||
log.workspace = true
|
||||
openidconnect.workspace = true
|
||||
serde.workspace = true
|
||||
sqlx.workspace = true
|
||||
|
@ -4,7 +4,7 @@ use crate::domain::entity::user::User;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UserResponseDto {
|
||||
pub source_id: String,
|
||||
pub id: i32,
|
||||
pub displayed_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
@ -12,7 +12,7 @@ pub struct UserResponseDto {
|
||||
impl From<User> for UserResponseDto {
|
||||
fn from(user: User) -> Self {
|
||||
UserResponseDto {
|
||||
source_id: user.source_id,
|
||||
id: user.id,
|
||||
displayed_name: user.displayed_name,
|
||||
email: user.email,
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
pub mod oidc_claims_response_dto;
|
||||
pub mod auth_oidc_service;
|
||||
pub mod auth_repository_impl;
|
||||
pub mod oidc_claims_response_dto;
|
||||
pub mod user_db_service;
|
||||
pub mod user_db_mapper;
|
||||
|
@ -3,7 +3,9 @@ use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{
|
||||
adapter::gateway::auth_oidc_service::AuthOidcService,
|
||||
adapter::gateway::{
|
||||
auth_oidc_service::AuthOidcService, user_db_service::UserDbService, user_db_mapper::UserMapper,
|
||||
},
|
||||
application::{
|
||||
error::auth_error::AuthError, gateway::auth_repository::AuthRepository,
|
||||
use_case::get_auth_url_use_case::AuthUrl,
|
||||
@ -12,12 +14,19 @@ use crate::{
|
||||
};
|
||||
|
||||
pub struct AuthRepositoryImpl {
|
||||
user_db_service: Arc<dyn UserDbService>,
|
||||
auth_oidc_service: Arc<dyn AuthOidcService>,
|
||||
}
|
||||
|
||||
impl AuthRepositoryImpl {
|
||||
pub fn new(auth_oidc_service: Arc<dyn AuthOidcService>) -> Self {
|
||||
Self { auth_oidc_service }
|
||||
pub fn new(
|
||||
user_db_service: Arc<dyn UserDbService>,
|
||||
auth_oidc_service: Arc<dyn AuthOidcService>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_db_service,
|
||||
auth_oidc_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,4 +46,21 @@ impl AuthRepository for AuthRepositoryImpl {
|
||||
.await
|
||||
.map(|dto| dto.into_entity())
|
||||
}
|
||||
|
||||
async fn get_user_by_source_id(
|
||||
&self,
|
||||
issuer: &str,
|
||||
source_id: &str,
|
||||
) -> Result<User, AuthError> {
|
||||
self.user_db_service
|
||||
.get_user_by_source_id(issuer, source_id)
|
||||
.await
|
||||
.map(|mapper| mapper.into_entity())
|
||||
}
|
||||
|
||||
async fn save_user(&self, user: User) -> Result<i32, AuthError> {
|
||||
self.user_db_service
|
||||
.create_user(UserMapper::from(user))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ use crate::domain::entity::user::User;
|
||||
|
||||
pub struct OidcClaimsResponseDto {
|
||||
pub sub: String,
|
||||
pub issuer: String,
|
||||
pub preferred_username: Option<String>,
|
||||
pub email: Option<String>,
|
||||
}
|
||||
@ -9,6 +10,8 @@ pub struct OidcClaimsResponseDto {
|
||||
impl OidcClaimsResponseDto {
|
||||
pub fn into_entity(self) -> User {
|
||||
User {
|
||||
id: -1,
|
||||
issuer: self.issuer,
|
||||
source_id: self.sub,
|
||||
displayed_name: self.preferred_username.unwrap_or_default(),
|
||||
email: self.email.unwrap_or_default(),
|
||||
|
33
backend/feature/auth/src/adapter/gateway/user_db_mapper.rs
Normal file
33
backend/feature/auth/src/adapter/gateway/user_db_mapper.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use crate::domain::entity::user::User;
|
||||
|
||||
pub struct UserMapper {
|
||||
pub id: i32,
|
||||
pub issuer: String,
|
||||
pub source_id: String,
|
||||
pub displayed_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl From<User> for UserMapper {
|
||||
fn from(user: User) -> Self {
|
||||
Self {
|
||||
id: user.id,
|
||||
issuer: user.issuer,
|
||||
source_id: user.source_id,
|
||||
displayed_name: user.displayed_name,
|
||||
email: user.email,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UserMapper {
|
||||
pub fn into_entity(self) -> User {
|
||||
User {
|
||||
id: self.id,
|
||||
issuer: self.issuer,
|
||||
source_id: self.source_id,
|
||||
displayed_name: self.displayed_name,
|
||||
email: self.email,
|
||||
}
|
||||
}
|
||||
}
|
14
backend/feature/auth/src/adapter/gateway/user_db_service.rs
Normal file
14
backend/feature/auth/src/adapter/gateway/user_db_service.rs
Normal file
@ -0,0 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{adapter::gateway::user_db_mapper::UserMapper, application::error::auth_error::AuthError};
|
||||
|
||||
#[async_trait]
|
||||
pub trait UserDbService: Send + Sync {
|
||||
async fn get_user_by_source_id(
|
||||
&self,
|
||||
issuer: &str,
|
||||
source_id: &str,
|
||||
) -> Result<UserMapper, AuthError>;
|
||||
|
||||
async fn create_user(&self, user: UserMapper) -> Result<i32, AuthError>;
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AuthError {
|
||||
DatabaseError(String),
|
||||
OidcError(String),
|
||||
InvalidState,
|
||||
InvalidNonce,
|
||||
InvalidAuthCode,
|
||||
InvalidIdToken,
|
||||
UserNotFound,
|
||||
}
|
||||
|
@ -8,6 +8,12 @@ use crate::{
|
||||
#[async_trait]
|
||||
pub trait AuthRepository: Send + Sync {
|
||||
fn get_auth_url(&self) -> Result<AuthUrl, AuthError>;
|
||||
|
||||
async fn exchange_auth_code(&self, code: &str, expected_nonce: &str)
|
||||
-> Result<User, AuthError>;
|
||||
|
||||
async fn get_user_by_source_id(&self, issuer: &str, source_id: &str)
|
||||
-> Result<User, AuthError>;
|
||||
|
||||
async fn save_user(&self, user: User) -> Result<i32, AuthError>;
|
||||
}
|
||||
|
@ -41,8 +41,32 @@ impl ExchangeAuthCodeUseCase for ExchangeAuthCodeUseCaseImpl {
|
||||
return Err(AuthError::InvalidState);
|
||||
}
|
||||
|
||||
self.auth_repository
|
||||
let mut logged_in_user = self
|
||||
.auth_repository
|
||||
.exchange_auth_code(code, expected_nonce)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
let saved_user_result = self
|
||||
.auth_repository
|
||||
.get_user_by_source_id(&logged_in_user.issuer, &logged_in_user.source_id)
|
||||
.await;
|
||||
|
||||
match saved_user_result {
|
||||
Ok(user) => {
|
||||
logged_in_user.id = user.id;
|
||||
}
|
||||
Err(AuthError::UserNotFound) => {
|
||||
let id = self
|
||||
.auth_repository
|
||||
.save_user(logged_in_user.clone())
|
||||
.await?;
|
||||
logged_in_user.id = id;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(logged_in_user)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
#[derive(Clone)]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
pub issuer: String,
|
||||
pub source_id: String,
|
||||
pub displayed_name: String,
|
||||
pub email: String,
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod db;
|
||||
pub mod oidc;
|
||||
pub mod web;
|
||||
|
2
backend/feature/auth/src/framework/db.rs
Normal file
2
backend/feature/auth/src/framework/db.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod user_db_service_impl;
|
||||
pub mod user_record;
|
@ -0,0 +1,65 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
use crate::{
|
||||
adapter::gateway::{user_db_service::UserDbService, user_db_mapper::UserMapper},
|
||||
application::error::auth_error::AuthError,
|
||||
framework::db::user_record::UserRecord,
|
||||
};
|
||||
|
||||
pub struct UserDbServiceImpl {
|
||||
db_pool: Pool<Postgres>,
|
||||
}
|
||||
|
||||
impl UserDbServiceImpl {
|
||||
pub fn new(db_pool: Pool<Postgres>) -> Self {
|
||||
Self { db_pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl UserDbService for UserDbServiceImpl {
|
||||
async fn get_user_by_source_id(
|
||||
&self,
|
||||
issuer: &str,
|
||||
source_id: &str,
|
||||
) -> Result<UserMapper, AuthError> {
|
||||
let record = sqlx::query_as!(
|
||||
UserRecord,
|
||||
r#"
|
||||
SELECT id, issuer, source_id, displayed_name, email
|
||||
FROM "user"
|
||||
WHERE issuer = $1 AND source_id = $2
|
||||
"#,
|
||||
issuer,
|
||||
source_id
|
||||
)
|
||||
.fetch_optional(&self.db_pool)
|
||||
.await
|
||||
.map_err(|e| AuthError::DatabaseError(e.to_string()))?;
|
||||
|
||||
match record {
|
||||
Some(record) => Ok(record.into_mapper()),
|
||||
None => Err(AuthError::UserNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_user(&self, user: UserMapper) -> Result<i32, AuthError> {
|
||||
let id = sqlx::query_scalar!(
|
||||
r#"
|
||||
INSERT INTO "user" (issuer, source_id, displayed_name, email)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING id
|
||||
"#,
|
||||
user.issuer,
|
||||
user.source_id,
|
||||
user.displayed_name,
|
||||
user.email
|
||||
)
|
||||
.fetch_one(&self.db_pool)
|
||||
.await
|
||||
.map_err(|e| AuthError::DatabaseError(e.to_string()))?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
24
backend/feature/auth/src/framework/db/user_record.rs
Normal file
24
backend/feature/auth/src/framework/db/user_record.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::adapter::gateway::user_db_mapper::UserMapper;
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct UserRecord {
|
||||
pub id: i32,
|
||||
pub issuer: String,
|
||||
pub source_id: String,
|
||||
pub displayed_name: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl UserRecord {
|
||||
pub fn into_mapper(self) -> UserMapper {
|
||||
UserMapper {
|
||||
id: self.id,
|
||||
issuer: self.issuer,
|
||||
source_id: self.source_id,
|
||||
displayed_name: self.displayed_name,
|
||||
email: self.email,
|
||||
}
|
||||
}
|
||||
}
|
@ -93,6 +93,8 @@ impl AuthOidcService for AuthOidcServiceImpl {
|
||||
)
|
||||
.map_err(|_| AuthError::InvalidIdToken)?;
|
||||
|
||||
let issuer = claims.issuer().to_string();
|
||||
|
||||
let preferred_username = claims
|
||||
.preferred_username()
|
||||
.map(|username| username.to_string());
|
||||
@ -101,6 +103,7 @@ impl AuthOidcService for AuthOidcServiceImpl {
|
||||
|
||||
Ok(OidcClaimsResponseDto {
|
||||
sub: claims.subject().to_string(),
|
||||
issuer: issuer,
|
||||
preferred_username: preferred_username,
|
||||
email: email,
|
||||
})
|
||||
|
@ -1 +1,3 @@
|
||||
pub mod auth_web_routes;
|
||||
|
||||
mod constants;
|
||||
|
@ -6,12 +6,11 @@ use crate::{
|
||||
auth_controller::AuthController, oidc_callback_query_dto::OidcCallbackQueryDto,
|
||||
},
|
||||
application::error::auth_error::AuthError,
|
||||
framework::web::constants::{
|
||||
SESSION_KEY_AUTH_NONCE, SESSION_KEY_AUTH_STATE, SESSION_KEY_USER_ID,
|
||||
},
|
||||
};
|
||||
|
||||
const SESSION_KEY_AUTH_STATE: &str = "auth_state";
|
||||
const SESSION_KEY_AUTH_NONCE: &str = "auth_nonce";
|
||||
const SESSION_KEY_USER: &str = "user";
|
||||
|
||||
pub fn configure_auth_routes(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("/auth")
|
||||
@ -29,11 +28,11 @@ async fn oidc_login_handler(
|
||||
|
||||
match result {
|
||||
Ok(auth_url) => {
|
||||
if let Err(e) = session.insert(SESSION_KEY_AUTH_STATE, auth_url.state) {
|
||||
if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_STATE, auth_url.state) {
|
||||
log::error!("{e:?}");
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
if let Err(e) = session.insert(SESSION_KEY_AUTH_NONCE, auth_url.nonce) {
|
||||
if let Err(e) = session.insert::<String>(SESSION_KEY_AUTH_NONCE, auth_url.nonce) {
|
||||
log::error!("{e:?}");
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
@ -53,12 +52,12 @@ async fn oidc_callback_handler(
|
||||
query: web::Query<OidcCallbackQueryDto>,
|
||||
session: Session,
|
||||
) -> impl Responder {
|
||||
let expected_state: String = match session.get(SESSION_KEY_AUTH_STATE) {
|
||||
let expected_state = match session.get::<String>(SESSION_KEY_AUTH_STATE) {
|
||||
Ok(Some(state)) => state,
|
||||
_ => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
|
||||
let expected_nonce: String = match session.get(SESSION_KEY_AUTH_NONCE) {
|
||||
let expected_nonce = match session.get::<String>(SESSION_KEY_AUTH_NONCE) {
|
||||
Ok(Some(nonce)) => nonce,
|
||||
_ => return HttpResponse::BadRequest().finish(),
|
||||
};
|
||||
@ -71,7 +70,7 @@ async fn oidc_callback_handler(
|
||||
session.remove(SESSION_KEY_AUTH_NONCE);
|
||||
match result {
|
||||
Ok(user) => {
|
||||
if let Err(e) = session.insert(SESSION_KEY_USER, user) {
|
||||
if let Err(e) = session.insert::<i32>(SESSION_KEY_USER_ID, user.id) {
|
||||
log::error!("{e:?}");
|
||||
return HttpResponse::InternalServerError().finish();
|
||||
}
|
||||
@ -95,7 +94,7 @@ async fn oidc_callback_handler(
|
||||
async fn logout_handler(session: Session) -> impl Responder {
|
||||
session.remove(SESSION_KEY_AUTH_STATE);
|
||||
session.remove(SESSION_KEY_AUTH_NONCE);
|
||||
session.remove(SESSION_KEY_USER);
|
||||
session.remove(SESSION_KEY_USER_ID);
|
||||
HttpResponse::Found()
|
||||
.append_header((header::LOCATION, "/"))
|
||||
.finish()
|
||||
|
3
backend/feature/auth/src/framework/web/constants.rs
Normal file
3
backend/feature/auth/src/framework/web/constants.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub const SESSION_KEY_AUTH_STATE: &str = "auth_state";
|
||||
pub const SESSION_KEY_AUTH_NONCE: &str = "auth_nonce";
|
||||
pub const SESSION_KEY_USER_ID: &str = "user_id";
|
@ -8,7 +8,7 @@ pub struct ImageRequestDto {
|
||||
impl ImageRequestDto {
|
||||
pub fn into_entity(self) -> Image {
|
||||
Image {
|
||||
id: None,
|
||||
id: -1,
|
||||
mime_type: self.mime_type,
|
||||
data: self.data,
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::domain::entity::image::Image;
|
||||
|
||||
pub struct ImageDbMapper {
|
||||
pub id: Option<i32>,
|
||||
pub id: i32,
|
||||
pub mime_type: String,
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
pub struct Image {
|
||||
pub id: Option<i32>,
|
||||
pub id: i32,
|
||||
pub mime_type: String,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ impl ImageDbService for ImageDbServiceImpl {
|
||||
match image_record {
|
||||
Ok(record) => match record {
|
||||
Some(record) => Ok(ImageDbMapper {
|
||||
id: Some(record.id),
|
||||
id: record.id,
|
||||
mime_type: record.mime_type,
|
||||
}),
|
||||
None => Err(ImageError::NotFound),
|
||||
|
@ -6,7 +6,25 @@ CREATE TABLE "image" (
|
||||
"updated_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE "user" (
|
||||
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||
"issuer" VARCHAR(100) NOT NULL,
|
||||
"source_id" VARCHAR(100) NOT NULL,
|
||||
"displayed_name" VARCHAR(100) NOT NULL,
|
||||
"email" VARCHAR(100) NOT NULL,
|
||||
"created_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_time" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "user_source_id_issuer_key" ON "user" ("source_id", "issuer");
|
||||
CREATE INDEX "user_email_key" ON "user" HASH ("email");
|
||||
|
||||
CREATE TRIGGER "update_image_updated_time"
|
||||
BEFORE UPDATE ON "image"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_time_column();
|
||||
|
||||
CREATE TRIGGER "update_user_updated_time"
|
||||
BEFORE UPDATE ON "user"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_time_column();
|
||||
|
@ -9,7 +9,10 @@ use auth::{
|
||||
exchange_auth_code_use_case::ExchangeAuthCodeUseCaseImpl,
|
||||
get_auth_url_use_case::LoginUseCaseImpl,
|
||||
},
|
||||
framework::oidc::auth_oidc_service_impl::AuthOidcServiceImpl,
|
||||
framework::{
|
||||
db::user_db_service_impl::UserDbServiceImpl,
|
||||
oidc::auth_oidc_service_impl::AuthOidcServiceImpl,
|
||||
},
|
||||
};
|
||||
use image::{
|
||||
adapter::{
|
||||
@ -60,7 +63,8 @@ impl Container {
|
||||
oidc_configuration.redirect_url.clone(),
|
||||
http_client,
|
||||
));
|
||||
let auth_repository = Arc::new(AuthRepositoryImpl::new(auth_oidc_service));
|
||||
let user_db_service = Arc::new(UserDbServiceImpl::new(db_pool.clone()));
|
||||
let auth_repository = Arc::new(AuthRepositoryImpl::new(user_db_service, auth_oidc_service));
|
||||
let get_auth_url_use_case = Arc::new(LoginUseCaseImpl::new(auth_repository.clone()));
|
||||
let exchange_auth_code_use_case =
|
||||
Arc::new(ExchangeAuthCodeUseCaseImpl::new(auth_repository.clone()));
|
||||
|
Loading…
x
Reference in New Issue
Block a user