mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:17:32 +00:00
feat: add default redirect uri and handling of unknown usernames (#3616)
* feat: add possibility to ignore username errors on first login screen * console changes * fix: handling of unknown usernames (#3445) * fix: handling of unknown usernames * fix: handle HideLoginNameSuffix on unknown users * feat: add default redirect uri on login policy (#3607) * feat: add default redirect uri on login policy * fix tests * feat: Console login policy default redirect (#3613) * console default redirect * placeholder * validate default redirect uri * allow empty default redirect uri Co-authored-by: Max Peintner <max@caos.ch> * remove wonrgly cherry picked migration Co-authored-by: Max Peintner <max@caos.ch>
This commit is contained in:
@@ -16,6 +16,8 @@ func updateLoginPolicyToDomain(p *admin_pb.UpdateLoginPolicyRequest) *domain.Log
|
||||
ForceMFA: p.ForceMfa,
|
||||
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
|
||||
HidePasswordReset: p.HidePasswordReset,
|
||||
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
|
||||
DefaultRedirectURI: p.DefaultRedirectUri,
|
||||
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
|
||||
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
|
||||
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),
|
||||
|
@@ -16,6 +16,8 @@ func addLoginPolicyToDomain(p *mgmt_pb.AddCustomLoginPolicyRequest) *domain.Logi
|
||||
ForceMFA: p.ForceMfa,
|
||||
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
|
||||
HidePasswordReset: p.HidePasswordReset,
|
||||
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
|
||||
DefaultRedirectURI: p.DefaultRedirectUri,
|
||||
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
|
||||
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
|
||||
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),
|
||||
@@ -32,6 +34,8 @@ func updateLoginPolicyToDomain(p *mgmt_pb.UpdateCustomLoginPolicyRequest) *domai
|
||||
ForceMFA: p.ForceMfa,
|
||||
PasswordlessType: policy_grpc.PasswordlessTypeToDomain(p.PasswordlessType),
|
||||
HidePasswordReset: p.HidePasswordReset,
|
||||
IgnoreUnknownUsernames: p.IgnoreUnknownUsernames,
|
||||
DefaultRedirectURI: p.DefaultRedirectUri,
|
||||
PasswordCheckLifetime: p.PasswordCheckLifetime.AsDuration(),
|
||||
ExternalLoginCheckLifetime: p.ExternalLoginCheckLifetime.AsDuration(),
|
||||
MFAInitSkipLifetime: p.MfaInitSkipLifetime.AsDuration(),
|
||||
|
@@ -1,12 +1,13 @@
|
||||
package policy
|
||||
|
||||
import (
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/object"
|
||||
policy_pb "github.com/zitadel/zitadel/pkg/grpc/policy"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
timestamp_pb "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
|
||||
@@ -18,6 +19,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
|
||||
ForceMfa: policy.ForceMFA,
|
||||
PasswordlessType: ModelPasswordlessTypeToPb(policy.PasswordlessType),
|
||||
HidePasswordReset: policy.HidePasswordReset,
|
||||
IgnoreUnknownUsernames: policy.IgnoreUnknownUsernames,
|
||||
DefaultRedirectUri: policy.DefaultRedirectURI,
|
||||
PasswordCheckLifetime: durationpb.New(policy.PasswordCheckLifetime),
|
||||
ExternalLoginCheckLifetime: durationpb.New(policy.ExternalLoginCheckLifetime),
|
||||
MfaInitSkipLifetime: durationpb.New(policy.MFAInitSkipLifetime),
|
||||
@@ -25,8 +28,8 @@ func ModelLoginPolicyToPb(policy *query.LoginPolicy) *policy_pb.LoginPolicy {
|
||||
MultiFactorCheckLifetime: durationpb.New(policy.MultiFactorCheckLifetime),
|
||||
Details: &object.ObjectDetails{
|
||||
Sequence: policy.Sequence,
|
||||
CreationDate: timestamp_pb.New(policy.CreationDate),
|
||||
ChangeDate: timestamp_pb.New(policy.ChangeDate),
|
||||
CreationDate: timestamppb.New(policy.CreationDate),
|
||||
ChangeDate: timestamppb.New(policy.ChangeDate),
|
||||
ResourceOwner: policy.OrgID,
|
||||
},
|
||||
}
|
||||
|
@@ -75,7 +75,7 @@ func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if authReq == nil {
|
||||
http.Redirect(w, r, l.consolePath, http.StatusFound)
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
l.handleIDP(w, r, authReq, data.IDPConfigID)
|
||||
|
@@ -58,7 +58,7 @@ func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if authReq == nil {
|
||||
http.Redirect(w, r, l.consolePath, http.StatusFound)
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID)
|
||||
|
@@ -39,8 +39,8 @@ type initPasswordData struct {
|
||||
HasSymbol string
|
||||
}
|
||||
|
||||
func InitPasswordLink(origin, userID, code string) string {
|
||||
return fmt.Sprintf("%s%s?userID=%s&code=%s", externalLink(origin), EndpointInitPassword, userID, code)
|
||||
func InitPasswordLink(origin, userID, code, orgID string) string {
|
||||
return fmt.Sprintf("%s%s?userID=%s&code=%s&orgID=%s", externalLink(origin), EndpointInitPassword, userID, code, orgID)
|
||||
}
|
||||
|
||||
func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -42,8 +42,8 @@ type initUserData struct {
|
||||
HasSymbol string
|
||||
}
|
||||
|
||||
func InitUserLink(origin, userID, code string, passwordSet bool) string {
|
||||
return fmt.Sprintf("%s%s?userID=%s&code=%s&passwordset=%t", externalLink(origin), EndpointInitUser, userID, code, passwordSet)
|
||||
func InitUserLink(origin, userID, code, orgID string, passwordSet bool) string {
|
||||
return fmt.Sprintf("%s%s?userID=%s&code=%s&passwordset=%t&orgID=%s", externalLink(origin), EndpointInitUser, userID, code, passwordSet, orgID)
|
||||
}
|
||||
|
||||
func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -120,7 +120,7 @@ func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
resourceOwner := l.getOrgID(authReq)
|
||||
resourceOwner := l.getOrgID(r, authReq)
|
||||
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
|
@@ -3,13 +3,16 @@ package login
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplLogin = "login"
|
||||
tmplLogin = "login"
|
||||
queryOrgID = "orgID"
|
||||
)
|
||||
|
||||
type loginData struct {
|
||||
@@ -17,8 +20,8 @@ type loginData struct {
|
||||
Register bool `schema:"register"`
|
||||
}
|
||||
|
||||
func LoginLink(origin string) string {
|
||||
return externalLink(origin) + EndpointLogin
|
||||
func LoginLink(origin, orgID string) string {
|
||||
return externalLink(origin) + EndpointLogin + "?orgID=" + orgID
|
||||
}
|
||||
|
||||
func externalLink(origin string) string {
|
||||
@@ -32,12 +35,23 @@ func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if authReq == nil {
|
||||
http.Redirect(w, r, l.consolePath, http.StatusFound)
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) defaultRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
orgID := r.FormValue(queryOrgID)
|
||||
policy, err := l.getLoginPolicy(r, orgID)
|
||||
logging.OnError(err).WithField("orgID", orgID).Error("error loading login policy")
|
||||
redirect := l.consolePath
|
||||
if policy != nil && policy.DefaultRedirectURI != "" {
|
||||
redirect = policy.DefaultRedirectURI
|
||||
}
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
}
|
||||
|
||||
func (l *Login) handleLoginName(w http.ResponseWriter, r *http.Request) {
|
||||
authReq, err := l.getAuthRequest(r)
|
||||
if err != nil {
|
||||
|
@@ -27,8 +27,8 @@ type mailVerificationData struct {
|
||||
UserID string
|
||||
}
|
||||
|
||||
func MailVerificationLink(origin, userID, code string) string {
|
||||
return fmt.Sprintf("%s%s?userID=%s&code=%s", externalLink(origin), EndpointMailVerification, userID, code)
|
||||
func MailVerificationLink(origin, userID, code, orgID string) string {
|
||||
return fmt.Sprintf("%s%s?userID=%s&code=%s&orgID=%s", externalLink(origin), EndpointMailVerification, userID, code, orgID)
|
||||
}
|
||||
|
||||
func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -40,6 +40,10 @@ func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Password, authReq.AgentID, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
if authReq.LoginPolicy.IgnoreUnknownUsernames {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderPassword(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,6 +30,9 @@ func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
passwordCodeGenerator, err := l.query.InitEncryptionGenerator(r.Context(), domain.SecretGeneratorTypePasswordResetCode, l.userCodeAlg)
|
||||
if err != nil {
|
||||
if authReq.LoginPolicy.IgnoreUnknownUsernames && errors.IsNotFound(err) {
|
||||
err = nil
|
||||
}
|
||||
l.renderPasswordResetDone(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
@@ -21,3 +21,10 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain
|
||||
func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) {
|
||||
return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID)
|
||||
}
|
||||
|
||||
func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) {
|
||||
if orgID == "" {
|
||||
return l.query.DefaultLoginPolicy(r.Context())
|
||||
}
|
||||
return l.query.LoginPolicyByID(r.Context(), orgID)
|
||||
}
|
||||
|
@@ -90,7 +90,7 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if authRequest == nil {
|
||||
http.Redirect(w, r, l.consolePath, http.StatusFound)
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
|
@@ -73,7 +73,7 @@ func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if authRequest == nil {
|
||||
http.Redirect(w, r, l.consolePath, http.StatusFound)
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authRequest)
|
||||
|
@@ -342,8 +342,8 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title
|
||||
Theme: l.getTheme(r),
|
||||
ThemeMode: l.getThemeMode(r),
|
||||
DarkMode: l.isDarkMode(r),
|
||||
PrivateLabelingOrgID: l.getPrivateLabelingID(authz.GetInstance(r.Context()).InstanceID(), authReq),
|
||||
OrgID: l.getOrgID(authReq),
|
||||
PrivateLabelingOrgID: l.getPrivateLabelingID(r, authReq),
|
||||
OrgID: l.getOrgID(r, authReq),
|
||||
OrgName: l.getOrgName(authReq),
|
||||
PrimaryDomain: l.getOrgPrimaryDomain(authReq),
|
||||
DisplayLoginNameSuffix: l.isDisplayLoginNameSuffix(authReq),
|
||||
@@ -361,6 +361,10 @@ func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title
|
||||
}
|
||||
privacyPolicy = authReq.PrivacyPolicy
|
||||
} else {
|
||||
labelPolicy, _ := l.query.ActiveLabelPolicyByOrg(r.Context(), baseData.PrivateLabelingOrgID)
|
||||
if labelPolicy != nil {
|
||||
baseData.LabelPolicy = labelPolicy.ToDomain()
|
||||
}
|
||||
policy, err := l.query.DefaultPrivacyPolicy(r.Context())
|
||||
if err != nil {
|
||||
return baseData
|
||||
@@ -446,9 +450,9 @@ func (l *Login) isDarkMode(r *http.Request) bool {
|
||||
return strings.HasSuffix(cookie.Value, "dark")
|
||||
}
|
||||
|
||||
func (l *Login) getOrgID(authReq *domain.AuthRequest) string {
|
||||
func (l *Login) getOrgID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||
if authReq == nil {
|
||||
return ""
|
||||
return r.FormValue(queryOrgID)
|
||||
}
|
||||
if authReq.RequestedOrgID != "" {
|
||||
return authReq.RequestedOrgID
|
||||
@@ -456,9 +460,12 @@ func (l *Login) getOrgID(authReq *domain.AuthRequest) string {
|
||||
return authReq.UserOrgID
|
||||
}
|
||||
|
||||
func (l *Login) getPrivateLabelingID(instanceID string, authReq *domain.AuthRequest) string {
|
||||
privateLabelingOrgID := instanceID
|
||||
func (l *Login) getPrivateLabelingID(r *http.Request, authReq *domain.AuthRequest) string {
|
||||
privateLabelingOrgID := authz.GetInstance(r.Context()).InstanceID()
|
||||
if authReq == nil {
|
||||
if id := r.FormValue(queryOrgID); id != "" {
|
||||
return id
|
||||
}
|
||||
return privateLabelingOrgID
|
||||
}
|
||||
if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified {
|
||||
|
@@ -322,6 +322,8 @@ Errors:
|
||||
Empty: Passwort ist leer
|
||||
Invalid: Passwort ungültig
|
||||
InvalidAndLocked: Password ist undgültig und Benutzer wurde gesperrt, melden Sie sich bei ihrem Administrator.
|
||||
UsernameOrPassword:
|
||||
Invalid: Username oder Passwort ist ungültig
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Passwort Policy konnte nicht gefunden werden
|
||||
MinLength: Passwort ist zu kurz
|
||||
|
@@ -323,6 +323,8 @@ Errors:
|
||||
Empty: Password is empty
|
||||
Invalid: Password is invalid
|
||||
InvalidAndLocked: Password is invalid and user is locked, contact your administrator.
|
||||
UsernameOrPassword:
|
||||
Invalid: Username or Password is invalid
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Password policy not found
|
||||
MinLength: Password is to short
|
||||
|
@@ -323,6 +323,8 @@ Errors:
|
||||
Empty: La password è vuota
|
||||
Invalid: La password non è valida
|
||||
InvalidAndLocked: La password non è valida e l'utente è bloccato, contatta il tuo amministratore.
|
||||
UsernameOrPassword:
|
||||
Invalid: Il nome utente o la password non sono validi
|
||||
PasswordComplexityPolicy:
|
||||
NotFound: Impostazioni della password non trovate
|
||||
MinLength: La password è troppo corta
|
||||
|
@@ -13,6 +13,7 @@
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="userID" value="{{ .UserID }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
@@ -56,4 +57,4 @@
|
||||
<script src="{{ resourceUrl "scripts/init_password_check.js" }}"></script>
|
||||
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -12,15 +12,13 @@
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="lgnactions">
|
||||
<a class="lgn-stroked-button lgn-primary" href="{{ loginUrl }}">
|
||||
{{t "InitPasswordDone.CancelButtonText"}}
|
||||
</a>
|
||||
<div class="lgn-actions">
|
||||
<span class="fill-space"></span>
|
||||
<button class="lgn-raised-button lgn-primary" type="submit">{{t "InitPasswordDone.NextButtonText"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -15,6 +15,7 @@
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="userID" value="{{ .UserID }}" />
|
||||
<input type="hidden" name="passwordSet" value="{{ .PasswordSet }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
@@ -63,4 +64,4 @@
|
||||
<script src="{{ resourceUrl "scripts/init_password_check.js" }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -13,6 +13,7 @@
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="lgn-actions lgn-reverse-order">
|
||||
<button class="lgn-raised-button lgn-primary" type="submit">{{t "InitUserDone.NextButtonText"}}</button>
|
||||
@@ -24,4 +25,4 @@
|
||||
</form>
|
||||
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -13,6 +13,7 @@
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="userID" value="{{ .UserID }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="fields">
|
||||
<label class="lgn-label" for="code">{{t "EmailVerification.CodeLabel"}}</label>
|
||||
@@ -41,4 +42,4 @@
|
||||
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -12,6 +12,7 @@
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="lgn-actions">
|
||||
<a class="lgn-stroked-button lgn-primary" href="{{ loginUrl }}">
|
||||
@@ -29,4 +30,4 @@
|
||||
</form>
|
||||
|
||||
|
||||
{{template "main-bottom" .}}
|
||||
{{template "main-bottom" .}}
|
||||
|
@@ -16,6 +16,7 @@
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" name="orgID" value="{{ .OrgID }}" />
|
||||
|
||||
<div class="lgn-actions">
|
||||
{{if not .HideNextButton }}
|
||||
|
Reference in New Issue
Block a user