fix(login): allow fallback to local auth in case of IdP errors (#9178)

# Which Problems Are Solved

The current login will always prefer external authentication (through an
IdP) over local authentication. So as soon as either the user had
connected to an IdP or even when the login policy was just set up to
have an IdP allowed, users would be redirected to that IdP for
(re)authentication.
This could lead to problems, where the IdP was not available or any
other error occurred in the process (such as secret expired for
EntraID).
Even when local authentication (passkeys or password) was allowed for
the corresponding user, they would always be redirected to the IdP
again, preventing any authentication. If admins were affected, they
might not even be able to update the client secret of the IdP.

# How the Problems Are Solved

Errors during the external IdP flow are handled in an
`externalAuthFailed` function, which will check if the organisation
allows local authentication and if the user has set up such.
If either password or passkeys is set up, the corresponding login page
will be presented to the user. As already with local auth passkeys is
preferred over password authentication.
The user is informed that the external login failed and fail back to
local auth as an error on the corresponding page in a focused mode. Any
interaction or after 5 second the focus mode is disabled.

# Additional Changes

None.

# Additional Context

closes #6466
This commit is contained in:
Livio Spring 2025-01-15 11:39:28 +01:00 committed by GitHub
parent 1949d1546a
commit 40082745f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 388 additions and 195 deletions

View File

@ -35,10 +35,6 @@ func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errType, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
if authReq == nil || len(authReq.PossibleSteps) < 1 {
l.renderError(w, r, authReq, err)
@ -50,7 +46,7 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut
return
}
data := passwordData{
baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", errType, errMessage),
baseData: l.getBaseData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", err),
profileData: l.getProfileData(authReq),
Expired: step.Expired,
}
@ -75,6 +71,6 @@ func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, aut
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", "", "")
data := l.getUserData(r, authReq, translator, "PasswordChange.Title", "PasswordChange.Description", nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePasswordDone], data, nil)
}

View File

