mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 17:19:15 +08:00
feat(users): implemented openidconnect (#5124)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
268
Cargo.lock
generated
268
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -130,7 +130,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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1351,6 +1351,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)))
|
||||
@ -1414,7 +1416,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")]
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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,
|
||||
))
|
||||
|
||||
@ -230,7 +230,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
|
||||
|
||||
@ -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,
|
||||
))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>>,
|
||||
|
||||
@ -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);
|
||||
|
||||
197
crates/router/src/services/openidconnect.rs
Normal file
197
crates/router/src/services/openidconnect.rs
Normal file
@ -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")
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user