oidc authentication
This commit is contained in:
@@ -6,6 +6,7 @@ use warren::{
|
||||
file_system::{FileSystem, FileSystemConfig},
|
||||
metrics_debug_logger::MetricsDebugLogger,
|
||||
notifier_debug_logger::NotifierDebugLogger,
|
||||
oidc::{Oidc, OidcConfig},
|
||||
postgres::{Postgres, PostgresConfig},
|
||||
},
|
||||
};
|
||||
@@ -39,8 +40,20 @@ async fn main() -> anyhow::Result<()> {
|
||||
fs_service.clone(),
|
||||
);
|
||||
|
||||
let auth_service =
|
||||
domain::warren::service::auth::Service::new(postgres, metrics, notifier, config.auth);
|
||||
let oidc_service = if let Ok(oidc_config) = OidcConfig::from_env() {
|
||||
let repo = Oidc::new(&oidc_config).await?;
|
||||
Some(domain::oidc::service::Service::new(repo, metrics, notifier))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let auth_service = domain::warren::service::auth::Service::new(
|
||||
postgres,
|
||||
metrics,
|
||||
notifier,
|
||||
config.auth,
|
||||
oidc_service,
|
||||
);
|
||||
|
||||
let server_config = HttpServerConfig::new(
|
||||
&config.server_address,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
pub mod oidc;
|
||||
pub mod warren;
|
||||
|
||||
4
backend/src/lib/domain/oidc/mod.rs
Normal file
4
backend/src/lib/domain/oidc/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod models;
|
||||
pub mod ports;
|
||||
pub mod requests;
|
||||
pub mod service;
|
||||
69
backend/src/lib/domain/oidc/models.rs
Normal file
69
backend/src/lib/domain/oidc/models.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use crate::domain::warren::models::user::{UserEmail, UserName};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct UserInfo {
|
||||
sub: String,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
preferred_username: Option<UserName>,
|
||||
picture: Option<String>,
|
||||
locale: Option<String>,
|
||||
updated_at: Option<i64>,
|
||||
warren_admin: Option<bool>,
|
||||
}
|
||||
|
||||
impl UserInfo {
|
||||
pub fn new(
|
||||
sub: String,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
preferred_username: Option<UserName>,
|
||||
picture: Option<String>,
|
||||
locale: Option<String>,
|
||||
updated_at: Option<i64>,
|
||||
warren_admin: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
sub,
|
||||
name,
|
||||
email,
|
||||
preferred_username,
|
||||
picture,
|
||||
locale,
|
||||
updated_at,
|
||||
warren_admin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub(&self) -> &String {
|
||||
&self.sub
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &UserName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &UserEmail {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn preferred_username(&self) -> Option<&UserName> {
|
||||
self.preferred_username.as_ref()
|
||||
}
|
||||
|
||||
pub fn picture(&self) -> Option<&String> {
|
||||
self.picture.as_ref()
|
||||
}
|
||||
|
||||
pub fn locale(&self) -> Option<&String> {
|
||||
self.locale.as_ref()
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> Option<i64> {
|
||||
self.updated_at
|
||||
}
|
||||
|
||||
pub fn warren_admin(&self) -> Option<bool> {
|
||||
self.warren_admin
|
||||
}
|
||||
}
|
||||
39
backend/src/lib/domain/oidc/ports.rs
Normal file
39
backend/src/lib/domain/oidc/ports.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use super::requests::{
|
||||
GetRedirectError, GetRedirectRequest, GetRedirectResponse, GetUserInfoError,
|
||||
GetUserInfoRequest, GetUserInfoResponse,
|
||||
};
|
||||
|
||||
pub trait OidcService: Clone + Send + Sync + 'static {
|
||||
fn get_redirect(
|
||||
&self,
|
||||
request: GetRedirectRequest,
|
||||
) -> impl Future<Output = Result<GetRedirectResponse, GetRedirectError>> + Send;
|
||||
fn get_user_info(
|
||||
&self,
|
||||
request: GetUserInfoRequest,
|
||||
) -> impl Future<Output = Result<GetUserInfoResponse, GetUserInfoError>> + Send;
|
||||
}
|
||||
|
||||
pub trait OidcRepository: Clone + Send + Sync + 'static {
|
||||
fn get_redirect(
|
||||
&self,
|
||||
request: GetRedirectRequest,
|
||||
) -> impl Future<Output = Result<GetRedirectResponse, GetRedirectError>> + Send;
|
||||
fn get_user_info(
|
||||
&self,
|
||||
request: GetUserInfoRequest,
|
||||
) -> impl Future<Output = Result<GetUserInfoResponse, GetUserInfoError>> + Send;
|
||||
}
|
||||
|
||||
pub trait OidcMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_get_redirect_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_get_redirect_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_get_user_info_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_get_user_info_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait OidcNotifier: Clone + Send + Sync + 'static {
|
||||
fn get_redirect(&self, response: &GetRedirectResponse) -> impl Future<Output = ()> + Send;
|
||||
fn get_user_info(&self, response: &GetUserInfoResponse) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
74
backend/src/lib/domain/oidc/requests.rs
Normal file
74
backend/src/lib/domain/oidc/requests.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use super::models::UserInfo;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GetRedirectRequest {}
|
||||
|
||||
impl GetRedirectRequest {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GetRedirectResponse {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl GetRedirectResponse {
|
||||
pub fn new(url: String) -> Self {
|
||||
Self { url }
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &String {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetRedirectError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GetUserInfoRequest {
|
||||
code: String,
|
||||
state: Option<String>,
|
||||
}
|
||||
|
||||
impl GetUserInfoRequest {
|
||||
pub fn new(code: String, state: Option<String>) -> Self {
|
||||
Self { code, state }
|
||||
}
|
||||
|
||||
pub fn code(&self) -> &String {
|
||||
&self.code
|
||||
}
|
||||
|
||||
pub fn state(&self) -> Option<&String> {
|
||||
self.state.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GetUserInfoResponse {
|
||||
info: UserInfo,
|
||||
}
|
||||
|
||||
impl GetUserInfoResponse {
|
||||
pub fn new(info: UserInfo) -> Self {
|
||||
Self { info }
|
||||
}
|
||||
|
||||
pub fn info(&self) -> &UserInfo {
|
||||
&self.info
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetUserInfoError {
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
73
backend/src/lib/domain/oidc/service.rs
Normal file
73
backend/src/lib/domain/oidc/service.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use super::{
|
||||
ports::{OidcMetrics, OidcNotifier, OidcRepository, OidcService},
|
||||
requests::{
|
||||
GetRedirectError, GetRedirectRequest, GetRedirectResponse, GetUserInfoError,
|
||||
GetUserInfoRequest, GetUserInfoResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<R, M, N>
|
||||
where
|
||||
R: OidcRepository,
|
||||
M: OidcMetrics,
|
||||
N: OidcNotifier,
|
||||
{
|
||||
repository: R,
|
||||
metrics: M,
|
||||
notifier: N,
|
||||
}
|
||||
|
||||
impl<R, M, N> Service<R, M, N>
|
||||
where
|
||||
R: OidcRepository,
|
||||
M: OidcMetrics,
|
||||
N: OidcNotifier,
|
||||
{
|
||||
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
|
||||
Self {
|
||||
repository,
|
||||
metrics,
|
||||
notifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, M, N> OidcService for Service<R, M, N>
|
||||
where
|
||||
R: OidcRepository,
|
||||
M: OidcMetrics,
|
||||
N: OidcNotifier,
|
||||
{
|
||||
async fn get_redirect(
|
||||
&self,
|
||||
request: GetRedirectRequest,
|
||||
) -> Result<GetRedirectResponse, GetRedirectError> {
|
||||
let result = self.repository.get_redirect(request).await;
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_get_redirect_success().await;
|
||||
self.notifier.get_redirect(response).await;
|
||||
} else {
|
||||
self.metrics.record_get_redirect_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
&self,
|
||||
request: GetUserInfoRequest,
|
||||
) -> Result<GetUserInfoResponse, GetUserInfoError> {
|
||||
let result = self.repository.get_user_info(request).await;
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_get_user_info_success().await;
|
||||
self.notifier.get_user_info(response).await;
|
||||
} else {
|
||||
self.metrics.record_get_user_info_failure().await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,21 @@ use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
oidc_sub: Option<String>,
|
||||
id: Uuid,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
hash: String,
|
||||
hash: Option<String>,
|
||||
admin: bool,
|
||||
updated_at: NaiveDateTime,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn oidc_sub(&self) -> Option<&String> {
|
||||
self.oidc_sub.as_ref()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &Uuid {
|
||||
&self.id
|
||||
}
|
||||
@@ -33,8 +38,8 @@ impl User {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn password_hash(&self) -> &str {
|
||||
&self.hash
|
||||
pub fn password_hash(&self) -> Option<&String> {
|
||||
self.hash.as_ref()
|
||||
}
|
||||
|
||||
pub fn admin(&self) -> bool {
|
||||
@@ -74,6 +79,7 @@ impl UserName {
|
||||
}
|
||||
|
||||
/// A valid email
|
||||
// TODO: Maybe move this somewhere else (emails are used here and for OIDC)
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserEmail(String);
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::{
|
||||
oidc::models::UserInfo,
|
||||
warren::models::user::{UserEmail, UserName},
|
||||
};
|
||||
|
||||
/// An admin request to create a new OIDC user or update an existing one if the `sub` already exists
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CreateOrUpdateUserOidcRequest {
|
||||
sub: String,
|
||||
name: UserName,
|
||||
email: UserEmail,
|
||||
admin: bool,
|
||||
}
|
||||
|
||||
impl CreateOrUpdateUserOidcRequest {
|
||||
pub fn new(sub: String, name: UserName, email: UserEmail, admin: bool) -> Self {
|
||||
Self {
|
||||
sub,
|
||||
name,
|
||||
email,
|
||||
admin,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sub(&self) -> &String {
|
||||
&self.sub
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &UserName {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn email(&self) -> &UserEmail {
|
||||
&self.email
|
||||
}
|
||||
|
||||
pub fn admin(&self) -> bool {
|
||||
self.admin
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&UserInfo> for CreateOrUpdateUserOidcRequest {
|
||||
fn from(value: &UserInfo) -> Self {
|
||||
let name = value.preferred_username().unwrap_or(value.name()).clone();
|
||||
Self::new(
|
||||
value.sub().clone(),
|
||||
name,
|
||||
value.email().clone(),
|
||||
value.warren_admin().unwrap_or(false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CreateOrUpdateUserOidcError {
|
||||
#[error("This email is already taken")]
|
||||
EmailTaken,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::oidc::requests::GetRedirectError;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GetOidcRedirectRequest {}
|
||||
|
||||
impl GetOidcRedirectRequest {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetOidcRedirectRequest> for crate::domain::oidc::requests::GetRedirectRequest {
|
||||
fn from(_value: GetOidcRedirectRequest) -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct GetOidcRedirectResponse {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl GetOidcRedirectResponse {
|
||||
pub fn new(url: String) -> Self {
|
||||
Self { url }
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &String {
|
||||
&self.url
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GetOidcRedirectError {
|
||||
#[error("OIDC is not enabled")]
|
||||
Disabled,
|
||||
#[error(transparent)]
|
||||
GetRedirect(#[from] GetRedirectError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::{
|
||||
oidc::requests::{GetUserInfoError, GetUserInfoRequest},
|
||||
warren::models::{
|
||||
auth_session::{AuthSession, requests::CreateAuthSessionError},
|
||||
user::User,
|
||||
},
|
||||
};
|
||||
|
||||
use super::CreateOrUpdateUserOidcError;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct LoginUserOidcRequest {
|
||||
pub(super) code: String,
|
||||
pub(super) state: Option<String>,
|
||||
}
|
||||
|
||||
impl LoginUserOidcRequest {
|
||||
pub fn new(code: String, state: Option<String>) -> Self {
|
||||
Self { code, state }
|
||||
}
|
||||
|
||||
pub fn code(&self) -> &String {
|
||||
&self.code
|
||||
}
|
||||
|
||||
pub fn state(&self) -> Option<&String> {
|
||||
self.state.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LoginUserOidcRequest> for GetUserInfoRequest {
|
||||
fn from(value: LoginUserOidcRequest) -> Self {
|
||||
GetUserInfoRequest::new(value.code, value.state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct LoginUserOidcResponse {
|
||||
session: AuthSession,
|
||||
user: User,
|
||||
}
|
||||
|
||||
impl LoginUserOidcResponse {
|
||||
pub fn new(session: AuthSession, user: User) -> Self {
|
||||
Self { session, user }
|
||||
}
|
||||
|
||||
pub fn session(&self) -> &AuthSession {
|
||||
&self.session
|
||||
}
|
||||
|
||||
pub fn user(&self) -> &User {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LoginUserOidcError {
|
||||
#[error("OIDC is not enabled")]
|
||||
Disabled,
|
||||
#[error(transparent)]
|
||||
GetUserInfo(#[from] GetUserInfoError),
|
||||
#[error(transparent)]
|
||||
CreateOrUpdateUser(#[from] CreateOrUpdateUserOidcError),
|
||||
#[error(transparent)]
|
||||
CreateAuthToken(#[from] CreateAuthSessionError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
mod create;
|
||||
mod create_or_update;
|
||||
mod delete;
|
||||
mod edit;
|
||||
mod get_oidc_redirect;
|
||||
mod list;
|
||||
mod list_all;
|
||||
mod login;
|
||||
mod login_oidc;
|
||||
mod register;
|
||||
mod verify_password;
|
||||
|
||||
pub use create::*;
|
||||
pub use create_or_update::*;
|
||||
pub use delete::*;
|
||||
pub use edit::*;
|
||||
pub use get_oidc_redirect::*;
|
||||
pub use list::*;
|
||||
pub use list_all::*;
|
||||
pub use login::*;
|
||||
pub use login_oidc::*;
|
||||
pub use register::*;
|
||||
pub use verify_password::*;
|
||||
|
||||
@@ -35,6 +35,8 @@ impl From<LoginUserRequest> for VerifyUserPasswordRequest {
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VerifyUserPasswordError {
|
||||
#[error("This user does not use password authentication")]
|
||||
PasswordNotAllowed,
|
||||
#[error("There is no user with this email: {0}")]
|
||||
NotFound(UserEmail),
|
||||
#[error("The password is incorrect")]
|
||||
|
||||
@@ -86,6 +86,9 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
||||
fn record_user_login_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_login_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_user_login_oidc_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_login_oidc_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
fn record_user_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||
fn record_user_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||
|
||||
|
||||
@@ -21,9 +21,11 @@ use super::models::{
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, LoginUserError,
|
||||
LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User,
|
||||
EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest, GetOidcRedirectResponse,
|
||||
ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse,
|
||||
ListUsersError, ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
|
||||
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
||||
RegisterUserRequest, User,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
@@ -141,6 +143,11 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
warren_service: &WS,
|
||||
) -> impl Future<Output = Result<Warren, AuthError<DeleteWarrenError>>> + Send;
|
||||
|
||||
fn get_oidc_redirect(
|
||||
&self,
|
||||
request: GetOidcRedirectRequest,
|
||||
) -> impl Future<Output = Result<GetOidcRedirectResponse, GetOidcRedirectError>> + Send;
|
||||
|
||||
fn register_user(
|
||||
&self,
|
||||
request: RegisterUserRequest,
|
||||
@@ -149,6 +156,10 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: LoginUserRequest,
|
||||
) -> impl Future<Output = Result<LoginUserResponse, LoginUserError>> + Send;
|
||||
fn login_user_oidc(
|
||||
&self,
|
||||
request: LoginUserOidcRequest,
|
||||
) -> impl Future<Output = Result<LoginUserOidcResponse, LoginUserOidcError>> + Send;
|
||||
|
||||
/// An action that creates a user (MUST REQUIRE ADMIN PRIVILEGES)
|
||||
fn create_user(
|
||||
|
||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
||||
use crate::domain::warren::models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse,
|
||||
@@ -81,6 +81,10 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
|
||||
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
|
||||
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
|
||||
fn user_logged_in_oidc(
|
||||
&self,
|
||||
response: &LoginUserOidcResponse,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;
|
||||
fn user_edited(&self, editor: &User, edited: &User) -> impl Future<Output = ()> + Send;
|
||||
fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send;
|
||||
|
||||
@@ -12,10 +12,10 @@ use crate::domain::warren::models::{
|
||||
SaveRequest, SaveResponse, TouchError, TouchRequest,
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User,
|
||||
VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||
CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
|
||||
CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, EditUserRequest,
|
||||
ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse,
|
||||
ListUsersError, ListUsersRequest, User, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
@@ -81,6 +81,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||
}
|
||||
|
||||
pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||
fn create_or_update_user_oidc(
|
||||
&self,
|
||||
request: CreateOrUpdateUserOidcRequest,
|
||||
) -> impl Future<Output = Result<User, CreateOrUpdateUserOidcError>> + Send;
|
||||
fn create_user(
|
||||
&self,
|
||||
request: CreateUserRequest,
|
||||
|
||||
@@ -1,42 +1,47 @@
|
||||
use crate::{
|
||||
config::Config,
|
||||
domain::warren::{
|
||||
models::{
|
||||
auth_session::{
|
||||
AuthError, AuthRequest, AuthSession,
|
||||
requests::{
|
||||
CreateAuthSessionError, CreateAuthSessionRequest, FetchAuthSessionError,
|
||||
FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime,
|
||||
domain::{
|
||||
oidc::ports::OidcService,
|
||||
warren::{
|
||||
models::{
|
||||
auth_session::{
|
||||
AuthError, AuthRequest, AuthSession,
|
||||
requests::{
|
||||
CreateAuthSessionError, CreateAuthSessionRequest, FetchAuthSessionError,
|
||||
FetchAuthSessionRequest, FetchAuthSessionResponse, SessionExpirationTime,
|
||||
},
|
||||
},
|
||||
file::FileStream,
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||
EditUserError, EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest,
|
||||
GetOidcRedirectResponse, ListAllUsersAndWarrensError,
|
||||
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
||||
ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
|
||||
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
|
||||
RegisterUserRequest, User,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
requests::{
|
||||
CreateUserWarrenError, CreateUserWarrenRequest, DeleteUserWarrenError,
|
||||
DeleteUserWarrenRequest, EditUserWarrenError, EditUserWarrenRequest,
|
||||
FetchUserWarrenRequest, FetchUserWarrensError, FetchUserWarrensRequest,
|
||||
},
|
||||
},
|
||||
warren::{
|
||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||
FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError,
|
||||
WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest,
|
||||
WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse,
|
||||
WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError,
|
||||
WarrenRmRequest, WarrenRmResponse, WarrenSaveError, WarrenSaveRequest,
|
||||
WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse,
|
||||
},
|
||||
},
|
||||
file::FileStream,
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||
EditUserError, EditUserRequest, ListAllUsersAndWarrensError,
|
||||
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
|
||||
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse,
|
||||
RegisterUserError, RegisterUserRequest, User,
|
||||
},
|
||||
user_warren::{
|
||||
UserWarren,
|
||||
requests::{
|
||||
CreateUserWarrenError, CreateUserWarrenRequest, DeleteUserWarrenError,
|
||||
DeleteUserWarrenRequest, EditUserWarrenError, EditUserWarrenRequest,
|
||||
FetchUserWarrenRequest, FetchUserWarrensError, FetchUserWarrensRequest,
|
||||
},
|
||||
},
|
||||
warren::{
|
||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||
FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError,
|
||||
WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest,
|
||||
WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse,
|
||||
WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError, WarrenRmRequest,
|
||||
WarrenRmResponse, WarrenSaveError, WarrenSaveRequest, WarrenSaveResponse,
|
||||
WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse,
|
||||
},
|
||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
||||
},
|
||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,7 +50,7 @@ const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION";
|
||||
/// The authentication service configuration
|
||||
///
|
||||
/// * `session_lifetime`: The amount of milliseconds a client session is valid
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AuthConfig {
|
||||
session_lifetime: SessionExpirationTime,
|
||||
}
|
||||
@@ -69,39 +74,50 @@ impl AuthConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Service<R, M, N>
|
||||
pub struct Service<R, M, N, OIDC>
|
||||
where
|
||||
R: AuthRepository,
|
||||
M: AuthMetrics,
|
||||
N: AuthNotifier,
|
||||
OIDC: OidcService,
|
||||
{
|
||||
repository: R,
|
||||
metrics: M,
|
||||
notifier: N,
|
||||
oidc: Option<OIDC>,
|
||||
config: AuthConfig,
|
||||
}
|
||||
|
||||
impl<R, M, N> Service<R, M, N>
|
||||
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
|
||||
where
|
||||
R: AuthRepository,
|
||||
M: AuthMetrics,
|
||||
N: AuthNotifier,
|
||||
OIDC: OidcService,
|
||||
{
|
||||
pub fn new(repository: R, metrics: M, notifier: N, config: AuthConfig) -> Self {
|
||||
pub fn new(
|
||||
repository: R,
|
||||
metrics: M,
|
||||
notifier: N,
|
||||
config: AuthConfig,
|
||||
oidc: Option<OIDC>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repository,
|
||||
metrics,
|
||||
notifier,
|
||||
config,
|
||||
oidc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, M, N> AuthService for Service<R, M, N>
|
||||
impl<R, M, N, OIDC> AuthService for Service<R, M, N, OIDC>
|
||||
where
|
||||
R: AuthRepository,
|
||||
M: AuthMetrics,
|
||||
N: AuthNotifier,
|
||||
OIDC: OidcService,
|
||||
{
|
||||
async fn create_warren<WS: WarrenService>(
|
||||
&self,
|
||||
@@ -197,6 +213,18 @@ where
|
||||
result
|
||||
}
|
||||
|
||||
async fn get_oidc_redirect(
|
||||
&self,
|
||||
request: GetOidcRedirectRequest,
|
||||
) -> Result<GetOidcRedirectResponse, GetOidcRedirectError> {
|
||||
let oidc = self.oidc.as_ref().ok_or(GetOidcRedirectError::Disabled)?;
|
||||
|
||||
oidc.get_redirect(request.into())
|
||||
.await
|
||||
.map(|response| GetOidcRedirectResponse::new(response.url().clone()))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
|
||||
let result = self.repository.create_user(request.into()).await;
|
||||
|
||||
@@ -238,6 +266,37 @@ where
|
||||
result.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn login_user_oidc(
|
||||
&self,
|
||||
request: LoginUserOidcRequest,
|
||||
) -> Result<LoginUserOidcResponse, LoginUserOidcError> {
|
||||
let oidc = self.oidc.as_ref().ok_or(LoginUserOidcError::Disabled)?;
|
||||
|
||||
let user_info = oidc.get_user_info(request.into()).await?;
|
||||
|
||||
let user = self
|
||||
.repository
|
||||
.create_or_update_user_oidc(user_info.info().into())
|
||||
.await?;
|
||||
|
||||
let result = self
|
||||
.create_auth_session(CreateAuthSessionRequest::new(
|
||||
user.clone(),
|
||||
self.config.session_lifetime(),
|
||||
))
|
||||
.await
|
||||
.map(|session| LoginUserOidcResponse::new(session, user));
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_user_login_oidc_success().await;
|
||||
self.notifier.user_logged_in_oidc(response).await;
|
||||
} else {
|
||||
self.metrics.record_user_login_oidc_failure().await;
|
||||
}
|
||||
|
||||
result.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
&self,
|
||||
request: AuthRequest<CreateUserRequest>,
|
||||
|
||||
@@ -2,7 +2,10 @@ use crate::{
|
||||
domain::warren::models::{
|
||||
auth_session::{AuthError, requests::FetchAuthSessionError},
|
||||
file::{LsError, MkdirError, RmError},
|
||||
user::{CreateUserError, LoginUserError, RegisterUserError, VerifyUserPasswordError},
|
||||
user::{
|
||||
CreateUserError, GetOidcRedirectError, LoginUserError, LoginUserOidcError,
|
||||
RegisterUserError, VerifyUserPasswordError,
|
||||
},
|
||||
user_warren::requests::FetchUserWarrenError,
|
||||
warren::{
|
||||
FetchWarrenError, FetchWarrensError, WarrenLsError, WarrenMkdirError, WarrenMvError,
|
||||
@@ -117,6 +120,9 @@ impl From<LoginUserError> for ApiError {
|
||||
fn from(value: LoginUserError) -> Self {
|
||||
match value {
|
||||
LoginUserError::VerifyUser(e) => match e {
|
||||
VerifyUserPasswordError::PasswordNotAllowed => {
|
||||
Self::NotFound("This user does not use password authentication".to_string())
|
||||
}
|
||||
VerifyUserPasswordError::NotFound(_) => {
|
||||
Self::NotFound("Could not find a user with that email".to_string())
|
||||
}
|
||||
@@ -131,6 +137,18 @@ impl From<LoginUserError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LoginUserOidcError> for ApiError {
|
||||
fn from(value: LoginUserOidcError) -> Self {
|
||||
match value {
|
||||
LoginUserOidcError::Disabled => Self::BadRequest("OIDC is disabled".to_string()),
|
||||
LoginUserOidcError::CreateOrUpdateUser(e) => Self::InternalServerError(e.to_string()),
|
||||
LoginUserOidcError::GetUserInfo(e) => Self::InternalServerError(e.to_string()),
|
||||
LoginUserOidcError::CreateAuthToken(e) => Self::InternalServerError(e.to_string()),
|
||||
LoginUserOidcError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FetchAuthSessionError> for ApiError {
|
||||
fn from(value: FetchAuthSessionError) -> Self {
|
||||
match value {
|
||||
@@ -177,3 +195,17 @@ impl From<CreateUserError> for ApiError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GetOidcRedirectError> for ApiError {
|
||||
fn from(value: GetOidcRedirectError) -> Self {
|
||||
match value {
|
||||
GetOidcRedirectError::GetRedirect(e) => match e {
|
||||
crate::domain::oidc::requests::GetRedirectError::Unknown(e) => {
|
||||
Self::InternalServerError(e.to_string())
|
||||
}
|
||||
},
|
||||
GetOidcRedirectError::Disabled => Self::BadRequest("OIDC is disabled".to_string()),
|
||||
GetOidcRedirectError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ impl LoginUserHttpRequestBody {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LoginResponseBody {
|
||||
token: String,
|
||||
user: UserData,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
mod fetch_session;
|
||||
mod login;
|
||||
mod oidc_login;
|
||||
mod oidc_redirect;
|
||||
mod register;
|
||||
|
||||
use fetch_session::fetch_session;
|
||||
use login::login;
|
||||
use oidc_login::oidc_login;
|
||||
use oidc_redirect::oidc_redirect;
|
||||
use register::register;
|
||||
|
||||
use axum::{
|
||||
@@ -20,4 +25,6 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
||||
.route("/register", post(register))
|
||||
.route("/login", post(login))
|
||||
.route("/session", get(fetch_session))
|
||||
.route("/oidc", get(oidc_redirect))
|
||||
.route("/oidc/login", get(oidc_login))
|
||||
}
|
||||
|
||||
62
backend/src/lib/inbound/http/handlers/auth/oidc_login.rs
Normal file
62
backend/src/lib/inbound/http/handlers/auth/oidc_login.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::user::{LoginUserOidcRequest, LoginUserOidcResponse},
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
handlers::UserData,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OidcLoginRequestBody {
|
||||
code: String,
|
||||
state: Option<String>,
|
||||
}
|
||||
|
||||
impl OidcLoginRequestBody {
|
||||
pub fn into_domain(self) -> LoginUserOidcRequest {
|
||||
LoginUserOidcRequest::new(self.code, self.state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct OidcLoginResponseBody {
|
||||
token: String,
|
||||
user: UserData,
|
||||
expires_at: i64,
|
||||
}
|
||||
|
||||
impl From<LoginUserOidcResponse> for OidcLoginResponseBody {
|
||||
fn from(value: LoginUserOidcResponse) -> Self {
|
||||
Self {
|
||||
token: value.session().session_id().to_string(),
|
||||
user: value.user().to_owned().into(),
|
||||
expires_at: value.session().expires_at().and_utc().timestamp_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn oidc_login<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
Query(request): Query<OidcLoginRequestBody>,
|
||||
) -> Result<ApiSuccess<OidcLoginResponseBody>, ApiError> {
|
||||
let domain_request = request.into_domain();
|
||||
|
||||
state
|
||||
.auth_service
|
||||
.login_user_oidc(domain_request)
|
||||
.await
|
||||
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
23
backend/src/lib/inbound/http/handlers/auth/oidc_redirect.rs
Normal file
23
backend/src/lib/inbound/http/handlers/auth/oidc_redirect.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use axum::{extract::State, http::StatusCode};
|
||||
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::user::GetOidcRedirectRequest,
|
||||
ports::{AuthService, WarrenService},
|
||||
},
|
||||
inbound::http::{
|
||||
AppState,
|
||||
responses::{ApiError, ApiSuccess},
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn oidc_redirect<WS: WarrenService, AS: AuthService>(
|
||||
State(state): State<AppState<WS, AS>>,
|
||||
) -> Result<ApiSuccess<String>, ApiError> {
|
||||
state
|
||||
.auth_service
|
||||
.get_oidc_redirect(GetOidcRedirectRequest::new())
|
||||
.await
|
||||
.map(|response| ApiSuccess::new(StatusCode::FOUND, response.url().clone()))
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
use crate::domain::warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics};
|
||||
use crate::domain::{
|
||||
oidc::ports::OidcMetrics,
|
||||
warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MetricsDebugLogger;
|
||||
@@ -175,17 +178,17 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] Warren creation by admin failed");
|
||||
}
|
||||
|
||||
async fn record_auth_warren_edit_success(&self) -> () {
|
||||
async fn record_auth_warren_edit_success(&self) {
|
||||
tracing::debug!("[Metrics] Warren edit by admin succeeded");
|
||||
}
|
||||
async fn record_auth_warren_edit_failure(&self) -> () {
|
||||
async fn record_auth_warren_edit_failure(&self) {
|
||||
tracing::debug!("[Metrics] Warren edit by admin failed");
|
||||
}
|
||||
|
||||
async fn record_auth_warren_deletion_success(&self) -> () {
|
||||
async fn record_auth_warren_deletion_success(&self) {
|
||||
tracing::debug!("[Metrics] Warren deletion by admin succeeded");
|
||||
}
|
||||
async fn record_auth_warren_deletion_failure(&self) -> () {
|
||||
async fn record_auth_warren_deletion_failure(&self) {
|
||||
tracing::debug!("[Metrics] Warren deletion by admin failed");
|
||||
}
|
||||
|
||||
@@ -203,6 +206,13 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] User login failed");
|
||||
}
|
||||
|
||||
async fn record_user_login_oidc_success(&self) {
|
||||
tracing::debug!("[Metrics] User login succeeded");
|
||||
}
|
||||
async fn record_user_login_oidc_failure(&self) {
|
||||
tracing::debug!("[Metrics] User login failed");
|
||||
}
|
||||
|
||||
async fn record_user_creation_success(&self) {
|
||||
tracing::debug!("[Metrics] User creation succeeded");
|
||||
}
|
||||
@@ -350,3 +360,19 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
tracing::debug!("[Metrics] Auth warren cp failed");
|
||||
}
|
||||
}
|
||||
|
||||
impl OidcMetrics for MetricsDebugLogger {
|
||||
async fn record_get_redirect_success(&self) {
|
||||
tracing::debug!("[Metrics] OIDC get redirect succeeded");
|
||||
}
|
||||
async fn record_get_redirect_failure(&self) {
|
||||
tracing::debug!("[Metrics] OIDC get redirect failed");
|
||||
}
|
||||
|
||||
async fn record_get_user_info_success(&self) {
|
||||
tracing::debug!("[Metrics] OIDC get user info succeeded");
|
||||
}
|
||||
async fn record_get_user_info_failure(&self) {
|
||||
tracing::debug!("[Metrics] OIDC get user info failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod file_system;
|
||||
pub mod metrics_debug_logger;
|
||||
pub mod notifier_debug_logger;
|
||||
pub mod oidc;
|
||||
pub mod postgres;
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::{
|
||||
models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse,
|
||||
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
|
||||
},
|
||||
use crate::domain::{
|
||||
oidc::{
|
||||
ports::OidcNotifier,
|
||||
requests::{GetRedirectResponse, GetUserInfoResponse},
|
||||
},
|
||||
warren::{
|
||||
models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
user::{
|
||||
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
|
||||
},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse,
|
||||
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
|
||||
},
|
||||
},
|
||||
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
|
||||
},
|
||||
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -220,6 +228,13 @@ impl AuthNotifier for NotifierDebugLogger {
|
||||
tracing::debug!("[Notifier] Logged in user {}", response.user().name());
|
||||
}
|
||||
|
||||
async fn user_logged_in_oidc(&self, response: &LoginUserOidcResponse) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Logged in user {} with OIDC",
|
||||
response.user().name()
|
||||
);
|
||||
}
|
||||
|
||||
async fn auth_session_created(&self, user_id: &Uuid) {
|
||||
tracing::debug!("[Notifier] Created auth session for user {}", user_id);
|
||||
}
|
||||
@@ -354,3 +369,19 @@ impl AuthNotifier for NotifierDebugLogger {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl OidcNotifier for NotifierDebugLogger {
|
||||
async fn get_redirect(&self, response: &GetRedirectResponse) {
|
||||
tracing::debug!("[Notifier] Got OIDC redirect: {}", response.url());
|
||||
}
|
||||
async fn get_user_info(&self, response: &GetUserInfoResponse) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Got OIDC user info: {} ({})",
|
||||
response
|
||||
.info()
|
||||
.preferred_username()
|
||||
.unwrap_or(response.info().name()),
|
||||
response.info().sub(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
187
backend/src/lib/outbound/oidc.rs
Normal file
187
backend/src/lib/outbound/oidc.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::{str::FromStr as _, sync::Arc};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use openid::{Client, CompactJson, CustomClaims, Discovered, Options, StandardClaims};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
domain::{
|
||||
oidc::{
|
||||
models::UserInfo,
|
||||
ports::OidcRepository,
|
||||
requests::{
|
||||
GetRedirectError, GetRedirectRequest, GetRedirectResponse, GetUserInfoError,
|
||||
GetUserInfoRequest, GetUserInfoResponse,
|
||||
},
|
||||
},
|
||||
warren::models::user::{UserEmail, UserName},
|
||||
},
|
||||
};
|
||||
|
||||
type OidcClient = Client<Discovered, Claims>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OidcConfig {
|
||||
issuer_url: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect_url: String,
|
||||
origin_url: String,
|
||||
}
|
||||
|
||||
impl OidcConfig {
|
||||
pub fn from_env() -> anyhow::Result<Self> {
|
||||
let issuer_url = Config::load_env("OIDC_ISSUER_URL")?;
|
||||
let client_id = Config::load_env("OIDC_CLIENT_ID")?;
|
||||
let client_secret = Config::load_env("OIDC_CLIENT_SECRET")?;
|
||||
let origin = Config::load_env("OIDC_ORIGIN_URL")?;
|
||||
let redirect_url = Config::load_env("OIDC_REDIRECT_URL")?;
|
||||
|
||||
Ok(Self::new(
|
||||
issuer_url,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_url,
|
||||
origin,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
issuer_url: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
redirect_url: String,
|
||||
origin_url: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
issuer_url,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_url,
|
||||
origin_url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Oidc {
|
||||
client: OidcClient,
|
||||
|
||||
auth_options: Arc<openid::Options>,
|
||||
}
|
||||
|
||||
impl Oidc {
|
||||
pub async fn new(config: &OidcConfig) -> anyhow::Result<Self> {
|
||||
let client = OidcClient::discover(
|
||||
config.client_id.clone(),
|
||||
config.client_secret.clone(),
|
||||
config.redirect_url.clone(),
|
||||
Url::from_str(&config.issuer_url)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let auth_options = Options {
|
||||
scope: Some("openid profile email".into()),
|
||||
state: Some(config.origin_url.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
auth_options: Arc::new(auth_options),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct Claims {
|
||||
warren_admin: Option<bool>,
|
||||
#[serde(flatten)]
|
||||
standard_claims: StandardClaims,
|
||||
}
|
||||
|
||||
impl CustomClaims for Claims {
|
||||
fn standard_claims(&self) -> &StandardClaims {
|
||||
&self.standard_claims
|
||||
}
|
||||
}
|
||||
|
||||
impl CompactJson for Claims {}
|
||||
|
||||
impl OidcRepository for Oidc {
|
||||
async fn get_redirect(
|
||||
&self,
|
||||
_request: GetRedirectRequest,
|
||||
) -> Result<GetRedirectResponse, GetRedirectError> {
|
||||
let url = self.client.auth_url(&self.auth_options);
|
||||
|
||||
Ok(GetRedirectResponse::new(url.into()))
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
&self,
|
||||
request: GetUserInfoRequest,
|
||||
) -> Result<GetUserInfoResponse, GetUserInfoError> {
|
||||
let mut token: openid::Token<Claims> = self
|
||||
.client
|
||||
.request_token(request.code())
|
||||
.await
|
||||
.context("Failed to request token")?
|
||||
.into();
|
||||
|
||||
let id_token = token
|
||||
.id_token
|
||||
.as_mut()
|
||||
.context("Token didn't have id_token field")?;
|
||||
|
||||
self.client
|
||||
.decode_token(id_token)
|
||||
.context("Failed to decode token")?;
|
||||
|
||||
id_token
|
||||
.payload_mut()
|
||||
.context("Failed to get id_token payload")?
|
||||
.standard_claims
|
||||
.azp = Some(self.client.client_id.clone());
|
||||
|
||||
self.client
|
||||
.validate_token(id_token, None, None)
|
||||
.context("Failed to validate token")?;
|
||||
|
||||
let user_info = try_get_user_info(
|
||||
id_token
|
||||
.payload()
|
||||
.context("Failed to get id token payload")?,
|
||||
)?;
|
||||
|
||||
Ok(GetUserInfoResponse::new(user_info))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_get_user_info(claims: &Claims) -> anyhow::Result<UserInfo> {
|
||||
let sub = claims.standard_claims.sub.clone();
|
||||
let raw = &claims.standard_claims.userinfo;
|
||||
|
||||
let name = UserName::new(raw.name.as_ref().context("Missing name")?)?;
|
||||
let email = UserEmail::new(raw.email.as_ref().context("Missing email")?)?;
|
||||
let preferred_username = if let Some(preferred_username) = raw.preferred_username.as_ref() {
|
||||
Some(UserName::new(preferred_username)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let user_info = UserInfo::new(
|
||||
sub,
|
||||
name,
|
||||
email,
|
||||
preferred_username,
|
||||
raw.picture.as_ref().map(|url| url.clone().into()),
|
||||
raw.locale.clone(),
|
||||
raw.updated_at,
|
||||
claims.warren_admin,
|
||||
);
|
||||
|
||||
Ok(user_info)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, anyhow};
|
||||
use anyhow::{Context as _, anyhow, bail};
|
||||
use argon2::{
|
||||
Argon2, PasswordHash, PasswordVerifier as _,
|
||||
password_hash::{
|
||||
@@ -23,8 +23,9 @@ use crate::domain::warren::{
|
||||
},
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
|
||||
CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, EditUserRequest,
|
||||
ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
|
||||
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User, UserEmail,
|
||||
UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest,
|
||||
},
|
||||
@@ -66,6 +67,30 @@ impl AuthRepository for Postgres {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn create_or_update_user_oidc(
|
||||
&self,
|
||||
request: CreateOrUpdateUserOidcRequest,
|
||||
) -> Result<User, CreateOrUpdateUserOidcError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
.acquire()
|
||||
.await
|
||||
.context("Failed to get a PostgreSQL connection")?;
|
||||
|
||||
let user = self
|
||||
.create_or_update_user(
|
||||
&mut connection,
|
||||
request.sub(),
|
||||
request.name(),
|
||||
request.email(),
|
||||
request.admin(),
|
||||
)
|
||||
.await
|
||||
.context(format!("Failed to create or update user"))?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> {
|
||||
let mut connection = self
|
||||
.pool
|
||||
@@ -127,7 +152,11 @@ impl AuthRepository for Postgres {
|
||||
}
|
||||
})?;
|
||||
|
||||
self.check_user_password_against_hash(request.password(), user.password_hash())?;
|
||||
self.check_user_password_against_hash(
|
||||
request.password(),
|
||||
user.password_hash()
|
||||
.ok_or(VerifyUserPasswordError::PasswordNotAllowed)?,
|
||||
)?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
@@ -423,6 +452,121 @@ impl Postgres {
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn create_or_update_user(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
sub: &String,
|
||||
name: &UserName,
|
||||
email: &UserEmail,
|
||||
is_admin: bool,
|
||||
) -> anyhow::Result<User> {
|
||||
let mut tx = connection.begin().await?;
|
||||
|
||||
let existing_user: Option<User> = sqlx::query_as(
|
||||
"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
oidc_sub = $1 OR
|
||||
email = $2
|
||||
",
|
||||
)
|
||||
.bind(sub)
|
||||
.bind(email)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let user: User = match existing_user {
|
||||
Some(existing_user) => {
|
||||
if let Some(existing_oidc_sub) = existing_user.oidc_sub() {
|
||||
if existing_oidc_sub != sub {
|
||||
// TODO: Return a proper error
|
||||
bail!("The user is already linked to another OIDC subject");
|
||||
}
|
||||
|
||||
sqlx::query_as(
|
||||
"
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
name = $3,
|
||||
email = $4,
|
||||
admin = $5
|
||||
WHERE
|
||||
id = $1 AND
|
||||
oidc_sub = $2
|
||||
RETURNING
|
||||
*
|
||||
",
|
||||
)
|
||||
.bind(existing_user.id())
|
||||
.bind(sub)
|
||||
.bind(name)
|
||||
.bind(email)
|
||||
.bind(is_admin)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"
|
||||
UPDATE
|
||||
users
|
||||
SET
|
||||
oidc_sub = $2,
|
||||
name = $3,
|
||||
email = $4,
|
||||
admin = $5
|
||||
WHERE
|
||||
id = $1 AND
|
||||
oidc_sub IS NULL
|
||||
RETURNING
|
||||
*
|
||||
",
|
||||
)
|
||||
.bind(existing_user.id())
|
||||
.bind(sub)
|
||||
.bind(name)
|
||||
.bind(email)
|
||||
.bind(is_admin)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
None => {
|
||||
sqlx::query_as(
|
||||
"
|
||||
INSERT INTO users (
|
||||
oidc_sub,
|
||||
name,
|
||||
email,
|
||||
admin
|
||||
)
|
||||
VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4
|
||||
)
|
||||
RETURNING
|
||||
*
|
||||
",
|
||||
)
|
||||
.bind(sub)
|
||||
.bind(name)
|
||||
.bind(email)
|
||||
.bind(is_admin)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
async fn edit_user(
|
||||
&self,
|
||||
connection: &mut PgConnection,
|
||||
|
||||
Reference in New Issue
Block a user