mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# Which Problems Are Solved When resolving the organization for a self-registration, the resource owner was determined only from an explicitly requested org, falling back to the default org. The login policy applied to the same request, however, is resolved from the requested org, the user org, and only then the default org. As a result the two could diverge — e.g. after the login context had already resolved a user's organization — and a user could be registered in a different organization than the one whose settings were evaluated for the request. # How the Problems Are Solved `determineResourceOwner` now uses the same resolution order as the policy lookup (requested org, then user org, then default org), so the organization a user is registered in matches the one the request was evaluated against. # Additional Changes Added a unit test covering the resolution order. # Additional Context Aligns login v1 registration org resolution with the behavior in login v2. Co-authored-by: Claude Opus 4.8 <[email protected]>
227 lines
6.8 KiB
Go
227 lines
6.8 KiB
Go
package login
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
const (
|
|
tmplRegister = "register"
|
|
)
|
|
|
|
type registerFormData struct {
|
|
Email domain.EmailAddress `schema:"email"`
|
|
Username string `schema:"username"`
|
|
Firstname string `schema:"firstname"`
|
|
Lastname string `schema:"lastname"`
|
|
Language string `schema:"language"`
|
|
Password string `schema:"register-password"`
|
|
Password2 string `schema:"register-password-confirmation"`
|
|
TermsConfirm bool `schema:"terms-confirm"`
|
|
}
|
|
|
|
type registerData struct {
|
|
baseData
|
|
registerFormData
|
|
MinLength uint64
|
|
HasUppercase string
|
|
HasLowercase string
|
|
HasNumber string
|
|
HasSymbol string
|
|
ShowUsername bool
|
|
ShowUsernameSuffix bool
|
|
OrgRegister bool
|
|
}
|
|
|
|
func determineResourceOwner(ctx context.Context, authRequest *domain.AuthRequest) string {
|
|
if authRequest == nil {
|
|
return authz.GetInstance(ctx).DefaultOrganisationID()
|
|
}
|
|
if authRequest.RequestedOrgID != "" {
|
|
return authRequest.RequestedOrgID
|
|
}
|
|
if authRequest.UserOrgID != "" {
|
|
return authRequest.UserOrgID
|
|
}
|
|
return authz.GetInstance(ctx).DefaultOrganisationID()
|
|
}
|
|
|
|
func (l *Login) handleRegister(w http.ResponseWriter, r *http.Request) {
|
|
data := new(registerFormData)
|
|
authRequest, err := l.getAuthRequestAndParseData(r, data)
|
|
if err != nil {
|
|
l.renderError(w, r, authRequest, err)
|
|
return
|
|
}
|
|
if err := l.checkRegistrationAllowed(r, determineResourceOwner(r.Context(), authRequest), authRequest); err != nil {
|
|
l.renderError(w, r, authRequest, err)
|
|
return
|
|
}
|
|
l.renderRegister(w, r, authRequest, data, nil)
|
|
}
|
|
|
|
func (l *Login) checkRegistrationAllowed(r *http.Request, orgID string, authReq *domain.AuthRequest) error {
|
|
if authReq != nil {
|
|
if registrationAllowed(authReq) {
|
|
return nil
|
|
}
|
|
return zerrors.ThrowPreconditionFailed(nil, "VIEW-RRGRXz4kGw", "Errors.Org.LoginPolicy.RegistrationNotAllowed")
|
|
}
|
|
loginPolicy, err := l.getLoginPolicy(r, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if loginPolicy.AllowRegister && loginPolicy.AllowUsernamePassword {
|
|
return nil
|
|
}
|
|
return zerrors.ThrowPreconditionFailed(nil, "VIEW-Vq3bduAacD", "Errors.Org.LoginPolicy.RegistrationNotAllowed")
|
|
}
|
|
|
|
func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
|
|
data := new(registerFormData)
|
|
authRequest, err := l.getAuthRequestAndParseData(r, data)
|
|
if err != nil {
|
|
l.renderError(w, r, authRequest, err)
|
|
return
|
|
}
|
|
resourceOwner := determineResourceOwner(r.Context(), authRequest)
|
|
if err := l.checkRegistrationAllowed(r, resourceOwner, authRequest); err != nil {
|
|
l.renderError(w, r, authRequest, err)
|
|
return
|
|
}
|
|
if data.Password != data.Password2 {
|
|
err := zerrors.ThrowInvalidArgument(nil, "VIEW-KaGue", "Errors.User.Password.ConfirmationWrong")
|
|
l.renderRegister(w, r, authRequest, data, err)
|
|
return
|
|
}
|
|
// For consistency with the external authentication flow,
|
|
// the setMetadata() function is provided on the pre creation hook, for now,
|
|
// like for the ExternalAuthentication flow.
|
|
// If there is a need for additional context after registration,
|
|
// we could provide that method in the PostCreation trigger too,
|
|
// without breaking existing actions.
|
|
// Also, if that field is needed, we probably also should provide it
|
|
// for ExternalAuthentication.
|
|
user, metadatas, err := l.runPreCreationActions(authRequest, r, data.toHumanDomain(), make([]*domain.Metadata, 0), resourceOwner, domain.FlowTypeInternalAuthentication)
|
|
if err != nil {
|
|
l.renderRegister(w, r, authRequest, data, err)
|
|
return
|
|
}
|
|
|
|
human := command.AddHumanFromDomain(user, metadatas, authRequest, nil)
|
|
err = l.command.AddUserHuman(setContext(r.Context(), resourceOwner), resourceOwner, human, true, l.userCodeAlg)
|
|
if err != nil {
|
|
l.renderRegister(w, r, authRequest, data, err)
|
|
return
|
|
}
|
|
userGrants, err := l.runPostCreationActions(human.ID, authRequest, r, resourceOwner, domain.FlowTypeInternalAuthentication)
|
|
if err != nil {
|
|
l.renderError(w, r, authRequest, err)
|
|
return
|
|
}
|
|
|
|
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
|
|
if err != nil {
|
|
l.renderError(w, r, authRequest, err)
|
|
return
|
|
}
|
|
|
|
if authRequest == nil {
|
|
l.defaultRedirect(w, r)
|
|
return
|
|
}
|
|
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
|
err = l.authRepo.SelectUser(r.Context(), authRequest.ID, human.ID, userAgentID, false)
|
|
if err != nil {
|
|
l.renderRegister(w, r, authRequest, data, err)
|
|
return
|
|
}
|
|
l.renderNextStep(w, r, authRequest)
|
|
}
|
|
|
|
func (l *Login) renderRegister(w http.ResponseWriter, r *http.Request, authRequest *domain.AuthRequest, formData *registerFormData, err error) {
|
|
translator := l.getTranslator(r.Context(), authRequest)
|
|
if formData == nil {
|
|
formData = new(registerFormData)
|
|
}
|
|
if formData.Language == "" {
|
|
formData.Language = l.renderer.ReqLang(translator, r).String()
|
|
}
|
|
|
|
resourceOwner := determineResourceOwner(r.Context(), authRequest)
|
|
data := registerData{
|
|
baseData: l.getBaseData(r, authRequest, translator, "RegistrationUser.Title", "RegistrationUser.Description", err),
|
|
registerFormData: *formData,
|
|
}
|
|
|
|
pwPolicy := l.getPasswordComplexityPolicy(r, resourceOwner)
|
|
if pwPolicy != nil {
|
|
data.MinLength = pwPolicy.MinLength
|
|
if pwPolicy.HasUppercase {
|
|
data.HasUppercase = UpperCaseRegex
|
|
}
|
|
if pwPolicy.HasLowercase {
|
|
data.HasLowercase = LowerCaseRegex
|
|
}
|
|
if pwPolicy.HasSymbol {
|
|
data.HasSymbol = SymbolRegex
|
|
}
|
|
if pwPolicy.HasNumber {
|
|
data.HasNumber = NumberRegex
|
|
}
|
|
}
|
|
|
|
orgIAMPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
|
if err != nil {
|
|
l.renderRegister(w, r, authRequest, formData, err)
|
|
return
|
|
}
|
|
data.ShowUsername = orgIAMPolicy.UserLoginMustBeDomain
|
|
data.OrgRegister = orgIAMPolicy.UserLoginMustBeDomain
|
|
|
|
labelPolicy, err := l.getLabelPolicy(r, resourceOwner)
|
|
if err != nil {
|
|
l.renderRegister(w, r, authRequest, formData, err)
|
|
return
|
|
}
|
|
data.ShowUsernameSuffix = !labelPolicy.HideLoginNameSuffix
|
|
|
|
funcs := map[string]interface{}{
|
|
"selectedLanguage": func(l string) bool {
|
|
if formData == nil {
|
|
return false
|
|
}
|
|
return formData.Language == l
|
|
},
|
|
}
|
|
if authRequest == nil {
|
|
l.customTexts(r.Context(), translator, resourceOwner)
|
|
}
|
|
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegister], data, funcs)
|
|
}
|
|
|
|
func (d registerFormData) toHumanDomain() *domain.Human {
|
|
return &domain.Human{
|
|
Username: d.Username,
|
|
Profile: &domain.Profile{
|
|
FirstName: d.Firstname,
|
|
LastName: d.Lastname,
|
|
PreferredLanguage: language.Make(d.Language),
|
|
},
|
|
Password: &domain.Password{
|
|
SecretString: d.Password,
|
|
},
|
|
Email: &domain.Email{
|
|
EmailAddress: d.Email,
|
|
},
|
|
}
|
|
}
|