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:
Livio Amstutz
2022-05-16 15:39:09 +02:00
committed by GitHub
parent f1fa74a2c0
commit 411d7c6c5c
69 changed files with 655 additions and 107 deletions

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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) {

View File

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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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())

View File

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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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" .}}

View File

@@ -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" .}}

View File

@@ -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" .}}

View File

@@ -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" .}}

View File

@@ -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" .}}

View File

@@ -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" .}}

View File

@@ -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 }}