feat(users): implemented openidconnect (#5124)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Rachit Naithani
2024-06-26 19:15:55 +05:30
committed by GitHub
parent ed021c1d99
commit ce7d0d427d
22 changed files with 877 additions and 41 deletions

View File

@ -86,6 +86,8 @@ pub enum UserErrors {
InvalidUserAuthMethodOperation,
#[error("Auth config parsing error")]
AuthConfigParsingError,
#[error("Invalid SSO request")]
SSOFailed,
}
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
@ -219,6 +221,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
Self::AuthConfigParsingError => {
AER::BadRequest(ApiError::new(sub_code, 45, self.get_error_message(), None))
}
Self::SSOFailed => {
AER::BadRequest(ApiError::new(sub_code, 46, self.get_error_message(), None))
}
}
}
}
@ -265,6 +270,7 @@ impl UserErrors {
Self::UserAuthMethodAlreadyExists => "User auth method already exists",
Self::InvalidUserAuthMethodOperation => "Invalid user auth method operation",
Self::AuthConfigParsingError => "Auth config parsing error",
Self::SSOFailed => "Invalid SSO request",
}
}
}

View File

@ -1,6 +1,9 @@
use std::collections::HashMap;
use api_models::user::{self as user_api, InviteMultipleUserResponse};
use api_models::{
payments::RedirectionResponse,
user::{self as user_api, InviteMultipleUserResponse},
};
use common_utils::ext_traits::ValueExt;
#[cfg(feature = "email")]
use diesel_models::user_role::UserRoleUpdate;
@ -13,7 +16,7 @@ use diesel_models::{
use error_stack::{report, ResultExt};
#[cfg(feature = "email")]
use external_services::email::EmailData;
use masking::{ExposeInterface, PeekInterface};
use masking::{ExposeInterface, PeekInterface, Secret};
#[cfg(feature = "email")]
use router_env::env;
use router_env::logger;
@ -26,7 +29,7 @@ use crate::services::email::types as email_types;
use crate::{
consts,
routes::{app::ReqState, SessionState},
services::{authentication as auth, authorization::roles, ApplicationResponse},
services::{authentication as auth, authorization::roles, openidconnect, ApplicationResponse},
types::{domain, transformers::ForeignInto},
utils::{self, user::two_factor_auth as tfa_utils},
};
@ -2198,3 +2201,114 @@ pub async fn list_user_authentication_methods(
.collect::<UserResult<_>>()?,
))
}
pub async fn get_sso_auth_url(
state: SessionState,
request: user_api::GetSsoAuthUrlRequest,
) -> UserResponse<()> {
let user_authentication_method = state
.store
.get_user_authentication_method_by_id(request.id.as_str())
.await
.to_not_found_response(UserErrors::InvalidUserAuthMethodOperation)?;
let open_id_private_config =
utils::user::decrypt_oidc_private_config(&state, user_authentication_method.private_config)
.await?;
let open_id_public_config: user_api::OpenIdConnectPublicConfig = user_authentication_method
.public_config
.ok_or(UserErrors::InternalServerError)
.attach_printable("Public config not present")?
.parse_value("OpenIdConnectPublicConfig")
.change_context(UserErrors::InternalServerError)
.attach_printable("unable to parse OpenIdConnectPublicConfig")?;
let oidc_state = Secret::new(nanoid::nanoid!());
utils::user::set_sso_id_in_redis(&state, oidc_state.clone(), request.id).await?;
let redirect_url =
utils::user::get_oidc_sso_redirect_url(&state, &open_id_public_config.name.to_string());
openidconnect::get_authorization_url(
state,
redirect_url,
oidc_state,
open_id_private_config.base_url.into(),
open_id_private_config.client_id,
)
.await
.map(|url| {
ApplicationResponse::JsonForRedirection(RedirectionResponse {
headers: Vec::with_capacity(0),
return_url: String::new(),
http_method: String::new(),
params: Vec::with_capacity(0),
return_url_with_query_params: url.to_string(),
})
})
}
pub async fn sso_sign(
state: SessionState,
request: user_api::SsoSignInRequest,
user_from_single_purpose_token: Option<auth::UserFromSinglePurposeToken>,
) -> UserResponse<user_api::TokenResponse> {
let authentication_method_id =
utils::user::get_sso_id_from_redis(&state, request.state.clone()).await?;
let user_authentication_method = state
.store
.get_user_authentication_method_by_id(&authentication_method_id)
.await
.change_context(UserErrors::InternalServerError)?;
let open_id_private_config =
utils::user::decrypt_oidc_private_config(&state, user_authentication_method.private_config)
.await?;
let open_id_public_config: user_api::OpenIdConnectPublicConfig = user_authentication_method
.public_config
.ok_or(UserErrors::InternalServerError)
.attach_printable("Public config not present")?
.parse_value("OpenIdConnectPublicConfig")
.change_context(UserErrors::InternalServerError)
.attach_printable("unable to parse OpenIdConnectPublicConfig")?;
let redirect_url =
utils::user::get_oidc_sso_redirect_url(&state, &open_id_public_config.name.to_string());
let email = openidconnect::get_user_email_from_oidc_provider(
&state,
redirect_url,
request.state,
open_id_private_config.base_url.into(),
open_id_private_config.client_id,
request.code,
open_id_private_config.client_secret,
)
.await?;
// TODO: Use config to handle not found error
let user_from_db = state
.global_store
.find_user_by_email(&email.into_inner())
.await
.map(Into::into)
.to_not_found_response(UserErrors::UserNotFound)?;
let next_flow = if let Some(user_from_single_purpose_token) = user_from_single_purpose_token {
let current_flow =
domain::CurrentFlow::new(user_from_single_purpose_token, domain::SPTFlow::SSO.into())?;
current_flow.next(user_from_db, &state).await?
} else {
domain::NextFlow::from_origin(domain::Origin::SignInWithSSO, user_from_db, &state).await?
};
let token = next_flow.get_token(&state).await?;
let response = user_api::TokenResponse {
token: token.clone(),
token_type: next_flow.get_flow().into(),
};
auth::cookies::set_cookie_response(response, token)
}