Bug: feat: add endpoint for terminate auth select



Add support to terminate auth select, 
- the api will take Auth Select SPT and
- it will either SSO or TOTP SPT as per requested scenario

diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index 4f5651e0a3c..b357a3389d9 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -10,16 +10,17 @@ use crate::user::{
     dashboard_metadata::{
         GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
     },
-    AcceptInviteFromEmailRequest, AuthorizeResponse, BeginTotpResponse, ChangePasswordRequest,
-    ConnectAccountRequest, CreateInternalUserRequest, CreateUserAuthenticationMethodRequest,
-    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,
+    AcceptInviteFromEmailRequest, AuthSelectRequest, AuthorizeResponse, BeginTotpResponse,
+    ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
+    CreateUserAuthenticationMethodRequest, 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 {
@@ -83,7 +84,8 @@ common_utils::impl_misc_api_event_type!(
     CreateUserAuthenticationMethodRequest,
     UpdateUserAuthenticationMethodRequest,
     GetSsoAuthUrlRequest,
-    SsoSignInRequest
+    SsoSignInRequest,
+    AuthSelectRequest
 );
 
 #[cfg(feature = "dummy_connector")]
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index b2ed491b677..02a4a09dc94 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -372,3 +372,8 @@ pub struct SsoSignInRequest {
 pub struct AuthIdQueryParam {
     pub auth_id: Option<String>,
 }
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct AuthSelectRequest {
+    pub id: String,
+}
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index c4260a92948..3e9145c2d33 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -2312,3 +2312,41 @@ pub async fn sso_sign(
 
     auth::cookies::set_cookie_response(response, token)
 }
+
+pub async fn terminate_auth_select(
+    state: SessionState,
+    user_token: auth::UserFromSinglePurposeToken,
+    req: user_api::AuthSelectRequest,
+) -> UserResponse<user_api::TokenResponse> {
+    let user_from_db: domain::UserFromStorage = state
+        .global_store
+        .find_user_by_id(&user_token.user_id)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    let user_authentication_method = state
+        .store
+        .get_user_authentication_method_by_id(&req.id)
+        .await
+        .change_context(UserErrors::InternalServerError)?;
+
+    let current_flow = domain::CurrentFlow::new(user_token, domain::SPTFlow::AuthSelect.into())?;
+    let mut next_flow = current_flow.next(user_from_db.clone(), &state).await?;
+
+    // Skip SSO if continue with password(TOTP)
+    if next_flow.get_flow() == domain::UserFlow::SPTFlow(domain::SPTFlow::SSO)
+        && user_authentication_method.auth_type != common_enums::UserAuthType::OpenIdConnect
+    {
+        next_flow = next_flow.skip(user_from_db, &state).await?;
+    }
+    let token = next_flow.get_token(&state).await?;
+
+    auth::cookies::set_cookie_response(
+        user_api::TokenResponse {
+            token: token.clone(),
+            token_type: next_flow.get_flow().into(),
+        },
+        token,
+    )
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 80157c476b3..0cf7de4d33c 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1417,7 +1417,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))),
+                .service(web::resource("/url").route(web::get().to(get_sso_auth_url)))
+                .service(web::resource("/select").route(web::post().to(terminate_auth_select))),
         );
 
         #[cfg(feature = "email")]
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 115bcf6b9a0..03d12386918 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -232,7 +232,8 @@ impl From<Flow> for ApiIdentifier {
             | Flow::UpdateUserAuthenticationMethod
             | Flow::ListUserAuthenticationMethods
             | Flow::GetSsoAuthUrl
-            | Flow::SignInWithSso => Self::User,
+            | Flow::SignInWithSso
+            | Flow::AuthSelect => Self::User,
 
             Flow::ListRoles
             | Flow::GetRole
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index dae78d31bf0..7e55393cc57 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -876,3 +876,22 @@ pub async fn list_user_authentication_methods(
     ))
     .await
 }
