fix: automatically link user without prompt (#8487)

# Which Problems Are Solved

There were UX issue with the autolinking prompt page and users were not
able to link their account or would not understand what to do. Since the
trust to the IdP is already bound by the configuration, the user can
directly be linked without any user input.

# How the Problems Are Solved

- remove the prompt page and directly link the user if possible
- remove corresponding customization texts from the API and Console

# Additional Changes

None

# Additional Context

- relates to https://github.com/zitadel/zitadel/issues/7977
- discussed with customers
- created as a `fix` to be able to backport

---------

Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
Livio Spring
2024-08-28 07:33:20 +02:00
committed by GitHub
parent bc2c2feefd
commit ca8f82423a
35 changed files with 21 additions and 491 deletions

View File

@@ -172,7 +172,6 @@ func SetLoginTextToDomain(req *admin_pb.SetCustomLoginTextsRequest) *domain.Cust
result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText)
result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText)
result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText)
result.LinkingUserPrompt = text.LinkingUserPromptScreenTextPbToDomain(req.LinkingUserPromptText)
result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText)
result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText)
result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText)

View File

@@ -1063,7 +1063,6 @@ func (s *Server) getCustomLoginTexts(ctx context.Context, org string, languages
RegistrationUserText: text_grpc.RegistrationUserScreenTextToPb(text.RegistrationUser),
ExternalRegistrationUserOverviewText: text_grpc.ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview),
RegistrationOrgText: text_grpc.RegistrationOrgScreenTextToPb(text.RegistrationOrg),
LinkingUserPromptText: text_grpc.LinkingUserPromptScreenTextToPb(text.LinkingUserPrompt),
LinkingUserDoneText: text_grpc.LinkingUserDoneScreenTextToPb(text.LinkingUsersDone),
ExternalUserNotFoundText: text_grpc.ExternalUserNotFoundScreenTextToPb(text.ExternalNotFound),
SuccessLoginText: text_grpc.SuccessLoginScreenTextToPb(text.LoginSuccess),

View File

@@ -171,7 +171,6 @@ func SetLoginCustomTextToDomain(req *mgmt_pb.SetCustomLoginTextsRequest) *domain
result.RegistrationUser = text.RegistrationUserScreenTextPbToDomain(req.RegistrationUserText)
result.ExternalRegistrationUserOverview = text.ExternalRegistrationUserOverviewScreenTextPbToDomain(req.ExternalRegistrationUserOverviewText)
result.RegistrationOrg = text.RegistrationOrgScreenTextPbToDomain(req.RegistrationOrgText)
result.LinkingUserPrompt = text.LinkingUserPromptScreenTextPbToDomain(req.LinkingUserPromptText)
result.LinkingUsersDone = text.LinkingUserDoneScreenTextPbToDomain(req.LinkingUserDoneText)
result.ExternalNotFound = text.ExternalUserNotFoundScreenTextPbToDomain(req.ExternalUserNotFoundText)
result.LoginSuccess = text.SuccessLoginScreenTextPbToDomain(req.SuccessLoginText)

View File

