Bug: feat: create redirect and sso login api



Required: 
- Api that should redirect to openidconnect provider login.
- Api that should accept the `code` and `state` passed by `openidconnect` provider and continue with login flow.
diff --git a/Cargo.lock b/Cargo.lock
index 8c7c88e6196..9b08fcbd57b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1430,6 +1430,12 @@ dependencies = [
  "rustc-demangle",
 ]
 
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
 [[package]]
 name = "base32"
 version = "0.4.0"
@@ -2307,6 +2313,18 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array",
+ "rand_core",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "crypto-common"
 version = "0.1.6"
@@ -2328,6 +2346,34 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "curve25519-dalek-derive",
+ "digest",
+ "fiat-crypto",
+ "platforms",
+ "rustc_version 0.4.0",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+]
+
 [[package]]
 name = "darling"
 version = "0.14.4"
@@ -2677,6 +2723,44 @@ version = "1.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
 
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest",
+ "elliptic-curve",
+ "rfc6979",
+ "signature",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "either"
 version = "1.10.0"
@@ -2686,6 +2770,27 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct",
+ "crypto-bigint",
+ "digest",
+ "ff",
+ "generic-array",
+ "group",
+ "hkdf",
+ "pem-rfc7468",
+ "pkcs8",
+ "rand_core",
+ "sec1",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.33"
@@ -2915,6 +3020,22 @@ dependencies = [
  "simd-adler32",
 ]
 
+[[package]]
+name = "ff"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
+dependencies = [
+ "rand_core",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
 [[package]]
 name = "finl_unicode"
 version = "1.2.0"
@@ -3197,6 +3318,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
 dependencies = [
  "typenum",
  "version_check",
+ "zeroize",
 ]
 
 [[package]]
@@ -3287,6 +3409,17 @@ dependencies = [
  "tempfile",
 ]
 
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core",
+ "subtle",
+]
+
 [[package]]
 name = "h2"
 version = "0.3.25"
@@ -4550,6 +4683,26 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "oauth2"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
+dependencies = [
+ "base64 0.13.1",
+ "chrono",
+ "getrandom",
+ "http 0.2.12",
+ "rand",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "sha2",
+ "thiserror",
+ "url",
+]
+
 [[package]]
 name = "object"
 version = "0.32.2"
@@ -4596,6 +4749,38 @@ dependencies = [
  "utoipa",
 ]
 
+[[package]]
+name = "openidconnect"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f47e80a9cfae4462dd29c41e987edd228971d6565553fbc14b8a11e666d91590"
+dependencies = [
+ "base64 0.13.1",
+ "chrono",
+ "dyn-clone",
+ "ed25519-dalek",
+ "hmac",
+ "http 0.2.12",
+ "itertools 0.10.5",
+ "log",
+ "oauth2",
+ "p256",
+ "p384",
+ "rand",
+ "rsa",
+ "serde",
+ "serde-value",
+ "serde_derive",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_plain",
+ "serde_with",
+ "sha2",
+ "subtle",
+ "thiserror",
+ "url",
+]
+
 [[package]]
 name = "opensearch"
 version = "2.2.0"
@@ -4743,6 +4928,15 @@ dependencies = [
  "tokio-stream",
 ]
 
+[[package]]
+name = "ordered-float"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "ordered-multimap"
 version = "0.6.0"
@@ -4765,6 +4959,30 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.9.0"
@@ -5040,6 +5258,12 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
 
+[[package]]
+name = "platforms"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
+
 [[package]]
 name = "plotters"
 version = "0.3.5"
@@ -5123,6 +5347,15 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
 [[package]]
 name = "proc-macro-crate"
 version = "1.3.1"
@@ -5584,6 +5817,16 @@ dependencies = [
  "winreg",
 ]
 
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
 [[package]]
 name = "ring"
 version = "0.16.20"
@@ -5728,6 +5971,7 @@ dependencies = [
  "num_cpus",
  "once_cell",
  "openapi",
+ "openidconnect",
  "openssl",
  "pm_auth",
  "qrcode",
@@ -6116,6 +6360,20 @@ version = "4.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
 
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct",
+ "der",
+ "generic-array",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
 [[package]]
 name = "security-framework"
 version = "2.9.2"
@@ -6172,6 +6430,16 @@ dependencies = [
  "serde_derive",
 ]
 
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float",
+ "serde",
+]
+
 [[package]]
 name = "serde-wasm-bindgen"
 version = "0.5.0"
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index e5d217cd8ea..4f5651e0a3c 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -12,14 +12,14 @@ use crate::user::{
     },
     AcceptInviteFromEmailRequest, AuthorizeResponse, BeginTotpResponse, ChangePasswordRequest,
     ConnectAccountRequest, CreateInternalUserRequest, CreateUserAuthenticationMethodRequest,
