fix: added login_hint to the idp intent (#11552)

# Which Problems Are Solved

In login V2, when the user started the login flow of an external IDP,
there was no login_hint passed to the external provider. This required
the user to enter their username again. Whilst they already entered it
in Zitadel. By adding the `login_hint` parameter, the username should
already be filled in on the external idp.

# How the Problems Are Solved

Pass the `login_hint` parameter to the external idp.

# Additional Changes

Also added the `login_hint` to the event-store for audit trailing.

# Additional Context


- Closes #11392

---------

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Wim Van Laer
2026-02-13 14:34:32 +00:00
committed by GitHub
co-authored by Copilot
parent 2a2d5392a3
commit ca6bd9d6a2
11 changed files with 225 additions and 19 deletions
+10 -5
View File
@@ -27,7 +27,7 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string, idpArguments map[string]any) preparation.Validation {
func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, successURL, failureURL string, loginHint string, idpArguments map[string]any) preparation.Validation {
return func() (_ preparation.CreateCommands, err error) {
if idpID == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing")
@@ -55,6 +55,7 @@ func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, s
successURL,
failureURL,
idpID,
loginHint,
idpArguments,
),
}, nil
@@ -62,7 +63,7 @@ func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID, s
}
}
func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL, failureURL, resourceOwner string, idpArguments map[string]any) (*IDPIntentWriteModel, *domain.ObjectDetails, error) {
func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL, failureURL, resourceOwner, loginHint string, idpArguments map[string]any) (*IDPIntentWriteModel, *domain.ObjectDetails, error) {
if intentID == "" {
var err error
intentID, err = c.idGenerator.Next()
@@ -73,7 +74,7 @@ func (c *Commands) CreateIntent(ctx context.Context, intentID, idpID, successURL
writeModel := NewIDPIntentWriteModel(intentID, resourceOwner, c.maxIdPIntentLifetime)
//nolint: staticcheck
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, idpArguments))
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL, loginHint, idpArguments))
if err != nil {
return nil, nil, err
}
@@ -135,7 +136,7 @@ func (c *Commands) GetActiveIntent(ctx context.Context, intentID string) (*IDPIn
return intent, nil
}
func (c *Commands) AuthFromProvider(ctx context.Context, idpID, idpCallback, samlRootURL string) (state string, session idp.Session, err error) {
func (c *Commands) AuthFromProvider(ctx context.Context, idpID, idpCallback, samlRootURL string, loginHint string) (state string, session idp.Session, err error) {
state, err = c.idGenerator.Next()
if err != nil {
return "", nil, err
@@ -144,7 +145,11 @@ func (c *Commands) AuthFromProvider(ctx context.Context, idpID, idpCallback, sam
if err != nil {
return "", nil, err
}
session, err = provider.BeginAuth(ctx, state)
var params []idp.Parameter
if loginHint != "" {
params = append(params, idp.LoginHintParam(loginHint))
}
session, err = provider.BeginAuth(ctx, state, params...)
return state, session, err
}
+2
View File
@@ -21,6 +21,7 @@ type IDPIntentWriteModel struct {
IDPUserID string
IDPUserName string
UserID string
LoginHint string
IDPAccessToken *crypto.CryptoValue
IDPIDToken string
@@ -97,6 +98,7 @@ func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) {
wm.SuccessURL = e.SuccessURL
wm.FailureURL = e.FailureURL
wm.IDPID = e.IDPID
wm.LoginHint = e.LoginHint
wm.IDPArguments = e.IDPArguments
wm.State = domain.IDPIntentStateStarted
}
+76 -3
View File
@@ -49,6 +49,7 @@ func TestCommands_CreateIntent(t *testing.T) {
successURL string
failureURL string
instanceID string
loginHint string
idpArguments map[string]any
}
type res struct {
@@ -202,6 +203,7 @@ func TestCommands_CreateIntent(t *testing.T) {
success,
failure,
"idp",
"",
nil,
)
}(),
@@ -257,6 +259,7 @@ func TestCommands_CreateIntent(t *testing.T) {
success,
failure,
"idp",
"",
map[string]interface{}{
"verifier": "pkceOAuthVerifier",
},
@@ -317,6 +320,7 @@ func TestCommands_CreateIntent(t *testing.T) {
success,
failure,
"idp",
"",
map[string]interface{}{
"verifier": "pkceOAuthVerifier",
},
@@ -377,6 +381,7 @@ func TestCommands_CreateIntent(t *testing.T) {
success,
failure,
"idp",
"",
map[string]interface{}{
"verifier": "pkceOAuthVerifier",
},
@@ -408,7 +413,7 @@ func TestCommands_CreateIntent(t *testing.T) {
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
}
intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.intentID, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID, tt.args.idpArguments)
intentWriteModel, details, err := c.CreateIntent(tt.args.ctx, tt.args.intentID, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.instanceID, tt.args.loginHint, tt.args.idpArguments)
require.ErrorIs(t, err, tt.res.err)
if intentWriteModel != nil {
assert.Equal(t, tt.res.intentID, intentWriteModel.AggregateID)
@@ -429,6 +434,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
type args struct {
ctx context.Context
idpID string
loginHint string
callbackURL string
samlRootURL string
}
@@ -582,6 +588,68 @@ func TestCommands_AuthFromProvider(t *testing.T) {
auth: &idp.RedirectAuth{RedirectURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=id"},
},
},
{
"oauth auth redirect with login_hint",
fields{
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
"auth",
"token",
"user",
"idAttribute",
nil,
true,
rep_idp.Options{},
)),
),
expectFilter(
eventFromEventPusherWithInstanceID(
"instance",
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
"idp",
"name",
"clientID",
&crypto.CryptoValue{
CryptoType: crypto.TypeEncryption,
Algorithm: "enc",
KeyID: "id",
Crypted: []byte("clientSecret"),
},
"auth",
"token",
"user",
"idAttribute",
nil,
true,
rep_idp.Options{},
)),
),
),
idGenerator: mock.ExpectID(t, "id"),
},
args{
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
idpID: "idp",
callbackURL: "url",
loginHint: "[email protected]",
},
res{
auth: &idp.RedirectAuth{RedirectURL: "auth?client_id=clientID&login_hint=user%40example.com&redirect_uri=url&response_type=code&state=id"},
},
},
{
"migrated and push",
fields{
@@ -681,7 +749,7 @@ func TestCommands_AuthFromProvider(t *testing.T) {
idpConfigEncryption: tt.fields.secretCrypto,
idGenerator: tt.fields.idGenerator,
}
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL)
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL, tt.args.loginHint)
require.ErrorIs(t, err, tt.res.err)
var got idp.Auth
@@ -705,6 +773,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
idpID string
callbackURL string
samlRootURL string
loginHint string
}
type res struct {
url string
@@ -779,6 +848,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
success,
failure,
"idp",
"[email protected]",
nil,
)
}(),
@@ -801,6 +871,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
idpID: "idp",
callbackURL: "url",
samlRootURL: "samlurl",
loginHint: "[email protected]",
},
res{
url: "http://localhost:8000/sso",
@@ -872,6 +943,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
success,
failure,
"idp",
"user123",
nil,
)
}(),
@@ -894,6 +966,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
idpID: "idp",
callbackURL: "url",
samlRootURL: "samlurl",
loginHint: "user123",
},
res{
url: "http://localhost:8000/sso",
@@ -911,7 +984,7 @@ func TestCommands_AuthFromProvider_SAML(t *testing.T) {
idpConfigEncryption: tt.fields.secretCrypto,
idGenerator: tt.fields.idGenerator,
}
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL)
_, session, err := c.AuthFromProvider(tt.args.ctx, tt.args.idpID, tt.args.callbackURL, tt.args.samlRootURL, tt.args.loginHint)
require.ErrorIs(t, err, tt.res.err)
auth, err := session.GetAuth(tt.args.ctx)
+1
View File
@@ -993,6 +993,7 @@ func TestCommands_updateSession(t *testing.T) {
nil,
nil,
"idpID",
"",
nil,
),
),