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
@@ -1038,3 +1038,109 @@ func TestServer_RetrieveIdentityProviderIntent(t *testing.T) {
})
}
}
func TestServer_StartIdentityProviderIntent_WithLoginHint(t *testing.T) {
oauthIdpResp := Instance.AddGenericOAuthProvider(IamCTX, integration.IDPName())
type args struct {
ctx context.Context
req *user.StartIdentityProviderIntentRequest
}
type want struct {
url string
parametersExisting []string
parametersEqual map[string]string
}
tests := []struct {
name string
args args
want want
wantErr bool
}{
{
name: "OAuth with login_hint",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: oauthIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
LoginHint: "[email protected]",
},
},
},
},
want: want{
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
"login_hint": "[email protected]",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
{
name: "Without login_hint",
args: args{
OrgCTX,
&user.StartIdentityProviderIntentRequest{
IdpId: oauthIdpResp.Id,
Content: &user.StartIdentityProviderIntentRequest_Urls{
Urls: &user.RedirectURLs{
SuccessUrl: "https://example.com/success",
FailureUrl: "https://example.com/failure",
// No LoginHint provided
},
},
},
},
want: want{
url: "https://example.com/oauth/v2/authorize",
parametersEqual: map[string]string{
"client_id": "clientID",
"prompt": "select_account",
"redirect_uri": "http://" + Instance.Domain + ":8082/idps/callback",
"response_type": "code",
"scope": "openid profile email",
},
parametersExisting: []string{"state"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.StartIdentityProviderIntent(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
authUrl, err := url.Parse(got.GetAuthUrl())
require.NoError(t, err)
assert.Equal(t, tt.want.url, authUrl.Scheme+"://"+authUrl.Host+authUrl.Path)
for _, param := range tt.want.parametersExisting {
assert.True(t, authUrl.Query().Has(param), "Expected parameter %s to exist", param)
}
for key, expectedValue := range tt.want.parametersEqual {
actualValue := authUrl.Query().Get(key)
assert.Equal(t, expectedValue, actualValue, "Parameter %s should have value %s but got %s", key, expectedValue, actualValue)
}
if tt.args.req.GetContent().(*user.StartIdentityProviderIntentRequest_Urls).Urls.LoginHint == "" {
assert.False(t, authUrl.Query().Has("login_hint"), "login_hint should not be in URL when not provided or empty")
}
})
}
}
+5 -5
View File
@@ -36,7 +36,7 @@ import (
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.Request[user.StartIdentityProviderIntentRequest]) (_ *connect.Response[user.StartIdentityProviderIntentResponse], err error) {
switch t := req.Msg.GetContent().(type) {
case *user.StartIdentityProviderIntentRequest_Urls:
return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls)
return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls, t.Urls.LoginHint)
case *user.StartIdentityProviderIntentRequest_Ldap:
return s.startLDAPIntent(ctx, req.Msg.GetIdpId(), t.Ldap)
default:
@@ -44,12 +44,12 @@ func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.R
}
}
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs, loginHint string) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID), loginHint)
if err != nil {
return nil, err
}
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), loginHint, session.PersistentParameters())
if err != nil {
return nil, err
}
@@ -78,7 +78,7 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), "", nil)
if err != nil {
return nil, err
}
+5 -5
View File
@@ -368,7 +368,7 @@ func userGrantsToIDs(userGrants []*query.UserGrant) []string {
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.Request[user.StartIdentityProviderIntentRequest]) (_ *connect.Response[user.StartIdentityProviderIntentResponse], err error) {
switch t := req.Msg.GetContent().(type) {
case *user.StartIdentityProviderIntentRequest_Urls:
return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls)
return s.startIDPIntent(ctx, req.Msg.GetIdpId(), t.Urls, t.Urls.LoginHint)
case *user.StartIdentityProviderIntentRequest_Ldap:
return s.startLDAPIntent(ctx, req.Msg.GetIdpId(), t.Ldap)
default:
@@ -376,12 +376,12 @@ func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *connect.R
}
}
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs, loginHint string) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID), loginHint)
if err != nil {
return nil, err
}
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), loginHint, session.PersistentParameters())
if err != nil {
return nil, err
}
@@ -410,7 +410,7 @@ func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.Re
}
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*connect.Response[user.StartIdentityProviderIntentResponse], error) {
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), "", nil)
if err != nil {
return nil, err
}