@@ -64,7 +64,6 @@ func CustomLoginTextToPb(text *domain.CustomLoginText) *text_pb.LoginCustomText
RegistrationUserText: RegistrationUserScreenTextToPb(text.RegistrationUser),
ExternalRegistrationUserOverviewText: ExternalRegistrationUserOverviewScreenTextToPb(text.ExternalRegistrationUserOverview),
RegistrationOrgText: RegistrationOrgScreenTextToPb(text.RegistrationOrg),
LinkingUserPromptText: LinkingUserPromptScreenTextToPb(text.LinkingUserPrompt),
LinkingUserDoneText: LinkingUserDoneScreenTextToPb(text.LinkingUsersDone),
ExternalUserNotFoundText: ExternalUserNotFoundScreenTextToPb(text.ExternalNotFound),
SuccessLoginText: SuccessLoginScreenTextToPb(text.LoginSuccess),
@@ -424,15 +423,6 @@ func LinkingUserDoneScreenTextToPb(text domain.LinkingUserDoneScreenText) *text_
}
}
func LinkingUserPromptScreenTextToPb(text domain.LinkingUserPromptScreenText) *text_pb.LinkingUserPromptScreenText {
return &text_pb.LinkingUserPromptScreenText{
Title: text.Title,
Description: text.Description,
LinkButtonText: text.LinkButtonText,
OtherButtonText: text.OtherButtonText,
}
}
func ExternalUserNotFoundScreenTextToPb(text domain.ExternalUserNotFoundScreenText) *text_pb.ExternalUserNotFoundScreenText {
return &text_pb.ExternalUserNotFoundScreenText{
Title: text.Title,
@@ -902,15 +892,6 @@ func RegistrationOrgScreenTextPbToDomain(text *text_pb.RegistrationOrgScreenText
}
}
func LinkingUserPromptScreenTextPbToDomain(text *text_pb.LinkingUserPromptScreenText) domain.LinkingUserPromptScreenText {
return domain.LinkingUserPromptScreenText{
Title: text.GetTitle(),
Description: text.GetDescription(),
LinkButtonText: text.GetLinkButtonText(),
OtherButtonText: text.GetOtherButtonText(),
}
}
func LinkingUserDoneScreenTextPbToDomain(text *text_pb.LinkingUserDoneScreenText) domain.LinkingUserDoneScreenText {
if text == nil {
return domain.LinkingUserDoneScreenText{}

View File

@@ -455,9 +455,9 @@ func (l *Login) handleExternalUserAuthenticated(
// checkAutoLinking checks if a user with the provided information (username or email) already exists within ZITADEL.
// The decision, which information will be checked is based on the IdP template option.
// The function returns a boolean whether a user was found or not.
// If single a user was found, it will be automatically linked.
func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) bool {
queries := make([]query.SearchQuery, 0, 2)
var user *query.NotifyUser
switch provider.AutoLinking {
case domain.AutoLinkingOptionUnspecified:
// is auto linking is disable, we shouldn't even get here, but in case we do we can directly return
@@ -472,7 +472,7 @@ func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq
if err != nil {
return false
}
l.renderLinkingUserPrompt(w, r, authReq, user, nil)
l.autoLinkUser(w, r, authReq, user)
return true
}
// If a specific org has been requested, we'll check the provided username against usernames (of that org).
@@ -501,10 +501,22 @@ func (l *Login) checkAutoLinking(w http.ResponseWriter, r *http.Request, authReq
if err != nil {
return false
}
l.renderLinkingUserPrompt(w, r, authReq, user, nil)
l.autoLinkUser(w, r, authReq, user)
return true
}
func (l *Login) autoLinkUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser) {
if err := l.authRepo.SelectUser(r.Context(), authReq.ID, user.ID, authReq.AgentID); err != nil {
l.renderError(w, r, authReq, err)
return
}
if err := l.authRepo.LinkExternalUsers(r.Context(), authReq.ID, authReq.AgentID, domain.BrowserInfoFromRequest(r)); err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
// externalUserNotExisting is called if an externalAuthentication couldn't find a corresponding externalID
// possible solutions are:
//

View File

@@ -1,62 +0,0 @@
package login
import (
"net/http"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
const (
tmplLinkingUserPrompt = "link_user_prompt"
)
type linkingUserPromptData struct {
userData
Username string
Linking domain.AutoLinkingOption
UserID string
}
type linkingUserPromptFormData struct {
OtherUser bool `schema:"other"`
UserID string `schema:"userID"`
}
func (l *Login) renderLinkingUserPrompt(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, user *query.NotifyUser, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
identification := user.PreferredLoginName
// hide the suffix in case the option is set and the auth request has been started with the primary domain scope
if authReq.RequestedOrgDomain && authReq.LabelPolicy != nil && authReq.LabelPolicy.HideLoginNameSuffix {
identification = user.Username
}
data := &linkingUserPromptData{
Username: identification,
UserID: user.ID,
userData: l.getUserData(r, authReq, translator, "LinkingUserPrompt.Title", "LinkingUserPrompt.Description", errID, errMessage),
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkingUserPrompt], data, nil)
}
func (l *Login) handleLinkingUserPrompt(w http.ResponseWriter, r *http.Request) {
data := new(linkingUserPromptFormData)
authReq, err := l.ensureAuthRequestAndParseData(r, data)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
if data.OtherUser {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil)
return
}
err = l.authRepo.SelectUser(r.Context(), authReq.ID, data.UserID, authReq.AgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}

View File

@@ -83,7 +83,6 @@ func CreateRenderer(pathPrefix string, staticStorage static.Storage, cookieName
tmplLDAPLogin: "ldap_login.html",
tmplDeviceAuthUserCode: "device_usercode.html",
tmplDeviceAuthAction: "device_action.html",
tmplLinkingUserPrompt: "link_user_prompt.html",
}
funcs := map[string]interface{}{
"resourceUrl": func(file string) string {

View File

@@ -124,6 +124,5 @@ func CreateRouter(login *Login, interceptors ...mux.MiddlewareFunc) *mux.Router
router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently))
router.HandleFunc(EndpointDeviceAuth, login.handleDeviceAuthUserCode).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointDeviceAuthAction, login.handleDeviceAuthAction).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointLinkingUserPrompt, login.handleLinkingUserPrompt).Methods(http.MethodPost)
return router
}

View File

@@ -1,37 +0,0 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "LinkingUserPrompt.Title"}}</h1>
<p>
{{t "LinkingUserPrompt.Description"}}<br>
{{.Username}}
</p>
</div>
<form action="{{ linkingUserPromptUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" name="userID" value="{{ .UserID }}" />
{{template "error-message" .}}
<div class="lgn-actions lgn-reverse-order">
<a class="lgn-icon-button lgn-left-action" id="back-button" href="#">
<i class="lgn-icon-arrow-left-solid"></i>
</a>
<button class="lgn-raised-button lgn-primary lgn-initial-focus" id="submit-button" type="submit">{{t "LinkingUserPrompt.LinkButtonText"}}</button>
<span class="fill-space"></span>
<button class="lgn-stroked-button" name="other" value="true">{{t "LinkingUserPrompt.OtherButtonText"}}</button>
</div>
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
<script src="{{ resourceUrl "scripts/input_suffix_offset.js" }}"></script>
<script src="{{ resourceUrl "scripts/go_back.js" }}"></script>
{{template "main-bottom" .}}