@ -22,13 +22,11 @@ const (
)
func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, err error) {
var errID, errMessage string
if err != nil {
logging.WithError(err).Error()
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), nil)
data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
data := l.getBaseData(r, nil, translator, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", err)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
}
@ -41,7 +39,7 @@ func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, a
ClientID string
Scopes []string
}{
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Action.Description", nil),
AuthRequestID: authReq.ID,
Username: authReq.UserName,
ClientID: authReq.ApplicationID,
@ -63,7 +61,7 @@ func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, aut
baseData
Message string
}{
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
baseData: l.getBaseData(r, authReq, translator, "DeviceAuth.Title", "DeviceAuth.Done.Description", nil),
}
switch action {
case deviceAuthAllowed:

View File

@ -2,8 +2,10 @@ package login
import (
"context"
"errors"
"net/http"
"net/url"
"slices"
"strings"
"github.com/crewjam/saml/samlsp"
@ -150,7 +152,7 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
l.externalAuthFailed(w, r, authReq, err)
return
}
var provider idp.Provider
@ -183,17 +185,17 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
case domain.IDPTypeUnspecified:
fallthrough
default:
l.renderLogin(w, r, authReq, zerrors.ThrowInvalidArgument(nil, "LOGIN-AShek", "Errors.ExternalIDP.IDPTypeNotImplemented"))
l.externalAuthFailed(w, r, authReq, zerrors.ThrowInvalidArgument(nil, "LOGIN-AShek", "Errors.ExternalIDP.IDPTypeNotImplemented"))
return
}
if err != nil {
l.renderLogin(w, r, authReq, err)
l.externalAuthFailed(w, r, authReq, err)
return
}
params := l.sessionParamsFromAuthRequest(r.Context(), authReq, identityProvider.ID)
session, err := provider.BeginAuth(r.Context(), authReq.ID, params...)
if err != nil {
l.renderLogin(w, r, authReq, err)
l.externalAuthFailed(w, r, authReq, err)
return
}
@ -215,7 +217,7 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
l.renderLogin(w, r, nil, err)
l.externalAuthFailed(w, r, nil, err)
return
}
state := r.Form.Get(queryState)
@ -223,7 +225,7 @@ func (l *Login) handleExternalLoginCallbackForm(w http.ResponseWriter, r *http.R
state = r.Form.Get(queryRelayState)
}
if state == "" {
l.renderLogin(w, r, nil, zerrors.ThrowInvalidArgument(nil, "LOGIN-dsg3f", "Errors.AuthRequest.NotFound"))
l.externalAuthFailed(w, r, nil, zerrors.ThrowInvalidArgument(nil, "LOGIN-dsg3f", "Errors.AuthRequest.NotFound"))
return
}
l.caches.idpFormCallbacks.Set(r.Context(), &idpFormCallback{
@ -243,7 +245,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
// workaround because of CSRF on external identity provider flows using form_post
if r.URL.Query().Get(queryMethod) == http.MethodPost {
if err := l.setDataFromFormCallback(r, r.URL.Query().Get(queryState)); err != nil {
l.renderLogin(w, r, nil, err)
l.externalAuthFailed(w, r, nil, err)
return
}
}
@ -251,7 +253,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
data := new(externalIDPCallbackData)
err := l.getParseData(r, data)
if err != nil {
l.renderLogin(w, r, nil, err)
l.externalAuthFailed(w, r, nil, err)
return
}
if data.State == "" {
@ -261,12 +263,12 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
var provider idp.Provider
@ -275,75 +277,75 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
case domain.IDPTypeOAuth:
provider, err = l.oauthProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &oauth.Session{Provider: provider.(*oauth.Provider), Code: data.Code}
case domain.IDPTypeOIDC:
provider, err = l.oidcProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code}
case domain.IDPTypeAzureAD:
provider, err = l.azureProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &azuread.Session{Provider: provider.(*azuread.Provider), Code: data.Code}
case domain.IDPTypeGitHub:
provider, err = l.githubProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &oauth.Session{Provider: provider.(*github.Provider).Provider, Code: data.Code}
case domain.IDPTypeGitHubEnterprise:
provider, err = l.githubEnterpriseProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &oauth.Session{Provider: provider.(*github.Provider).Provider, Code: data.Code}
case domain.IDPTypeGitLab:
provider, err = l.gitlabProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &openid.Session{Provider: provider.(*gitlab.Provider).Provider, Code: data.Code}
case domain.IDPTypeGitLabSelfHosted:
provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &openid.Session{Provider: provider.(*gitlab.Provider).Provider, Code: data.Code}
case domain.IDPTypeGoogle:
provider, err = l.googleProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code}
case domain.IDPTypeApple:
provider, err = l.appleProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session = &apple.Session{Session: &openid.Session{Provider: provider.(*apple.Provider).Provider, Code: data.Code}, UserFormValue: data.User}
case domain.IDPTypeSAML:
provider, err = l.samlProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
session, err = saml.NewSession(provider.(*saml.Provider), authReq.SAMLRequestID, r)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
l.externalAuthCallbackFailed(w, r, authReq, nil, nil, err)
return
}
case domain.IDPTypeJWT,
@ -351,7 +353,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
domain.IDPTypeUnspecified:
fallthrough
default:
l.renderLogin(w, r, authReq, zerrors.ThrowInvalidArgument(nil, "LOGIN-SFefg", "Errors.ExternalIDP.IDPTypeNotImplemented"))
l.externalAuthFailed(w, r, authReq, zerrors.ThrowInvalidArgument(nil, "LOGIN-SFefg", "Errors.ExternalIDP.IDPTypeNotImplemented"))
return
}
@ -361,7 +363,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
"instance", authz.GetInstance(r.Context()).InstanceID(),
"providerID", identityProvider.ID,
).WithError(err).Info("external authentication failed")
l.externalAuthFailed(w, r, authReq, tokens(session), user, err)
l.externalAuthCallbackFailed(w, r, authReq, tokens(session), user, err)
return
}
l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep)
@ -619,10 +621,6 @@ func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, a
// renderExternalNotFoundOption renders a page, where the user is able to edit the IDP data,
// create a new externalUser of link to existing on (based on the IDP template)
func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, human *domain.Human, idpLink *domain.UserIDPLink, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
resourceOwner := determineResourceOwner(r.Context(), authReq)
if orgIAMPolicy == nil {
orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner)
@ -656,7 +654,7 @@ func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Requ
translator := l.getTranslator(r.Context(), authReq)
data := externalNotFoundOptionData{
baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "ExternalNotFound.Title", "ExternalNotFound.Description", err),
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
externalRegisterFormData: externalRegisterFormData{
Email: human.EmailAddress,
@ -1215,7 +1213,7 @@ func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserG
return nil
}
func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens[*oidc.IDTokenClaims], user idp.User, err error) {
func (l *Login) externalAuthCallbackFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens[*oidc.IDTokenClaims], user idp.User, err error) {
if authReq == nil {
l.renderLogin(w, r, authReq, err)
return
@ -1223,7 +1221,37 @@ func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authR
if _, _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, user, err); actionErr != nil {
logging.WithError(err).Error("both external user authentication and action post authentication failed")
}
l.renderLogin(w, r, authReq, err)
l.externalAuthFailed(w, r, authReq, err)
}
func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
if authReq == nil || authReq.LoginPolicy == nil || !authReq.LoginPolicy.AllowUsernamePassword || authReq.UserID == "" {
l.renderLogin(w, r, authReq, err)
return
}
authMethods, authMethodsError := l.query.ListUserAuthMethodTypes(setUserContext(r.Context(), authReq.UserID, ""), authReq.UserID, true, false, "")
if authMethodsError != nil {
logging.WithFields("userID", authReq.UserID).WithError(authMethodsError).Warn("unable to load user's auth methods for idp login error")
l.renderLogin(w, r, authReq, err)
return
}
passwordless := slices.Contains(authMethods.AuthMethodTypes, domain.UserAuthMethodTypePasswordless)
password := slices.Contains(authMethods.AuthMethodTypes, domain.UserAuthMethodTypePassword)
if !passwordless && !password {
l.renderLogin(w, r, authReq, err)
return
}
localAuthError := l.authRepo.RequestLocalAuth(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.AgentID)
if localAuthError != nil {
l.renderLogin(w, r, authReq, err)
return
}
err = WrapIdPError(err)
if passwordless {
l.renderPasswordlessVerification(w, r, authReq, password, err)
return
}
l.renderPassword(w, r, authReq, err)
}
// tokens extracts the oidc.Tokens for backwards compatibility of PostExternalAuthenticationActions
@ -1359,3 +1387,34 @@ func (l *Login) getUserLinks(ctx context.Context, userID, idpID string) (*query.
}, nil,
)
}
// IdPError wraps an error from an external IDP to be able to distinguish it from other errors and to display it
// more prominent (popup style) .
// It's used if an error occurs during the login process with an external IDP and local authentication is allowed,
// respectively used as fallback.
type IdPError struct {
err *zerrors.ZitadelError
}
func (e *IdPError) Error() string {
return e.err.Error()
}
func (e *IdPError) Unwrap() error {
return e.err
}
func (e *IdPError) Is(target error) bool {
_, ok := target.(*IdPError)
return ok
}
func WrapIdPError(err error) *IdPError {
zErr := new(zerrors.ZitadelError)
id := "LOGIN-JWo3f"
// keep the original error id if there is one
if errors.As(err, &zErr) {
id = zErr.ID
}
return &IdPError{err: zerrors.CreateZitadelError(err, id, "Errors.User.ExternalIDP.LoginFailedSwitchLocal")}
}

View File