-    DashboardEntryResponse, ForgotPasswordRequest, GetUserAuthenticationMethodsRequest,
-    GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
-    InviteUserRequest, ListUsersResponse, ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest,
-    RotatePasswordRequest, SendVerifyEmailRequest, SignInResponse, SignUpRequest,
-    SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse,
-    TwoFactorAuthStatusResponse, UpdateUserAccountDetailsRequest,
-    UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantCreate,
-    VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
+    DashboardEntryResponse, ForgotPasswordRequest, GetSsoAuthUrlRequest,
+    GetUserAuthenticationMethodsRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest,
+    GetUserRoleDetailsResponse, InviteUserRequest, ListUsersResponse, ReInviteUserRequest,
+    RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest,
+    SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest,
+    SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse, TwoFactorAuthStatusResponse,
+    UpdateUserAccountDetailsRequest, UpdateUserAuthenticationMethodRequest, UserFromEmailRequest,
+    UserMerchantCreate, VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
 };
 
 impl ApiEventMetric for DashboardEntryResponse {
@@ -81,7 +81,9 @@ common_utils::impl_misc_api_event_type!(
     RecoveryCodes,
     GetUserAuthenticationMethodsRequest,
     CreateUserAuthenticationMethodRequest,
-    UpdateUserAuthenticationMethodRequest
+    UpdateUserAuthenticationMethodRequest,
+    GetSsoAuthUrlRequest,
+    SsoSignInRequest
 );
 
 #[cfg(feature = "dummy_connector")]
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 0006afea0d9..b2ed491b677 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -306,8 +306,9 @@ pub struct OpenIdConnectPublicConfig {
     pub name: OpenIdProvider,
 }
 
-#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, strum::Display)]
 #[serde(rename_all = "snake_case")]
+#[strum(serialize_all = "snake_case")]
 pub enum OpenIdProvider {
     Okta,
 }
@@ -356,6 +357,17 @@ pub struct AuthMethodDetails {
     pub name: Option<OpenIdProvider>,
 }
 
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct GetSsoAuthUrlRequest {
+    pub id: String,
+}
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct SsoSignInRequest {
+    pub state: Secret<String>,
+    pub code: Secret<String>,
+}
+
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct AuthIdQueryParam {
     pub auth_id: Option<String>,
diff --git a/crates/diesel_models/src/query/user_authentication_method.rs b/crates/diesel_models/src/query/user_authentication_method.rs
index 08ea6556b82..14a28269cec 100644
--- a/crates/diesel_models/src/query/user_authentication_method.rs
+++ b/crates/diesel_models/src/query/user_authentication_method.rs
@@ -12,6 +12,13 @@ impl UserAuthenticationMethodNew {
 }
 
 impl UserAuthenticationMethod {
+    pub async fn get_user_authentication_method_by_id(
+        conn: &PgPooledConn,
+        id: &str,
+    ) -> StorageResult<Self> {
+        generics::generic_find_by_id::<<Self as HasTable>::Table, _, _>(conn, id.to_owned()).await
+    }
+
     pub async fn list_user_authentication_methods_for_auth_id(
         conn: &PgPooledConn,
         auth_id: &str,
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index e48a6b44194..dd14c8d8960 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -66,6 +66,7 @@ mime = "0.3.17"
 nanoid = "0.4.0"
 num_cpus = "1.16.0"
 once_cell = "1.19.0"
+openidconnect = "3.5.0" # TODO: remove reqwest
 openssl = "0.10.64"
 qrcode = "0.14.0"
 rand = "0.8.5"
diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs
index b471448368f..40c5c385007 100644
--- a/crates/router/src/analytics.rs
+++ b/crates/router/src/analytics.rs
@@ -126,7 +126,7 @@ pub mod routes {
             state,
             &req,
             domain.into_inner(),
-            |_, _, domain: analytics::AnalyticsDomain, _| async {
+            |_, _: (), domain: analytics::AnalyticsDomain, _| async {
                 analytics::core::get_domain_info(domain)
                     .await
                     .map(ApplicationResponse::Json)
diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs
index 331c256e8a4..261f8a1efcc 100644
--- a/crates/router/src/consts/user.rs
+++ b/crates/router/src/consts/user.rs
@@ -19,3 +19,6 @@ pub const REDIS_TOTP_PREFIX: &str = "TOTP_";
 pub const REDIS_RECOVERY_CODE_PREFIX: &str = "RC_";
 pub const REDIS_TOTP_SECRET_PREFIX: &str = "TOTP_SEC_";
 pub const REDIS_TOTP_SECRET_TTL_IN_SECS: i64 = 15 * 60; // 15 minutes
+
+pub const REDIS_SSO_PREFIX: &str = "SSO_";
+pub const REDIS_SSO_TTL: i64 = 5 * 60; // 5 minutes
diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs
index 28b80e16cc0..1cd37679ad4 100644
--- a/crates/router/src/core/errors/user.rs
+++ b/crates/router/src/core/errors/user.rs
@@ -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",
         }
     }
 }
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 0fd583143cc..c4260a92948 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -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)
+}
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index e60bcc79bdb..e5686d616d4 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -2971,6 +2971,15 @@ impl UserAuthenticationMethodInterface for KafkaStore {
             .await
     }
 
+    async fn get_user_authentication_method_by_id(
+        &self,
+        id: &str,
+    ) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
+        self.diesel_store
+            .get_user_authentication_method_by_id(id)
+            .await
+    }
+
     async fn list_user_authentication_methods_for_auth_id(
         &self,
         auth_id: &str,
diff --git a/crates/router/src/db/user_authentication_method.rs b/crates/router/src/db/user_authentication_method.rs
index 5b9aa5da8c9..bcc2313d5db 100644
--- a/crates/router/src/db/user_authentication_method.rs
+++ b/crates/router/src/db/user_authentication_method.rs
@@ -16,6 +16,11 @@ pub trait UserAuthenticationMethodInterface {
         user_authentication_method: storage::UserAuthenticationMethodNew,
     ) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError>;
 
+    async fn get_user_authentication_method_by_id(
+        &self,
+        id: &str,
+    ) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError>;
+
     async fn list_user_authentication_methods_for_auth_id(
         &self,
         auth_id: &str,
@@ -47,6 +52,17 @@ impl UserAuthenticationMethodInterface for Store {
             .map_err(|error| report!(errors::StorageError::from(error)))
     }
 
+    #[instrument(skip_all)]
+    async fn get_user_authentication_method_by_id(
+        &self,
+        id: &str,
+    ) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::UserAuthenticationMethod::get_user_authentication_method_by_id(&conn, id)
+            .await
+            .map_err(|error| report!(errors::StorageError::from(error)))
+    }
+
     #[instrument(skip_all)]
     async fn list_user_authentication_methods_for_auth_id(
         &self,
@@ -120,6 +136,28 @@ impl UserAuthenticationMethodInterface for MockDb {
         Ok(user_authentication_method)
     }
 
+    #[instrument(skip_all)]
+    async fn get_user_authentication_method_by_id(
+        &self,
+        id: &str,
+    ) -> CustomResult<storage::UserAuthenticationMethod, errors::StorageError> {
+        let user_authentication_methods = self.user_authentication_methods.lock().await;
+
+        let user_authentication_method = user_authentication_methods
+            .iter()
+            .find(|&auth_method_inner| auth_method_inner.id == id);
+
+        if let Some(user_authentication_method) = user_authentication_method {
+            Ok(user_authentication_method.to_owned())
+        } else {
+            return Err(errors::StorageError::ValueNotFound(format!(
+                "No user authentication method found for id = {}",
+                id
+            ))
+            .into());
+        }
+    }
+
     async fn list_user_authentication_methods_for_auth_id(
         &self,
         auth_id: &str,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index b91129f4575..3ef906c6ebc 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1347,6 +1347,8 @@ impl User {
         route = route
             .service(web::resource("").route(web::get().to(get_user_details)))
             .service(web::resource("/v2/signin").route(web::post().to(user_signin)))
+            // signin/signup with sso using openidconnect
+            .service(web::resource("/oidc").route(web::post().to(sso_sign)))
             .service(web::resource("/signout").route(web::post().to(signout)))
             .service(web::resource("/rotate_password").route(web::post().to(rotate_password)))
             .service(web::resource("/change_password").route(web::post().to(change_password)))
@@ -1410,7 +1412,8 @@ impl User {
                 )
                 .service(
                     web::resource("/list").route(web::get().to(list_user_authentication_methods)),
-                ),
+                )
+                .service(web::resource("/url").route(web::get().to(get_sso_auth_url))),
         );
 
         #[cfg(feature = "email")]
diff --git a/crates/router/src/routes/dummy_connector.rs b/crates/router/src/routes/dummy_connector.rs
index 79338fc620c..a0c85f90ef7 100644
--- a/crates/router/src/routes/dummy_connector.rs
+++ b/crates/router/src/routes/dummy_connector.rs
@@ -27,7 +27,7 @@ pub async fn dummy_connector_authorize_payment(
         state,
         &req,
         payload,
-        |state, _, req, _| core::payment_authorize(state, req),
+        |state, _: (), req, _| core::payment_authorize(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     )
@@ -51,7 +51,7 @@ pub async fn dummy_connector_complete_payment(
         state,
         &req,
         payload,
-        |state, _, req, _| core::payment_complete(state, req),
+        |state, _: (), req, _| core::payment_complete(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     )
@@ -70,7 +70,7 @@ pub async fn dummy_connector_payment(
         state,
         &req,
         payload,
-        |state, _, req, _| core::payment(state, req),
+        |state, _: (), req, _| core::payment(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     )
@@ -90,7 +90,7 @@ pub async fn dummy_connector_payment_data(
         state,
         &req,
         payload,
-        |state, _, req, _| core::payment_data(state, req),
+        |state, _: (), req, _| core::payment_data(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     )
@@ -111,7 +111,7 @@ pub async fn dummy_connector_refund(
         state,
         &req,
         payload,
-        |state, _, req, _| core::refund_payment(state, req),
+        |state, _: (), req, _| core::refund_payment(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     )
@@ -131,7 +131,7 @@ pub async fn dummy_connector_refund_data(
         state,
         &req,
         payload,
-        |state, _, req, _| core::refund_data(state, req),
+        |state, _: (), req, _| core::refund_data(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     )
diff --git a/crates/router/src/routes/health.rs b/crates/router/src/routes/health.rs
index 3e2f42bcc25..f15a0db1923 100644
--- a/crates/router/src/routes/health.rs
+++ b/crates/router/src/routes/health.rs
@@ -34,7 +34,7 @@ pub async fn deep_health_check(
         state,
         &request,
         (),
-        |state, _, _, _| deep_health_check_func(state),
+        |state, _: (), _, _| deep_health_check_func(state),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index b18720652cf..28698146410 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -229,7 +229,9 @@ impl From<Flow> for ApiIdentifier {
             | Flow::TwoFactorAuthStatus
             | Flow::CreateUserAuthenticationMethod
             | Flow::UpdateUserAuthenticationMethod
-            | Flow::ListUserAuthenticationMethods => Self::User,
+            | Flow::ListUserAuthenticationMethods
+            | Flow::GetSsoAuthUrl
+            | Flow::SignInWithSso => Self::User,
 
             Flow::ListRoles
             | Flow::GetRole
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 816e7f7e2f8..dae78d31bf0 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -72,7 +72,7 @@ pub async fn user_signup(
         state,
         &http_req,
         req_payload.clone(),
-        |state, _, req_body, _| async move {
+        |state, _: (), req_body, _| async move {
             if let Some(true) = is_token_only {
                 user_core::signup_token_only_flow(state, req_body).await
             } else {
@@ -99,7 +99,7 @@ pub async fn user_signin(
         state,
         &http_req,
         req_payload.clone(),
-        |state, _, req_body, _| async move {
+        |state, _: (), req_body, _| async move {
             if let Some(true) = is_token_only {
                 user_core::signin_token_only_flow(state, req_body).await
             } else {
@@ -127,7 +127,7 @@ pub async fn user_connect_account(
         state,
         &http_req,
         req_payload.clone(),
-        |state, _, req_body, _| user_core::connect_account(state, req_body, auth_id.clone()),
+        |state, _: (), req_body, _| user_core::connect_account(state, req_body, auth_id.clone()),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -397,7 +397,7 @@ pub async fn forgot_password(
         state.clone(),
         &req,
         payload.into_inner(),
-        |state, _, payload, _| user_core::forgot_password(state, payload, auth_id.clone()),
+        |state, _: (), payload, _| user_core::forgot_password(state, payload, auth_id.clone()),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -432,7 +432,7 @@ pub async fn reset_password(
             state.clone(),
             &req,
             payload.into_inner(),
-            |state, _, payload, _| user_core::reset_password(state, payload),
+            |state, _: (), payload, _| user_core::reset_password(state, payload),
             &auth::NoAuth,
             api_locking::LockAction::NotApplicable,
         ))
@@ -522,7 +522,7 @@ pub async fn accept_invite_from_email(
             state.clone(),
             &req,
             payload.into_inner(),
-            |state, _, request_payload, _| {
+            |state, _: (), request_payload, _| {
                 user_core::accept_invite_from_email(state, request_payload)
             },
             &auth::NoAuth,
@@ -560,7 +560,7 @@ pub async fn verify_email(
             state,
             &http_req,
             json_payload.into_inner(),
-            |state, _, req_payload, _| user_core::verify_email(state, req_payload),
+            |state, _: (), req_payload, _| user_core::verify_email(state, req_payload),
             &auth::NoAuth,
             api_locking::LockAction::NotApplicable,
         ))
@@ -582,7 +582,9 @@ pub async fn verify_email_request(
         state.clone(),
         &http_req,
         json_payload.into_inner(),
-        |state, _, req_body, _| user_core::send_verification_mail(state, req_body, auth_id.clone()),
+        |state, _: (), req_body, _| {
+            user_core::send_verification_mail(state, req_body, auth_id.clone())
+        },
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -774,6 +776,50 @@ pub async fn check_two_factor_auth_status(
     .await
 }
 
+pub async fn get_sso_auth_url(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    query: web::Query<user_api::GetSsoAuthUrlRequest>,
+) -> HttpResponse {
+    let flow = Flow::GetSsoAuthUrl;
+    let payload = query.into_inner();
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload,
+        |state, _: (), req, _| user_core::get_sso_auth_url(state, req),
+        &auth::NoAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn sso_sign(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_api::SsoSignInRequest>,
+) -> HttpResponse {
+    let flow = Flow::SignInWithSso;
+    let payload = json_payload.into_inner();
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload,
+        |state, user: Option<auth::UserFromSinglePurposeToken>, payload, _| {
+            user_core::sso_sign(state, payload, user)
+        },
+        auth::auth_type(
+            &auth::NoAuth,
+            &auth::SinglePurposeJWTAuth(TokenPurpose::SSO),
+            req.headers(),
+        ),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 pub async fn create_user_authentication_method(
     state: web::Data<AppState>,
     req: HttpRequest,
@@ -824,7 +870,7 @@ pub async fn list_user_authentication_methods(
         state.clone(),
         &req,
         query.into_inner(),
-        |state, _, req, _| user_core::list_user_authentication_methods(state, req),
+        |state, _: (), req, _| user_core::list_user_authentication_methods(state, req),
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs
index 7aed9fdb40a..8792f0c8d80 100644
--- a/crates/router/src/services.rs
+++ b/crates/router/src/services.rs
@@ -14,6 +14,9 @@ pub mod pm_auth;
 #[cfg(feature = "recon")]
 pub mod recon;
 
+#[cfg(feature = "olap")]
+pub mod openidconnect;
+
 use std::sync::Arc;
 
 use error_stack::ResultExt;
diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs
index add93e99119..7f8a01736ed 100644
--- a/crates/router/src/services/api/client.rs
+++ b/crates/router/src/services/api/client.rs
@@ -80,7 +80,7 @@ fn get_base_client(
 
 // We may need to use outbound proxy to connect to external world.
 // Precedence will be the environment variables, followed by the config.
-pub(super) fn create_client(
+pub fn create_client(
     proxy_config: &Proxy,
     should_bypass_proxy: bool,
     client_certificate: Option<masking::Secret<String>>,
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index 43d49b92efb..adca724e454 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -261,6 +261,20 @@ where
     }
 }
 
+#[async_trait]
+impl<A, T> AuthenticateAndFetch<Option<T>, A> for NoAuth
+where
+    A: SessionStateInfo + Sync,
+{
+    async fn authenticate_and_fetch(
+        &self,
+        _request_headers: &HeaderMap,
+        _state: &A,
+    ) -> RouterResult<(Option<T>, AuthenticationType)> {
+        Ok((None, AuthenticationType::NoAuth))
+    }
+}
+
 #[async_trait]
 impl<A> AuthenticateAndFetch<AuthenticationData, A> for ApiKeyAuth
 where
@@ -372,6 +386,40 @@ where
     }
 }
 
+#[cfg(feature = "olap")]
+#[async_trait]
+impl<A> AuthenticateAndFetch<Option<UserFromSinglePurposeToken>, A> for SinglePurposeJWTAuth
+where
+    A: SessionStateInfo + Sync,
+{
+    async fn authenticate_and_fetch(
+        &self,
+        request_headers: &HeaderMap,
+        state: &A,
+    ) -> RouterResult<(Option<UserFromSinglePurposeToken>, AuthenticationType)> {
+        let payload = parse_jwt_payload::<A, SinglePurposeToken>(request_headers, state).await?;
+        if payload.check_in_blacklist(state).await? {
+            return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
+        }
+
+        if self.0 != payload.purpose {
+            return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
+        }
+
+        Ok((
+            Some(UserFromSinglePurposeToken {
+                user_id: payload.user_id.clone(),
+                origin: payload.origin.clone(),
+                path: payload.path,
+            }),
+            AuthenticationType::SinglePurposeJwt {
+                user_id: payload.user_id,
+                purpose: payload.purpose,
+            },
+        ))
+    }
+}
+
 #[cfg(feature = "olap")]
 #[derive(Debug)]
 pub struct SinglePurposeOrLoginTokenAuth(pub TokenPurpose);
diff --git a/crates/router/src/services/openidconnect.rs b/crates/router/src/services/openidconnect.rs
new file mode 100644
index 00000000000..4f5056f3273
--- /dev/null
+++ b/crates/router/src/services/openidconnect.rs
@@ -0,0 +1,197 @@
+use error_stack::ResultExt;
+use masking::{ExposeInterface, Secret};
+use oidc::TokenResponse;
+use openidconnect::{self as oidc, core as oidc_core};
+use redis_interface::RedisConnectionPool;
+use storage_impl::errors::ApiClientError;
+
+use crate::{
+    consts,
+    core::errors::{UserErrors, UserResult},
+    routes::SessionState,
+    services::api::client,
+    types::domain::user::UserEmail,
+};
+
+pub async fn get_authorization_url(
+    state: SessionState,
+    redirect_url: String,
+    redirect_state: Secret<String>,
+    base_url: Secret<String>,
+    client_id: Secret<String>,
+) -> UserResult<url::Url> {
+    let discovery_document = get_discovery_document(base_url, &state).await?;
+
+    let (auth_url, csrf_token, nonce) =
+        get_oidc_core_client(discovery_document, client_id, None, redirect_url)?
+            .authorize_url(
+                oidc_core::CoreAuthenticationFlow::AuthorizationCode,
+                || oidc::CsrfToken::new(redirect_state.expose()),
+                oidc::Nonce::new_random,
+            )
+            .add_scope(oidc::Scope::new("email".to_string()))
+            .url();
+
+    // Save csrf & nonce as key value respectively
+    let key = get_oidc_redis_key(csrf_token.secret());
+    get_redis_connection(&state)?
+        .set_key_with_expiry(&key, nonce.secret(), consts::user::REDIS_SSO_TTL)
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to save csrf-nonce in redis")?;
+
+    Ok(auth_url)
+}
+
+pub async fn get_user_email_from_oidc_provider(
+    state: &SessionState,
+    redirect_url: String,
+    redirect_state: Secret<String>,
+    base_url: Secret<String>,
+    client_id: Secret<String>,
+    authorization_code: Secret<String>,
+    client_secret: Secret<String>,
+) -> UserResult<UserEmail> {
+    let nonce = get_nonce_from_redis(state, &redirect_state).await?;
+    let discovery_document = get_discovery_document(base_url, state).await?;
+    let client = get_oidc_core_client(
+        discovery_document,
+        client_id,
+        Some(client_secret),
+        redirect_url,
+    )?;
+
+    let nonce_clone = nonce.clone();
+    client
+        .authorize_url(
+            oidc_core::CoreAuthenticationFlow::AuthorizationCode,
+            || oidc::CsrfToken::new(redirect_state.expose()),
+            || nonce_clone,
+        )
+        .add_scope(oidc::Scope::new("email".to_string()));
+
+    // Send request to OpenId provider with authorization code
+    let token_response = client
+        .exchange_code(oidc::AuthorizationCode::new(authorization_code.expose()))
+        .request_async(|req| get_oidc_reqwest_client(state, req))
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to exchange code and fetch oidc token")?;
+
+    // Fetch id token from response
+    let id_token = token_response
+        .id_token()
+        .ok_or(UserErrors::InternalServerError)
+        .attach_printable("Id Token not provided in token response")?;
+
+    // Verify id token
+    let id_token_claims = id_token
+        .claims(&client.id_token_verifier(), &nonce)
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to verify id token")?;
+
+    // Get email from token
+    let email_from_token = id_token_claims
+        .email()
+        .map(|email| email.to_string())
+        .ok_or(UserErrors::InternalServerError)
+        .attach_printable("OpenID Provider Didnt provide email")?;
+
+    UserEmail::new(Secret::new(email_from_token))
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to create email type")
+}
+
+// TODO: Cache Discovery Document
+async fn get_discovery_document(
+    base_url: Secret<String>,
+    state: &SessionState,
+) -> UserResult<oidc_core::CoreProviderMetadata> {
+    let issuer_url =
+        oidc::IssuerUrl::new(base_url.expose()).change_context(UserErrors::InternalServerError)?;
+    oidc_core::CoreProviderMetadata::discover_async(issuer_url, |req| {
+        get_oidc_reqwest_client(state, req)
+    })
+    .await
+    .change_context(UserErrors::InternalServerError)
+}
+
+fn get_oidc_core_client(
+    discovery_document: oidc_core::CoreProviderMetadata,
+    client_id: Secret<String>,
+    client_secret: Option<Secret<String>>,
+    redirect_url: String,
+) -> UserResult<oidc_core::CoreClient> {
+    let client_id = oidc::ClientId::new(client_id.expose());
+    let client_secret = client_secret.map(|secret| oidc::ClientSecret::new(secret.expose()));
+    let redirect_url = oidc::RedirectUrl::new(redirect_url)
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Error creating redirect URL type")?;
+
+    Ok(
+        oidc_core::CoreClient::from_provider_metadata(discovery_document, client_id, client_secret)
+            .set_redirect_uri(redirect_url),
+    )
+}
+
+async fn get_nonce_from_redis(
+    state: &SessionState,
+    redirect_state: &Secret<String>,
+) -> UserResult<oidc::Nonce> {
+    let redis_connection = get_redis_connection(state)?;
+    let redirect_state = redirect_state.clone().expose();
+    let key = get_oidc_redis_key(&redirect_state);
+    redis_connection
+        .get_key::<Option<String>>(&key)
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Error Fetching CSRF from redis")?
+        .map(oidc::Nonce::new)
+        .ok_or(UserErrors::SSOFailed)
+        .attach_printable("Cannot find csrf in redis. Csrf invalid or expired")
+}
+
+async fn get_oidc_reqwest_client(
+    state: &SessionState,
+    request: oidc::HttpRequest,
+) -> Result<oidc::HttpResponse, ApiClientError> {
+    let client = client::create_client(&state.conf.proxy, false, None, None)
+        .map_err(|e| e.current_context().to_owned())?;
+
+    let mut request_builder = client
+        .request(request.method, request.url)
+        .body(request.body);
+    for (name, value) in &request.headers {
+        request_builder = request_builder.header(name.as_str(), value.as_bytes());
+    }
+
+    let request = request_builder
+        .build()
+        .map_err(|_| ApiClientError::ClientConstructionFailed)?;
+    let response = client
+        .execute(request)
+        .await
+        .map_err(|_| ApiClientError::RequestNotSent("OpenIDConnect".to_string()))?;
+
+    Ok(oidc::HttpResponse {
+        status_code: response.status(),
+        headers: response.headers().to_owned(),
+        body: response
+            .bytes()
+            .await
+            .map_err(|_| ApiClientError::ResponseDecodingFailed)?
+            .to_vec(),
+    })
+}
+
+fn get_oidc_redis_key(csrf: &str) -> String {
+    format!("{}OIDC_{}", consts::user::REDIS_SSO_PREFIX, csrf)
+}
+
+fn get_redis_connection(state: &SessionState) -> UserResult<std::sync::Arc<RedisConnectionPool>> {
+    state
+        .store
+        .get_redis_conn()
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to get redis connection")
+}
diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs
index 25c851549d1..6b957281f76 100644
--- a/crates/router/src/utils/user.rs
+++ b/crates/router/src/utils/user.rs
@@ -1,12 +1,14 @@
 use std::{collections::HashMap, sync::Arc};
 
 use api_models::user as user_api;
-use common_utils::errors::CustomResult;
-use diesel_models::{enums::UserStatus, user_role::UserRole};
+use common_utils::{errors::CustomResult, ext_traits::ValueExt};
+use diesel_models::{encryption::Encryption, enums::UserStatus, user_role::UserRole};
 use error_stack::ResultExt;
+use masking::{ExposeInterface, Secret};
 use redis_interface::RedisConnectionPool;
 
 use crate::{
+    consts::user::{REDIS_SSO_PREFIX, REDIS_SSO_TTL},
     core::errors::{StorageError, UserErrors, UserResult},
     routes::SessionState,
     services::{
@@ -78,7 +80,7 @@ pub async fn generate_jwt_auth_token(
     state: &SessionState,
     user: &UserFromStorage,
     user_role: &UserRole,
-) -> UserResult<masking::Secret<String>> {
+) -> UserResult<Secret<String>> {
     let token = AuthToken::new_token(
         user.get_user_id().to_string(),
         user_role.merchant_id.clone(),
@@ -87,7 +89,7 @@ pub async fn generate_jwt_auth_token(
         user_role.org_id.clone(),
     )
     .await?;
-    Ok(masking::Secret::new(token))
+    Ok(Secret::new(token))
 }
 
 pub async fn generate_jwt_auth_token_with_custom_role_attributes(
@@ -96,7 +98,7 @@ pub async fn generate_jwt_auth_token_with_custom_role_attributes(
     merchant_id: String,
     org_id: String,
     role_id: String,
-) -> UserResult<masking::Secret<String>> {
+) -> UserResult<Secret<String>> {
     let token = AuthToken::new_token(
         user.get_user_id().to_string(),
         merchant_id,
@@ -105,14 +107,14 @@ pub async fn generate_jwt_auth_token_with_custom_role_attributes(
         org_id,
     )
     .await?;
-    Ok(masking::Secret::new(token))
+    Ok(Secret::new(token))
 }
 
 pub fn get_dashboard_entry_response(
     state: &SessionState,
     user: UserFromStorage,
     user_role: UserRole,
-    token: masking::Secret<String>,
+    token: Secret<String>,
 ) -> UserResult<user_api::DashboardEntryResponse> {
     let verification_days_left = get_verification_days_left(state, &user)?;
 
@@ -189,7 +191,7 @@ pub async fn get_user_from_db_by_email(
         .map(UserFromStorage::from)
 }
 
-pub fn get_token_from_signin_response(resp: &user_api::SignInResponse) -> masking::Secret<String> {
+pub fn get_token_from_signin_response(resp: &user_api::SignInResponse) -> Secret<String> {
     match resp {
         user_api::SignInResponse::DashboardEntry(data) => data.token.clone(),
         user_api::SignInResponse::MerchantSelect(data) => data.token.clone(),
@@ -213,3 +215,74 @@ impl ForeignFrom<user_api::AuthConfig> for common_enums::UserAuthType {
         }
     }
 }
+
+pub async fn decrypt_oidc_private_config(
+    state: &SessionState,
+    encrypted_config: Option<Encryption>,
+) -> UserResult<user_api::OpenIdConnectPrivateConfig> {
+    let user_auth_key = hex::decode(
+        state
+            .conf
+            .user_auth_methods
+            .get_inner()
+            .encryption_key
+            .clone()
+            .expose(),
+    )
+    .change_context(UserErrors::InternalServerError)
+    .attach_printable("Failed to decode DEK")?;
+
+    let private_config = domain::types::decrypt::<serde_json::Value, masking::WithType>(
+        encrypted_config,
+        &user_auth_key,
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)
+    .attach_printable("Failed to decrypt private config")?
+    .ok_or(UserErrors::InternalServerError)
+    .attach_printable("Private config not found")?
+    .into_inner()
+    .expose();
+
+    private_config
+        .parse_value("OpenIdConnectPrivateConfig")
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("unable to parse OpenIdConnectPrivateConfig")
+}
+
+pub async fn set_sso_id_in_redis(
+    state: &SessionState,
+    oidc_state: Secret<String>,
+    sso_id: String,
+) -> UserResult<()> {
+    let connection = get_redis_connection(state)?;
+    let key = get_oidc_key(&oidc_state.expose());
+    connection
+        .set_key_with_expiry(&key, sso_id, REDIS_SSO_TTL)
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to set sso id in redis")
+}
+
+pub async fn get_sso_id_from_redis(
+    state: &SessionState,
+    oidc_state: Secret<String>,
+) -> UserResult<String> {
+    let connection = get_redis_connection(state)?;
+    let key = get_oidc_key(&oidc_state.expose());
+    connection
+        .get_key::<Option<String>>(&key)
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to get sso id from redis")?
+        .ok_or(UserErrors::SSOFailed)
+        .attach_printable("Cannot find oidc state in redis. Oidc state invalid or expired")
+}
+
+fn get_oidc_key(oidc_state: &str) -> String {
+    format!("{}{oidc_state}", REDIS_SSO_PREFIX)
+}
+
+pub fn get_oidc_sso_redirect_url(state: &SessionState, provider: &str) -> String {
+    format!("{}/redirect/oidc/{}", state.conf.user.base_url, provider)
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 0d146a9eb2b..af6f9be0482 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -432,6 +432,10 @@ pub enum Flow {
     UpdateUserAuthenticationMethod,
     // List user authentication methods
     ListUserAuthenticationMethods,
+    /// Get sso auth url
+    GetSsoAuthUrl,
+    /// Signin with SSO
+    SignInWithSso,
     /// List initial webhook delivery attempts
     WebhookEventInitialDeliveryAttemptList,
     /// List delivery attempts for a webhook event

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [X] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
Two new apis give 
- generate and redirect to openid provider.
- signin using data given by openid provider.
<!-- Describe your changes in detail -->


### Additional Changes

- [X] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
Required for users to signin using sso.
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
Redirect to sso 
```sh
curl --location '<BASE URL>/user/auth/url?id=<>' 
```


Signin with sso.
```sh
curl --location --request POST '<BASE URL>/user/oidc' \
--header 'Content-Type: application/json' \
--data-raw '{
    "state":"",
    "code": ""
}'
```


<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [X] I formatted the code `cargo +nightly fmt --all`
- [X] I addressed lints thrown by `cargo clippy`
- [X] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