+
+pub async fn terminate_auth_select(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_api::AuthSelectRequest>,
+) -> HttpResponse {
+    let flow = Flow::AuthSelect;
+
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        json_payload.into_inner(),
+        |state, user, req, _| user_core::terminate_auth_select(state, user, req),
+        &auth::SinglePurposeJWTAuth(TokenPurpose::AuthSelect),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs
index ef73b88015f..97fb69074a6 100644
--- a/crates/router/src/types/domain/user/decision_manager.rs
+++ b/crates/router/src/types/domain/user/decision_manager.rs
@@ -51,9 +51,8 @@ impl SPTFlow {
     ) -> UserResult<bool> {
         match self {
             // Auth
-            // AuthSelect and SSO flow are not enabled, once the terminate SSO API is ready, we can enable these flows
-            Self::AuthSelect => Ok(false),
-            Self::SSO => Ok(false),
+            Self::AuthSelect => Ok(true),
+            Self::SSO => Ok(true),
             // TOTP
             Self::TOTP => Ok(!path.contains(&TokenPurpose::SSO)),
             // Main email APIs
@@ -311,6 +310,26 @@ impl NextFlow {
             }
         }
     }
+
+    pub async fn skip(self, user: UserFromStorage, state: &SessionState) -> UserResult<Self> {
+        let flows = self.origin.get_flows();
+        let index = flows
+            .iter()
+            .position(|flow| flow == &self.get_flow())
+            .ok_or(UserErrors::InternalServerError)?;
+        let remaining_flows = flows.iter().skip(index + 1);
+        for flow in remaining_flows {
+            if flow.is_required(&user, &self.path, state).await? {
+                return Ok(Self {
+                    origin: self.origin.clone(),
+                    next_flow: *flow,
+                    user,
+                    path: self.path,
+                });
+            }
+        }
+        Err(UserErrors::InternalServerError.into())
+    }
 }
 
 impl From<UserFlow> for TokenPurpose {
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 023cd2a7944..0b8f7f184da 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -155,7 +155,7 @@ pub enum Flow {
     PaymentsStart,
     /// Payments list flow.
     PaymentsList,
-    // Payments filters flow
+    /// Payments filters flow
     PaymentsFilters,
     #[cfg(feature = "payouts")]
     /// Payouts create flow
@@ -412,7 +412,7 @@ pub enum Flow {
     UserFromEmail,
     /// Begin TOTP
     TotpBegin,
-    // Reset TOTP
+    /// Reset TOTP
     TotpReset,
     /// Verify TOTP
     TotpVerify,
@@ -422,20 +422,22 @@ pub enum Flow {
     RecoveryCodeVerify,
     /// Generate or Regenerate recovery codes
     RecoveryCodesGenerate,
-    // Terminate two factor authentication
+    /// Terminate two factor authentication
     TerminateTwoFactorAuth,
-    // Check 2FA status
+    /// Check 2FA status
     TwoFactorAuthStatus,
-    // Create user authentication method
+    /// Create user authentication method
     CreateUserAuthenticationMethod,
-    // Update user authentication method
+    /// Update user authentication method
     UpdateUserAuthenticationMethod,
-    // List user authentication methods
+    /// List user authentication methods
     ListUserAuthenticationMethods,
     /// Get sso auth url
     GetSsoAuthUrl,
     /// Signin with SSO
     SignInWithSso,
+    /// Auth Select
+    AuthSelect,
     /// 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
- Add endpoint `/auth/select` to terminate auth select
- The api will take Auth Select SPT and will give SSO SPT or TOTP SPT based on the choice selected
- SSO SPT if user selects continue with SSO
- TOTP SPT if user selects continue with TOTP

### Additional Changes

- [ ] 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
Closes #5127 



## How did you test it?
If request from id that has auth type as `open_id_connect`:
```
curl --location 'http://localhost:8080/user/auth/select' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer SPT' \
--header 'Cookie: Cookie_1=value' \
--data '{
    "id":"f20742d2-f1b8-4dad-8b2d-cf8fe6d457eb"

}'
```
response will contain token type as `sso`
```
{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYjJkNjMxZDUtMGQzMi00Y2IxLTg2MTQtNWVlNWI2MDczYTVkIiwicHVycG9zZSI6InNzbyIsIm9yaWdpbiI6ImFjY2VwdF9pbnZpdGF0aW9uX2Zyb21fZW1haWwiLCJwYXRoIjpbImF1dGhfc2VsZWN0Il0sImV4cCI6MTcxOTU4NDc2NX0.ChD9An8L4szVjLn9Q51otVOXOuZf8kdgRMmH865lQQI",
    "token_type": "sso"
}
```
If request from id other than `open_id_connect` 
```
curl --location 'http://localhost:8080/user/auth/select' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer SPT' \
--header 'Cookie: Cookie_1=value' \
--data '{

    "id":"65ff02eb-f3dc-4afc-b8ac-cef2a30c4d12"

}'
```
Response will contain token type as `totp`
```
{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYjJkNjMxZDUtMGQzMi00Y2IxLTg2MTQtNWVlNWI2MDczYTVkIiwicHVycG9zZSI6InRvdHAiLCJvcmlnaW4iOiJhY2NlcHRfaW52aXRhdGlvbl9mcm9tX2VtYWlsIiwicGF0aCI6WyJhdXRoX3NlbGVjdCJdLCJleHAiOjE3MTk1ODQ5MDl9.hEBp49fycxiSW1VoyPR6s9TISO4lQKP3ta0S4SH3C9s",
    "token_type": "totp"
}
```
Db entries for correponding request ids:
![Screenshot 2024-06-26 at 7 59 41 PM](https://github.com/juspay/hyperswitch/assets/64925866/0634150a-91c8-4cfa-bfe3-3412eba7f151)

## 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
