diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 774fdfb21a..02d151ea68 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -46,6 +46,7 @@ type AuthRequestRepo struct { IDPProviderViewProvider idpProviderViewProvider UserGrantProvider userGrantProvider ProjectProvider projectProvider + ApplicationProvider applicationProvider IdGenerator id.Generator @@ -111,6 +112,10 @@ type projectProvider interface { OrgProjectMappingByIDs(orgID, projectID string) (*project_view_model.OrgProjectMapping, error) } +type applicationProvider interface { + AppByOIDCClientID(context.Context, string) (*query.App, error) +} + func (repo *AuthRequestRepo) Health(ctx context.Context) error { return repo.AuthRequests.Health(ctx) } @@ -798,6 +803,13 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth return append(steps, &domain.GrantRequiredStep{}), nil } + ok, err = repo.hasSucceededPage(ctx, request, repo.ApplicationProvider) + if err != nil { + return nil, err + } + if ok { + steps = append(steps, &domain.LoginSucceededStep{}) + } return append(steps, &domain.RedirectToCallbackStep{}), nil } @@ -986,6 +998,14 @@ func (repo *AuthRequestRepo) getLoginTexts(ctx context.Context, aggregateID stri return iam_view_model.CustomTextViewsToDomain(loginTexts), err } +func (repo *AuthRequestRepo) hasSucceededPage(ctx context.Context, request *domain.AuthRequest, provider applicationProvider) (bool, error) { + app, err := provider.AppByOIDCClientID(ctx, request.ApplicationID) + if err != nil { + return false, err + } + return app.OIDCConfig.AppType == domain.OIDCApplicationTypeNative, nil +} + func setOrgID(orgViewProvider orgViewProvider, request *domain.AuthRequest) error { primaryDomain := request.GetScopeOrgPrimaryDomain() if primaryDomain == "" { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 8bebcb307c..82c66f6bb6 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -240,6 +240,17 @@ func (m *mockProject) OrgProjectMappingByIDs(orgID, projectID string) (*proj_vie return nil, errors.ThrowNotFound(nil, "ERROR", "error") } +type mockApp struct { + app *query.App +} + +func (m *mockApp) AppByOIDCClientID(ctx context.Context, id string) (*query.App, error) { + if m.app != nil { + return m.app, nil + } + return nil, errors.ThrowNotFound(nil, "ERROR", "error") +} + func TestAuthRequestRepo_nextSteps(t *testing.T) { type fields struct { AuthRequests *cache.AuthRequestCache @@ -250,6 +261,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { orgViewProvider orgViewProvider userGrantProvider userGrantProvider projectProvider projectProvider + applicationProvider applicationProvider loginPolicyProvider loginPolicyViewProvider lockoutPolicyProvider lockoutPolicyViewProvider PasswordCheckLifeTime time.Duration @@ -705,10 +717,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { IsEmailVerified: true, MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, - userGrantProvider: &mockUserGrants{}, - projectProvider: &mockProject{}, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + userGrantProvider: &mockUserGrants{}, + projectProvider: &mockProject{}, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}}, loginPolicyProvider: &mockLoginPolicy{ policy: &query.LoginPolicy{}, }, @@ -763,10 +776,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { IsEmailVerified: true, MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, - userGrantProvider: &mockUserGrants{}, - projectProvider: &mockProject{}, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + userGrantProvider: &mockUserGrants{}, + projectProvider: &mockProject{}, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}}, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ ShowFailures: true, @@ -994,10 +1008,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { IsEmailVerified: true, MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, - userGrantProvider: &mockUserGrants{}, - projectProvider: &mockProject{}, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + userGrantProvider: &mockUserGrants{}, + projectProvider: &mockProject{}, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}}, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ ShowFailures: true, @@ -1028,10 +1043,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { IsEmailVerified: true, MFAMaxSetUp: int32(model.MFALevelSecondFactor), }, - userEventProvider: &mockEventUser{}, - orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, - userGrantProvider: &mockUserGrants{}, - projectProvider: &mockProject{}, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + userGrantProvider: &mockUserGrants{}, + projectProvider: &mockProject{}, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}}, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ ShowFailures: true, @@ -1051,6 +1067,42 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []domain.NextStep{&domain.RedirectToCallbackStep{}}, nil, }, + { + "prompt none, checkLoggedIn true, authenticated and native, login succeeded step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().UTC().Add(-5 * time.Minute), + SecondFactorVerification: time.Now().UTC().Add(-5 * time.Minute), + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + IsEmailVerified: true, + MFAMaxSetUp: int32(model.MFALevelSecondFactor), + }, + userEventProvider: &mockEventUser{}, + orgViewProvider: &mockViewOrg{State: domain.OrgStateActive}, + userGrantProvider: &mockUserGrants{}, + projectProvider: &mockProject{}, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeNative}}}, + lockoutPolicyProvider: &mockLockoutPolicy{ + policy: &query.LockoutPolicy{ + ShowFailures: true, + }, + }, + PasswordCheckLifeTime: 10 * 24 * time.Hour, + SecondFactorCheckLifeTime: 18 * time.Hour, + }, + args{&domain.AuthRequest{ + UserID: "UserID", + Prompt: []domain.Prompt{domain.PromptNone}, + Request: &domain.AuthRequestOIDC{}, + LoginPolicy: &domain.LoginPolicy{ + SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeOTP}, + }, + }, true}, + []domain.NextStep{&domain.LoginSucceededStep{}, &domain.RedirectToCallbackStep{}}, + nil, + }, { "prompt none, checkLoggedIn true, authenticated and required user grants missing, grant required step", fields{ @@ -1107,7 +1159,8 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { roleCheck: true, userGrants: 2, }, - projectProvider: &mockProject{}, + projectProvider: &mockProject{}, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}}, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ ShowFailures: true, @@ -1184,6 +1237,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { projectCheck: true, hasProject: true, }, + applicationProvider: &mockApp{app: &query.App{OIDCConfig: &query.OIDCApp{AppType: domain.OIDCApplicationTypeWeb}}}, lockoutPolicyProvider: &mockLockoutPolicy{ policy: &query.LockoutPolicy{ ShowFailures: true, @@ -1279,6 +1333,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { OrgViewProvider: tt.fields.orgViewProvider, UserGrantProvider: tt.fields.userGrantProvider, ProjectProvider: tt.fields.projectProvider, + ApplicationProvider: tt.fields.applicationProvider, LoginPolicyViewProvider: tt.fields.loginPolicyProvider, LockoutPolicyViewProvider: tt.fields.lockoutPolicyProvider, PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 0c3427648f..59b69aac7d 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -122,6 +122,7 @@ func Start(conf Config, authZ authz.Config, systemDefaults sd.SystemDefaults, co Query: queries, UserGrantProvider: queryView, ProjectProvider: queryView, + ApplicationProvider: queries, IdGenerator: idGenerator, PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, ExternalLoginCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, diff --git a/internal/domain/next_step.go b/internal/domain/next_step.go index 01083b0d18..0b1b5fa2a9 100644 --- a/internal/domain/next_step.go +++ b/internal/domain/next_step.go @@ -28,6 +28,7 @@ const ( NextStepRegistration NextStepProjectRequired NextStepRedirectToExternalIDP + NextStepLoginSucceeded ) type LoginStep struct{} @@ -180,3 +181,9 @@ type RedirectToCallbackStep struct{} func (s *RedirectToCallbackStep) Type() NextStepType { return NextStepRedirectToCallback } + +type LoginSucceededStep struct{} + +func (s *LoginSucceededStep) Type() NextStepType { + return NextStepLoginSucceeded +} diff --git a/internal/ui/login/handler/login_success_handler.go b/internal/ui/login/handler/login_success_handler.go index b909eb1d8e..ee83b72634 100644 --- a/internal/ui/login/handler/login_success_handler.go +++ b/internal/ui/login/handler/login_success_handler.go @@ -21,8 +21,11 @@ func (l *Login) redirectToLoginSuccess(w http.ResponseWriter, r *http.Request, i func (l *Login) handleLoginSuccess(w http.ResponseWriter, r *http.Request) { authRequest, _ := l.getAuthRequest(r) - if authRequest != nil { - if !(len(authRequest.PossibleSteps) == 1 && authRequest.PossibleSteps[0].Type() == domain.NextStepRedirectToCallback) { + if authRequest == nil { + l.renderSuccessAndCallback(w, r, nil, nil) + } + for _, step := range authRequest.PossibleSteps { + if step.Type() != domain.NextStepLoginSucceeded && step.Type() != domain.NextStepRedirectToCallback { l.renderNextStep(w, r, authRequest) return } @@ -43,3 +46,8 @@ func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, } l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplLoginSuccess], data, nil) } + +func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) { + callback := l.oidcAuthCallbackURL + authReq.ID + http.Redirect(w, r, callback, http.StatusFound) +} diff --git a/internal/ui/login/handler/renderer.go b/internal/ui/login/handler/renderer.go index e58a589cf6..b8b7cbddf2 100644 --- a/internal/ui/login/handler/renderer.go +++ b/internal/ui/login/handler/renderer.go @@ -278,6 +278,8 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq * l.chooseNextStep(w, r, authReq, 1, err) return } + l.redirectToCallback(w, r, authReq) + case *domain.LoginSucceededStep: l.redirectToLoginSuccess(w, r, authReq.ID) case *domain.ChangePasswordStep: l.renderChangePassword(w, r, authReq, err)