@ -112,10 +112,6 @@ func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authRe
}
func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if userID == "" && authReq != nil {
userID = authReq.UserID
}
@ -123,7 +119,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
translator := l.getTranslator(r.Context(), authReq)
data := initPasswordData{
baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InitPassword.Title", "InitPassword.Description", err),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
@ -155,7 +151,7 @@ func (l *Login) renderInitPassword(w http.ResponseWriter, r *http.Request, authR
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", "", "")
data := l.getUserData(r, authReq, translator, "InitPasswordDone.Title", "InitPasswordDone.Description", nil)
if authReq == nil {
l.customTexts(r.Context(), translator, orgID)
}

View File

@ -131,17 +131,13 @@ func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq *
}
func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, loginName string, code string, passwordSet bool, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if authReq != nil {
userID = authReq.UserID
}
translator := l.getTranslator(r.Context(), authReq)
data := initUserData{
baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InitUser.Title", "InitUser.Description", err),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
@ -179,7 +175,7 @@ func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", "", "")
data := l.getUserData(r, authReq, translator, "InitUserDone.Title", "InitUserDone.Description", nil)
if authReq == nil {
l.customTexts(r.Context(), translator, orgID)
}

View File

@ -119,10 +119,6 @@ func (l *Login) resendUserInvite(w http.ResponseWriter, r *http.Request, authReq
}
func (l *Login) renderInviteUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, orgID, loginName string, code string, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if authReq != nil {
userID = authReq.UserID
orgID = authReq.UserOrgID
@ -130,7 +126,7 @@ func (l *Login) renderInviteUser(w http.ResponseWriter, r *http.Request, authReq
translator := l.getTranslator(r.Context(), authReq)
data := inviteUserData{
baseData: l.getBaseData(r, authReq, translator, "InviteUser.Title", "InviteUser.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InviteUser.Title", "InviteUser.Description", err),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,

View File

@ -30,13 +30,9 @@ func (l *Login) handleLDAP(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
temp := l.renderer.Templates[tmplLDAPLogin]
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", err)
l.renderer.RenderTemplate(w, r, translator, temp, data, nil)
}

View File

@ -18,11 +18,7 @@ func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domai
}
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errType, errMessage string
if err != nil {
errType, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", errType, errMessage)
data := l.getUserData(r, authReq, translator, "LinkingUsersDone.Title", "LinkingUsersDone.Description", err)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLinkUsersDone], data, nil)
}

View File

@ -91,16 +91,12 @@ func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderLogin(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if err == nil && singleIDPAllowed(authReq) {
l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
return
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", errID, errMessage)
data := l.getUserData(r, authReq, translator, "Login.Title", "Login.Description", err)
funcs := map[string]interface{}{
"hasUsernamePasswordLogin": func() bool {
return authReq != nil && authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword

View File

@ -37,13 +37,9 @@ func (l *Login) handleLoginSuccess(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderSuccessAndCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := loginSuccessData{
userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "LoginSuccess.Title", "", err),
}
if authReq != nil {
data.RedirectURI, err = l.authRequestCallback(r.Context(), authReq)

View File

@ -14,6 +14,6 @@ func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) {
translator := l.getTranslator(r.Context(), nil)
data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", "", "")
data := l.getUserData(r, nil, translator, "LogoutDone.Title", "LogoutDone.Description", nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplLogoutDone], data, nil)
}

View File

@ -145,17 +145,13 @@ func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *d
}
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string, passwordInit bool, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if userID == "" && authReq != nil {
userID = authReq.UserID
}
translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "EmailVerification.Title", "EmailVerification.Description", err),
UserID: userID,
profileData: l.getProfileData(authReq),
Code: code,
@ -191,7 +187,7 @@ func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, a
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string) {
translator := l.getTranslator(r.Context(), authReq)
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", "", ""),
baseData: l.getBaseData(r, authReq, translator, "EmailVerificationDone.Title", "EmailVerificationDone.Description", nil),
profileData: l.getProfileData(authReq),
}
if authReq == nil {

View File

@ -14,9 +14,8 @@ type mfaInitDoneData struct {
}
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", errType, errMessage)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFADone.Title", "InitMFADone.Description", nil)
data.profileData = l.getProfileData(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
}

View File

@ -53,12 +53,8 @@ func (l *Login) handleRegisterOTPSMS(w http.ResponseWriter, r *http.Request, aut
}
func (l *Login) renderRegisterSMS(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *smsInitData, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", err)
data.profileData = l.getProfileData(authReq)
data.MFAType = domain.MFATypeOTPSMS
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFASMSInit], data, nil)

View File

@ -18,21 +18,18 @@ type u2fInitData struct {
}
func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage, credentialData string
var credentialData string
var u2f *domain.WebAuthNToken
if err == nil {
u2f, err = l.command.HumanAddU2FSetup(setUserContext(r.Context(), authReq.UserID, authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if u2f != nil {
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
}
translator := l.getTranslator(r.Context(), authReq)
data := &u2fInitData{
webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "InitMFAU2F.Title", "InitMFAU2F.Description", err),
CredentialCreationData: credentialData,
},
MFAType: domain.MFATypeU2F,

View File

@ -66,12 +66,8 @@ func (l *Login) handleOTPVerify(w http.ResponseWriter, r *http.Request, authReq
}
func (l *Login) renderMFAInitVerify(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", errID, errMessage)
data.baseData = l.getBaseData(r, authReq, translator, "InitMFAOTP.Title", "InitMFAOTP.Description", err)
data.profileData = l.getProfileData(authReq)
if data.MFAType == domain.MFATypeTOTP {
code, err := generateQrCode(data.totpData.Url)

View File

@ -49,13 +49,9 @@ func (l *Login) handleMFAPromptSelection(w http.ResponseWriter, r *http.Request)
}
func (l *Login) renderMFAPrompt(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, mfaPromptData *domain.MFAPromptStep, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := mfaData{
baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "InitMFAPrompt.Title", "InitMFAPrompt.Description", err),
profileData: l.getProfileData(authReq),
}

View File

@ -62,12 +62,8 @@ func (l *Login) renderMFAVerify(w http.ResponseWriter, r *http.Request, authReq
}
func (l *Login) renderMFAVerifySelected(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, verificationStep *domain.MFAVerificationStep, selectedProvider domain.MFAType, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "", "", errID, errMessage)
data := l.getUserData(r, authReq, translator, "", "", err)
if verificationStep == nil {
l.renderError(w, r, authReq, err)
return

View File

@ -61,13 +61,9 @@ func (l *Login) handleOTPVerification(w http.ResponseWriter, r *http.Request, au
}
func (l *Login) renderOTPVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, providers []domain.MFAType, selectedProvider domain.MFAType, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := &mfaOTPData{
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", err),
MFAProviders: removeSelectedProviderFromList(providers, selectedProvider),
SelectedProvider: selectedProvider,
}

View File

@ -24,22 +24,19 @@ type mfaU2FFormData struct {
}
func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, providers []domain.MFAType, err error) {
var errID, errMessage, credentialData string
var credentialData string
var webAuthNLogin *domain.WebAuthNLogin
if err == nil {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
webAuthNLogin, err = l.authRepo.BeginMFAU2FLogin(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, userAgentID)
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if webAuthNLogin != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
}
translator := l.getTranslator(r.Context(), authReq)
data := &mfaU2FData{
webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "VerifyMFAU2F.Title", "VerifyMFAU2F.Description", err),
CredentialCreationData: credentialData,
},
MFAProviders: providers,

View File

@ -15,12 +15,8 @@ type passwordFormData struct {
}
func (l *Login) renderPassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", errID, errMessage)
data := l.getUserData(r, authReq, translator, "Password.Title", "Password.Description", err)
funcs := map[string]interface{}{
"showPasswordReset": func() bool {
if authReq.LoginPolicy != nil {

View File

@ -30,11 +30,7 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderPasswordResetDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", errID, errMessage)
data := l.getUserData(r, authReq, translator, "PasswordResetDone.Title", "PasswordResetDone.Description", err)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordResetDone], data, nil)
}

View File

@ -2,6 +2,7 @@ package login
import (
"encoding/base64"
"errors"
"net/http"
"github.com/zitadel/zitadel/internal/domain"
@ -22,13 +23,15 @@ type passwordlessFormData struct {
}
func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, passwordSet bool, err error) {
var errID, errMessage, credentialData string
var credentialData string
var webAuthNLogin *domain.WebAuthNLogin
if err == nil {
webAuthNLogin, err = l.authRepo.BeginPasswordlessLogin(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, authReq.AgentID)
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
if err == nil || errors.Is(err, &IdPError{}) { // make sure we still proceed with the webauthn login even if the idp login failed
var creationErr error
webAuthNLogin, creationErr = l.authRepo.BeginPasswordlessLogin(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, authReq.AgentID)
// and only overwrite the error if the webauthn creation failed
if creationErr != nil {
err = creationErr
}
}
if webAuthNLogin != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNLogin.CredentialAssertionData)
@ -39,7 +42,7 @@ func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Re
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessData{
webAuthNData{
userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "Passwordless.Title", "Passwordless.Description", err),
CredentialCreationData: credentialData,
},
passwordSet,

View File

@ -27,13 +27,9 @@ func (l *Login) handlePasswordlessPrompt(w http.ResponseWriter, r *http.Request)
}
func (l *Login) renderPasswordlessPrompt(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "PasswordlessPrompt.Title", "PasswordlessPrompt.Description", err),
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
}

View File

@ -78,7 +78,7 @@ func (l *Login) handlePasswordlessRegistration(w http.ResponseWriter, r *http.Re
}
func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, orgID, codeID, code string, requestedPlatformType authPlatform, err error) {
var errID, errMessage, credentialData string
var credentialData string
var disabled bool
if authReq != nil {
userID = authReq.UserID
@ -93,7 +93,6 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
}
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
disabled = true
}
if webAuthNToken != nil {
@ -102,7 +101,7 @@ func (l *Login) renderPasswordlessRegistration(w http.ResponseWriter, r *http.Re
translator := l.getTranslator(r.Context(), authReq)
data := &passwordlessRegistrationData{
webAuthNData{
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistration.Title", "PasswordlessRegistration.Description", err),
CredentialCreationData: credentialData,
},
code,
@ -185,13 +184,9 @@ func (l *Login) checkPasswordlessRegistration(w http.ResponseWriter, r *http.Req
}
func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgID string, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := passwordlessRegistrationDoneDate{
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", errID, errMessage),
userData: l.getUserData(r, authReq, translator, "PasswordlessRegistrationDone.Title", "PasswordlessRegistrationDone.Description", err),
HideNextButton: authReq == nil,
}
if authReq == nil {

View File

@ -142,10 +142,6 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authRequest *domain.AuthRequest, formData *registerFormData, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authRequest)
if formData == nil {
formData = new(registerFormData)
@ -156,7 +152,7 @@ func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authReque
resourceOwner := determineResourceOwner(r.Context(), authRequest)
data := registerData{
baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", errID, errMessage),
baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", err),
registerFormData: *formData,
}

View File

@ -33,10 +33,6 @@ func (l *Login) handleRegisterOption(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
allowed := registrationAllowed(authReq)
externalAllowed := externalRegistrationAllowed(authReq)
if err == nil {
@ -54,7 +50,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut
}
translator := l.getTranslator(r.Context(), authReq)
data := registerOptionData{
baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", errID, errMessage),
baseData: l.getBaseData(r, authReq, translator, "RegisterOption.Title", "RegisterOption.Description", err),
}
funcs := map[string]interface{}{
"hasRegistration": func() bool {

View File

@ -97,16 +97,12 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderRegisterOrg(w http.ResponseWriter, r *http.Request, authRequest *domain.AuthRequest, formData *registerOrgFormData, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if formData == nil {
formData = new(registerOrgFormData)
}
translator := l.getTranslator(r.Context(), authRequest)
data := registerOrgData{
baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", errID, errMessage),
baseData: l.getBaseData(r, authRequest, translator, "RegistrationOrg.Title", "RegistrationOrg.Description", err),
registerOrgFormData: *formData,
}
pwPolicy := l.getPasswordComplexityPolicy(r, "0")

View File

@ -341,7 +341,6 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
}
func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var msg string
if err != nil {
log := logging.WithError(err)
if authReq != nil {
@ -352,17 +351,15 @@ func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, auth
} else {
log.Info()
}
_, msg = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", "Internal", msg)
data := l.getBaseData(r, authReq, translator, "Errors.Internal", "", err)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
}
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) userData {
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, err error) userData {
userData := userData{
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, errType, errMessage),
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, err),
profileData: l.getProfileData(authReq),
}
if authReq != nil && authReq.LinkingUsers != nil {
@ -371,7 +368,7 @@ func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, transl
return userData
}
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, errType, errMessage string) baseData {
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, translator *i18n.Translator, titleI18nKey string, descriptionI18nKey string, err error) baseData {
title := ""
if titleI18nKey != "" {
title = translator.LocalizeWithoutArgs(titleI18nKey)
@ -383,10 +380,16 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, transl
}
lang, _ := l.renderer.ReqLang(translator, r).Base()
var errID, errMessage string
var errPopup bool
if err != nil {
errID, errMessage, errPopup = l.getErrorMessage(r, err)
}
baseData := baseData{
errorData: errorData{
ErrID: errType,
ErrID: errID,
ErrMessage: errMessage,
ErrPopup: errPopup,
},
Lang: lang.String(),
Title: title,
@ -482,14 +485,17 @@ func (l *Login) setLinksOnBaseData(baseData baseData, privacyPolicy *domain.Priv
return baseData
}
func (l *Login) getErrorMessage(r *http.Request, err error) (errID, errMsg string) {
func (l *Login) getErrorMessage(r *http.Request, err error) (errID, errMsg string, popup bool) {
idpErr := new(IdPError)
if errors.Is(err, idpErr) {
popup = true
}
caosErr := new(zerrors.ZitadelError)
if errors.As(err, &caosErr) {
localized := l.renderer.LocalizeFromRequest(l.getTranslator(r.Context(), nil), r, caosErr.Message, nil)
return caosErr.ID, localized
localized := l.renderer.LocalizeFromRequest(l.getTranslator(r.Context(), nil), r, caosErr.Message, map[string]interface{}{"Details": caosErr.Parent})
return caosErr.ID, localized, popup
}
return "", err.Error()
return "", err.Error(), popup
}
func (l *Login) getTheme(r *http.Request) string {
@ -662,6 +668,7 @@ type baseData struct {
type errorData struct {
ErrID string
ErrMessage string
ErrPopup bool
}
type userData struct {

View File

@ -27,7 +27,7 @@ func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, auth
descriptionI18nKey = "SelectAccount.DescriptionLinking"
}
data := userSelectionData{
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, "", ""),
baseData: l.getBaseData(r, authReq, translator, titleI18nKey, descriptionI18nKey, nil),
Users: selectionData.Users,
Linking: linking,
}

View File

@ -493,6 +493,10 @@ Errors:
CreationNotAllowed: Създаването на нов потребител не е разрешено на този доставчик
LinkingNotAllowed: Свързването на потребител не е разрешено на този доставчик
NoOptionAllowed: Нито създаване, нито свързване е разрешено за този доставчик. Моля, свържете се с администратора.
LoginFailedSwitchLocal: |
Вход в външен доставчик на идентификация е неуспешен. Връщане към локален вход.
Подробности за грешката: {{.Details}}
GrantRequired: 'Влизането не е възможно. '
ProjectRequired: 'Влизането не е възможно. '
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: Vytvoření nového uživatele není na tomto poskytovateli povoleno
LinkingNotAllowed: Propojení uživatele není na tomto poskytovateli povoleno
NoOptionAllowed: Ani vytvoření, ani propojení není povoleno pro tohoto poskytovatele. Obraťte se na svého správce.
LoginFailedSwitchLocal: |
Přihlášení u externího poskytovatele identit selhalo. Vracíme se k místnímu přihlášení.
Podrobnosti o chybě: {{.Details}}
GrantRequired: Přihlášení není možné. Uživatel musí mít alespoň jeden oprávnění na aplikaci. Prosím, kontaktujte svého správce.
ProjectRequired: Přihlášení není možné. Organizace uživatele musí být přidělena k projektu. Prosím, kontaktujte svého správce.
IdentityProvider:

View File

@ -504,6 +504,10 @@ Errors:
CreationNotAllowed: Erstellen eines neuen Benutzers mit diesem Provider ist nicht erlaubt
LinkingNotAllowed: Verknüpfen eines Benutzers mit diesem Provider ist nicht erlaubt
NoOptionAllowed: Weder Erstellung noch Verknüpfung ist für diesen Provider erlaubt. Bitte wenden Sie sich an Ihren Administrator.
LoginFailedSwitchLocal: |
Anmeldung beim externen Identitätsanbieter fehlgeschlagen. Zurück zur lokalen Anmeldung.
Fehlerdetails: {{.Details}}
GrantRequired: Die Anmeldung an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte wende dich an deinen Administrator.
ProjectRequired: Die Anmeldung an dieser Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte wende dich an deinen Administrator.
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: Creation of a new user is not allowed on this provider
LinkingNotAllowed: Linking of a user is not allowed on this provider
NoOptionAllowed: Neither creation of linking is allowed on this provider. Please contact your administrator.
LoginFailedSwitchLocal: |
Login at External IDP failed. Falling back to local login.
Error details: {{.Details}}
GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator.
ProjectRequired: Login not possible. The organization of the user must be granted to the project. Please contact your administrator.
IdentityProvider:

View File

@ -488,6 +488,10 @@ Errors:
CreationNotAllowed: La creación de un nuevo usuario no está permitida para este proveedor
LinkingNotAllowed: La vinculación de un usuario no está permitida para este proveedor
NoOptionAllowed: Ni la creación ni la vinculación están permitidas en este proveedor. Póngase en contacto con su administrador.
LoginFailedSwitchLocal: |
Error al iniciar sesión en el proveedor de identidad externo. Volviendo al inicio de sesión local.
Detalles del error: {{.Details}}
GrantRequired: El inicio de sesión no es posible. Se requiere que el usuario tenga al menos una concesión sobre la aplicación. Por favor contacta con tu administrador.
ProjectRequired: El inicio de sesión no es posible. La organización del usuario debe tener el acceso concedido para el proyecto. Por favor contacta con tu administrador.
IdentityProvider:

View File

@ -506,6 +506,10 @@ Errors:
CreationNotAllowed: La création d'un nouvel utilisateur n'est pas autorisée sur ce fournisseur.
LinkingNotAllowed: La création d'un lien vers un utilisateur n'est pas autorisée pour ce fournisseur.
NoOptionAllowed: Ni la création ni la liaison sont autorisées pour ce fournisseur. Veuillez contacter votre administrateur.
LoginFailedSwitchLocal: |
Échec de la connexion au fournisseur d'identité externe. Retour à la connexion locale.
Détails de l'erreur: {{.Details}}
GrantRequired: Connexion impossible. L'utilisateur doit avoir au moins une subvention sur l'application. Veuillez contacter votre administrateur.
ProjectRequired: Connexion impossible. L'organisation de l'utilisateur doit être accordée au projet. Veuillez contacter votre administrateur.
IdentityProvider:

View File

@ -465,6 +465,10 @@ Errors:
CreationNotAllowed: Új felhasználó létrehozása nem engedélyezett ezen a szolgáltatón
LinkingNotAllowed: A felhasználó összekapcsolása nem engedélyezett ezen a szolgáltatón
NoOptionAllowed: Sem új felhasználó létrehozása, sem összekapcsolás nem engedélyezett ezen a szolgáltatón. Kérjük, lépj kapcsolatba az adminisztrátoroddal.
LoginFailedSwitchLocal: |
Az egyéni azonosító szolgáltatóhoz való bejelentkezés sikertelen volt. Visszatérés a helyi bejelentkezéshez.
Hiba részletei: {{.Details}}
GrantRequired: Bejelentkezés nem lehetséges. A felhasználónak legalább egy jogosultsággal kell rendelkeznie az alkalmazáson. Kérlek, lépj kapcsolatba az adminisztrátoroddal.
ProjectRequired: Bejelentkezés nem lehetséges. A felhasználó szervezetének engedélyezve kell lennie a projektre. Kérlek, lépj kapcsolatba az adminisztrátoroddal.
IdentityProvider:

View File

@ -464,6 +464,10 @@ Errors:
CreationNotAllowed: Pembuatan pengguna baru tidak diperbolehkan pada penyedia ini
LinkingNotAllowed: Menautkan pengguna tidak diperbolehkan di penyedia ini
NoOptionAllowed: 'Pembuatan tautan tidak diperbolehkan pada penyedia ini. '
LoginFailedSwitchLocal: |
Gagal masuk ke Penyedia ID Eksternal. Kembali ke login lokal.
Detail kesalahan: {{.Details}}
GrantRequired: 'Masuk tidak dapat dilakukan. '
ProjectRequired: 'Masuk tidak dapat dilakukan. '
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: La creazione di un nuovo utente non è consentita su questo provider.
LinkingNotAllowed: Il collegamento di un utente non è consentito su questo provider.
NoOptionAllowed: Né la creazione né il collegamento sono consentiti per questo provider. Contattare l'amministratore.
LoginFailedSwitchLocal: |
Accesso al provider di identità esterno non riuscito. Ritorno all'accesso locale.
Dettagli dell'errore: {{.Details}}
GrantRequired: Accesso non possibile. L'utente deve avere almeno una sovvenzione sull'applicazione. Contatta il tuo amministratore.
ProjectRequired: Accesso non possibile. L'organizzazione dell'utente deve essere concessa al progetto. Contatta il tuo amministratore.
IdentityProvider:

View File

@ -469,6 +469,10 @@ Errors:
CreationNotAllowed: このプロバイダーでは、新しいユーザーの作成は許可されていません
LinkingNotAllowed: このプロバイダーでは、ユーザーのリンクが許可されていません
NoOptionAllowed: このプロバイダーでは作成もリンクも許可されていません。 管理者にお問い合わせください。
LoginFailedSwitchLocal: |
外部IDプロバイダーへのログインに失敗しました。ローカルログインに戻ります。
エラーの詳細: {{.Details}}
GrantRequired: ログインできません。このユーザーは、アプリケーションに少なくとも1つの権限を付与されていることが必要です。管理者にお問い合わせください。
ProjectRequired: ログインできません。ユーザーの組織がプロジェクトに権限を付与されている必要があります。管理者にお問い合わせください。
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: 이 제공자에서는 새 사용자 생성을 허용하지 않습니다
LinkingNotAllowed: 이 제공자에서는 사용자를 연결할 수 없습니다
NoOptionAllowed: 이 제공자에서는 생성과 연결이 모두 허용되지 않습니다. 관리자에게 문의하세요.
LoginFailedSwitchLocal: |
외부 IDP에서 로그인에 실패했습니다. 로컬 로그인으로 돌아갑니다.
오류 세부 정보: {{.Details}}
GrantRequired: 로그인 불가. 사용자는 애플리케이션에서 최소한 하나의 권한이 필요합니다. 관리자에게 문의하세요.
ProjectRequired: 로그인 불가. 사용자의 조직이 프로젝트에 허가되어야 합니다. 관리자에게 문의하세요.
IdentityProvider:

View File

@ -506,6 +506,10 @@ Errors:
CreationNotAllowed: Креирањето на нов корисник не е дозволено на овој провајдер
LinkingNotAllowed: Поврзувањето на корисник не е дозволено на овој провајдер
NoOptionAllowed: NНиту создавање, ниту поврзување е дозволено за овој провајдер. Ве молиме контактирајте го вашиот администратор.
LoginFailedSwitchLocal: |
Најавата во надворешен провајдер на идентитет не успеа. Враќање на локална најава.
Детали за грешката: {{.Details}}
GrantRequired: Не е можно најавување. Корисникот мора да има барем едно овластување за апликацијата. Ве молиме контактирајте го вашиот администратор.
ProjectRequired: Не е можно најавување. Организацијата на корисникот мора да биде доделена на проектот. Ве молиме контактирајте го вашиот администратор.
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: Creatie van een nieuwe gebruiker is niet toegestaan op deze Provider
LinkingNotAllowed: Koppeling van een gebruiker is niet toegestaan op deze Provider
NoOptionAllowed: Noch aanmaak noch koppeling is toegestaan voor deze provider. Neem contact op met uw beheerder.
LoginFailedSwitchLocal: |
Aanmelding bij externe identiteitsprovider is mislukt. Terug naar lokale aanmelding.
Foutdetails: {{.Details}}
GrantRequired: Inloggen niet mogelijk. De gebruiker moet minimaal één grant hebben op de applicatie. Neem contact op met uw beheerder.
ProjectRequired: Inloggen niet mogelijk. De organisatie van de gebruiker moet toegekend zijn aan het project. Neem contact op met uw beheerder.
IdentityProvider:

View File

@ -506,6 +506,10 @@ Errors:
CreationNotAllowed: Tworzenie nowego użytkownika nie jest dozwolone w tym Providencie
LinkingNotAllowed: Linkowanie użytkownika nie jest dozwolone na tym Providencie
NoOptionAllowed: Ani tworzenie, ani łączenie nie jest dozwolone dla tego dostawcy. Skontaktuj się z administratorem.
LoginFailedSwitchLocal: |
Logowanie w zewnętrznym dostawcy tożsamości nie powiodło się. Powrót do logowania lokalnego.
Szczegóły błędu: {{.Details}}
GrantRequired: Logowanie nie jest możliwe. Użytkownik musi posiadać przynajmniej jedno uprawnienie w aplikacji. Skontaktuj się z administratorem.
ProjectRequired: Logowanie nie jest możliwe. Organizacja użytkownika musi zostać udzielona projektowi. Skontaktuj się z administratorem.
IdentityProvider:

View File

@ -502,6 +502,10 @@ Errors:
CreationNotAllowed: A criação de um novo usuário não é permitida neste provedor
LinkingNotAllowed: A vinculação de um usuário não é permitida neste provedor
NoOptionAllowed: Nem criação nem vinculação são permitidas neste fornecedor. Contate o seu administrador.
LoginFailedSwitchLocal: |
Falha no login no provedor de identidade externo. Retornando ao login local.
Detalhes do erro: {{.Details}}
GrantRequired: Login não é possível. O usuário precisa ter pelo menos uma permissão no aplicativo. Entre em contato com o administrador.
ProjectRequired: Login não é possível. A organização do usuário precisa ser concedida ao projeto. Entre em contato com o administrador.
IdentityProvider:

View File

@ -506,6 +506,10 @@ Errors:
CreationNotAllowed: Создание нового пользователя для этого провайдера запрещено
LinkingNotAllowed: Привязка к этому провайдеру запрещена
NoOptionAllowed: Ни создание, ни привязка пользователя к этому провайдеру невозможны. Обратитесь к администратору.
LoginFailedSwitchLocal: |
Вход в внешний поставщик идентификации не удался. Возвращаемся к локальному входу.
Подробности об ошибке: {{.Details}}
GrantRequired: Вход невозможен. Пользователь должен иметь хотя бы один допуск к приложению. Обратитесь к администратору.
ProjectRequired: Вход невозможен. Организация пользователя должна иметь доступ к проекту. Обратитесь к администратору.
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: Det är inte tillåtet att skapa nya konton från den här externa leverantören
LinkingNotAllowed: Det är inte tillåtet att koppla ihop konton från den här externa leverantören
NoOptionAllowed: Varken skapande eller länkande är tillåtet för denna leverantör. Kontakta administratören.
LoginFailedSwitchLocal: |
Inloggning vid extern identitetsprovider misslyckades. Återgår till lokal inloggning.
Felaktighetsdetaljer: {{.Details}}
GrantRequired: Det går inte att logga in just nu. Användarkontot har inte tillgång till någonting i tjänsten. Ta kontakt med systemansvarig.
ProjectRequired: Det går inte att logga in just nu. Användarkontots organisation har inte tillgång till tjänsten. Ta kontakt med systemansvarig.
IdentityProvider:

View File

@ -505,6 +505,10 @@ Errors:
CreationNotAllowed: 不允许在该供应商上创建新用户
LinkingNotAllowed: 在此提供者上不允许链接一个用户
NoOptionAllowed: 此提供商不允许创建或链接。请联系您的管理员。
LoginFailedSwitchLocal: |
外部身份提供商的登录失败。返回到本地登录。
错误详情: {{.Details}}
GrantRequired: 无法登录,用户需要在应用程序上拥有至少一项授权,请联系您的管理员。
ProjectRequired: 无法登录,用户的组织必须授予项目,请联系您的管理员。
IdentityProvider:

View File

@ -0,0 +1,30 @@
function removeOverlay(overlay) {
if (overlay.classList.contains("show")) {
overlay.classList.remove("show");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("click", onClick);
}
}
function onMouseMove() {
const overlay = document.getElementById("dialog_overlay");
if (overlay) {
removeOverlay(overlay);
}
}
function onClick() {
const overlay = document.getElementById("dialog_overlay");
if (overlay) {
removeOverlay(overlay);
}
}
window.addEventListener('DOMContentLoaded', () => {
const overlay = document.getElementById("dialog_overlay");
if (overlay && overlay.classList.contains("show")) {
setTimeout(() => removeOverlay(overlay), 5000);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("click", onClick);
}
});

View File

@ -20,3 +20,26 @@ body {
.text-align-center {
text-align: center;
}
.dialog_overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: black;
z-index: 1001;
-moz-opacity: 0.8;
opacity: .80;
filter: alpha(opacity=80);
}
.dialog_overlay.show {
display: block;
}
.dialog_content {
position: relative;
z-index: 1002;
}

View File

@ -6,6 +6,10 @@
margin-right: .5rem;
font-size: 1.5rem;
}
.lgn-error-message {
white-space: pre-line;
}
}
#wa-error {

View File

@ -1,10 +1,9 @@
{{ define "error-message" }}
{{if .ErrMessage }}
<div class="lgn-error" title="{{.ErrID}}">
<div class="lgn-error dialog_content" title="{{.ErrID}}">
<i class="lgn-icon-exclamation-circle-solid lgn-warn"></i>
<p class="lgn-error-message">
{{ .ErrMessage }}
</p>
<p class="lgn-error-message">{{ .ErrMessage }}</p>
</div>
<div id="dialog_overlay" class="dialog_overlay {{if .ErrPopup}}show{{end}}"></div>
{{end}}
{{ end }}
{{end}}

View File

@ -41,4 +41,5 @@
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
<script src="{{ resourceUrl "scripts/error_popup.js" }}"></script>

View File

@ -40,5 +40,6 @@
<script src="{{ resourceUrl "scripts/utils.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_login.js" }}"></script>
<script src="{{ resourceUrl "scripts/error_popup.js" }}"></script>
{{template "main-bottom" .}}

View File

@ -16,12 +16,8 @@ type changeUsernameData struct {
}
func (l *Login) renderChangeUsername(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "UsernameChange.Title", "UsernameChange.Description", errID, errMessage)
data := l.getUserData(r, authReq, translator, "UsernameChange.Title", "UsernameChange.Description", err)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsername], data, nil)
}
@ -41,8 +37,7 @@ func (l *Login) handleChangeUsername(w http.ResponseWriter, r *http.Request) {
}
func (l *Login) renderChangeUsernameDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
var errType, errMessage string
translator := l.getTranslator(r.Context(), authReq)
data := l.getUserData(r, authReq, translator, "UsernameChangeDone.Title", "UsernameChangeDone.Description", errType, errMessage)
data := l.getUserData(r, authReq, translator, "UsernameChangeDone.Title", "UsernameChangeDone.Description", nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangeUsernameDone], data, nil)
}

View File

@ -41,4 +41,5 @@ type AuthRequestRepository interface {
AutoRegisterExternalUser(ctx context.Context, user *domain.Human, externalIDP *domain.UserIDPLink, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, metadatas []*domain.Metadata, info *domain.BrowserInfo) error
ResetLinkingUsers(ctx context.Context, authReqID, userAgentID string) error
ResetSelectedIDP(ctx context.Context, authReqID, userAgentID string) error
RequestLocalAuth(ctx context.Context, authReqID, userAgentID string) error
}

View File

@ -563,6 +563,15 @@ func (repo *AuthRequestRepo) ResetSelectedIDP(ctx context.Context, authReqID, us
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) RequestLocalAuth(ctx context.Context, authReqID, userAgentID string) error {
request, err := repo.getAuthRequest(ctx, authReqID, userAgentID)
if err != nil {
return err
}
request.RequestLocalAuth = true
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) AutoRegisterExternalUser(ctx context.Context, registerUser *domain.Human, externalIDP *domain.UserIDPLink, orgMemberRoles []string, authReqID, userAgentID, resourceOwner string, metadatas []*domain.Metadata, info *domain.BrowserInfo) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@ -1059,7 +1068,7 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
request.PreferredLanguage = gu.Ptr(language.Make(user.HumanView.PreferredLanguage))
}
isInternalLogin := request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == ""
isInternalLogin := (request.SelectedIDPConfigID == "" && userSession.SelectedIDPConfigID == "") || request.RequestLocalAuth
idps, err := checkExternalIDPsOfUser(ctx, repo.IDPUserLinksProvider, user.ID)
if err != nil {
return nil, err
@ -1067,7 +1076,9 @@ func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *domain.Auth
noLocalAuth := request.LoginPolicy != nil && !request.LoginPolicy.AllowUsernamePassword
allowedLinkedIDPs := checkForAllowedIDPs(request.AllowedExternalIDPs, idps.Links)
if (!isInternalLogin || len(allowedLinkedIDPs) > 0 || noLocalAuth) && len(request.LinkingUsers) == 0 {
if (!isInternalLogin || len(allowedLinkedIDPs) > 0 || noLocalAuth) &&
len(request.LinkingUsers) == 0 &&
!request.RequestLocalAuth {
step, err := repo.idpChecked(request, allowedLinkedIDPs, userSession)
if err != nil {
return nil, err

View File

@ -2263,6 +2263,86 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
[]domain.NextStep{&domain.LinkUsersStep{}},
nil,
},
{
"local auth requested (passwordless and password set up), passwordless step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MFAMaxSetUp: int32(domain.MFALevelMultiFactor),
PasswordlessTokens: user_view_model.WebAuthNTokens{&user_view_model.WebAuthNView{ID: "id", State: int32(user_model.MFAStateReady)}},
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
idpUserLinksProvider: &mockIDPUserLinks{
idps: []*query.IDPUserLink{{IDPID: "IDPConfigID"}},
},
},
args{
&domain.AuthRequest{
UserID: "UserID",
SelectedIDPConfigID: "IDPConfigID",
LoginPolicy: &domain.LoginPolicy{
PasswordlessType: domain.PasswordlessTypeAllowed,
},
AllowedExternalIDPs: []*domain.IDPProvider{
{
IDPConfigID: "IDPConfigID",
},
},
RequestLocalAuth: true,
}, false},
[]domain.NextStep{
&domain.PasswordlessStep{
PasswordSet: true,
},
},
nil,
},
{
"local auth requested (password set up), password step",
fields{
userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
},
userEventProvider: &mockEventUser{},
orgViewProvider: &mockViewOrg{State: domain.OrgStateActive},
lockoutPolicyProvider: &mockLockoutPolicy{
policy: &query.LockoutPolicy{
ShowFailures: true,
},
},
idpUserLinksProvider: &mockIDPUserLinks{
idps: []*query.IDPUserLink{{IDPID: "IDPConfigID"}},
},
},
args{
&domain.AuthRequest{
UserID: "UserID",
SelectedIDPConfigID: "IDPConfigID",
LoginPolicy: &domain.LoginPolicy{
PasswordlessType: domain.PasswordlessTypeAllowed,
},
AllowedExternalIDPs: []*domain.IDPProvider{
{
IDPConfigID: "IDPConfigID",
},
},
RequestLocalAuth: true,
}, false},
[]domain.NextStep{
&domain.PasswordStep{},
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -60,6 +60,7 @@ type AuthRequest struct {
DefaultTranslations []*CustomText
OrgTranslations []*CustomText
SAMLRequestID string
RequestLocalAuth bool
// orgID the policies were last loaded with
policyOrgID string
// SessionID is set to the computed sessionID of the login session table