diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index c37dd8a79f..9e797b8d0b 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -921,11 +921,15 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth steps = append(steps, new(domain.ExternalNotFoundOptionStep)) return steps, nil } - steps = append(steps, new(domain.LoginStep)) if domain.IsPrompt(request.Prompt, domain.PromptCreate) { return append(steps, &domain.RegistrationStep{}), nil } - if len(request.Prompt) == 0 || domain.IsPrompt(request.Prompt, domain.PromptSelectAccount) { + // if there's a login or consent prompt, but not select account, just return the login step + if len(request.Prompt) > 0 && !domain.IsPrompt(request.Prompt, domain.PromptSelectAccount) { + return append(steps, new(domain.LoginStep)), nil + } else { + // if no user was specified, no prompt or select_account was provided, + // then check the active user sessions (of the user agent) users, err := repo.usersForUserSelection(request) if err != nil { return nil, err @@ -936,11 +940,19 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth if request.SelectedIDPConfigID != "" { steps = append(steps, &domain.RedirectToExternalIDPStep{}) } - if len(request.Prompt) == 0 && len(users) > 0 { + if len(request.Prompt) == 0 && len(users) == 0 { + steps = append(steps, new(domain.LoginStep)) + } + // if no prompt was provided, but there are multiple user sessions, then the user must decide which to use + if len(request.Prompt) == 0 && len(users) > 1 { steps = append(steps, &domain.SelectUserStep{Users: users}) } + if len(steps) > 0 { + return steps, nil + } + // a single user session was found, use that automatically + request.UserID = users[0].UserID } - return steps, nil } user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, request.UserID, request.LoginPolicy.IgnoreUnknownUsernames) if err != nil { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 9603387952..c94b57d7eb 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -296,6 +296,28 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { []domain.NextStep{&domain.RedirectToCallbackStep{}}, nil, }, + { + "user not set prompt create, registration step", + fields{ + userSessionViewProvider: &mockViewNoUserSession{}, + }, + args{&domain.AuthRequest{ + Prompt: []domain.Prompt{domain.PromptCreate}, + }, false}, + []domain.NextStep{&domain.RegistrationStep{}}, + nil, + }, + { + "user not set, prompts other than select account, create step", + fields{ + userSessionViewProvider: &mockViewNoUserSession{}, + }, + args{&domain.AuthRequest{ + Prompt: []domain.Prompt{domain.PromptLogin, domain.PromptConsent}, + }, false}, + []domain.NextStep{&domain.LoginStep{}}, + nil, + }, { "user not set no active session, login step", fields{ @@ -333,7 +355,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { errors.IsInternal, }, { - "user not set, prompt select account, login and select account steps", + "user not set, prompt select account, select account step", fields{ userSessionViewProvider: &mockViewUserSession{ Users: []mockUser{ @@ -353,7 +375,6 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, args{&domain.AuthRequest{Prompt: []domain.Prompt{domain.PromptSelectAccount}}, false}, []domain.NextStep{ - &domain.LoginStep{}, &domain.SelectUserStep{ Users: []domain.UserSelection{ { @@ -373,7 +394,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { nil, }, { - "user not set, primary domain set, prompt select account, login and select account steps", + "user not set, primary domain set, prompt select account, select account step", fields{ userSessionViewProvider: &mockViewUserSession{ Users: []mockUser{ @@ -393,7 +414,6 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, args{&domain.AuthRequest{Prompt: []domain.Prompt{domain.PromptSelectAccount}, RequestedOrgID: "orgID1"}, false}, []domain.NextStep{ - &domain.LoginStep{}, &domain.SelectUserStep{ Users: []domain.UserSelection{ { @@ -407,7 +427,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { nil, }, { - "user not set, prompt select account, no active session, login and select account steps", + "user not set, prompt select account, no active session, select account step", fields{ userSessionViewProvider: &mockViewUserSession{ Users: nil, @@ -416,12 +436,113 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) { }, args{&domain.AuthRequest{Prompt: []domain.Prompt{domain.PromptSelectAccount}}, false}, []domain.NextStep{ - &domain.LoginStep{}, &domain.SelectUserStep{ Users: []domain.UserSelection{}, }}, nil, }, + { + "user not set single active session, callback step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + PasswordVerification: time.Now().Add(-5 * time.Minute), + SecondFactorVerification: time.Now().Add(-5 * time.Minute), + Users: []mockUser{ + { + "id1", + "loginname1", + "orgID1", + }, + }, + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + IsEmailVerified: true, + MFAMaxSetUp: int32(domain.MFALevelSecondFactor), + }, + 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, + }, + }, + idpUserLinksProvider: &mockIDPUserLinks{}, + }, + args{&domain.AuthRequest{ + Request: &domain.AuthRequestOIDC{}, + LoginPolicy: &domain.LoginPolicy{ + SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, + PasswordCheckLifetime: 10 * 24 * time.Hour, + SecondFactorCheckLifetime: 18 * time.Hour, + }, + }, false}, + []domain.NextStep{&domain.RedirectToCallbackStep{}}, + nil, + }, + { + "user not set multiple active sessions, select account step", + fields{ + userSessionViewProvider: &mockViewUserSession{ + Users: []mockUser{ + { + "id1", + "loginname1", + "orgID1", + }, + { + "id2", + "loginname2", + "orgID2", + }, + }, + }, + userViewProvider: &mockViewUser{ + PasswordSet: true, + IsEmailVerified: true, + MFAMaxSetUp: int32(domain.MFALevelSecondFactor), + }, + 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, + }, + }, + idpUserLinksProvider: &mockIDPUserLinks{}, + }, + args{&domain.AuthRequest{ + Request: &domain.AuthRequestOIDC{}, + LoginPolicy: &domain.LoginPolicy{ + SecondFactors: []domain.SecondFactorType{domain.SecondFactorTypeTOTP}, + PasswordCheckLifetime: 10 * 24 * time.Hour, + SecondFactorCheckLifetime: 18 * time.Hour, + }, + }, false}, + []domain.NextStep{&domain.SelectUserStep{ + Users: []domain.UserSelection{ + { + UserID: "id1", + LoginName: "loginname1", + SelectionPossible: true, + ResourceOwner: "orgID1", + }, + { + UserID: "id2", + LoginName: "loginname2", + SelectionPossible: true, + ResourceOwner: "orgID2", + }, + }, + }}, + nil, + }, { "user not found, not found error", fields{