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
+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)