fix: tos on external registration (#2164)

* faet: add tos checkbox to external login

* fix: add tos to external not found option

* fix: add tos to external not found option

* fix: show register external user overview

* fix: no init user mail on external register

* fix: custom login text

* add missing custom text tests on org

* add missing custom text tests on iam

* fix: custom login text external registration overview tests

* fix: back button on registration overview

* fix: add texts, change register form

* fix: external not found html

* fix: remove form validation

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi
2021-08-11 13:50:03 +02:00
committed by GitHub
parent 87fa6e58fb
commit b104011418
28 changed files with 3412 additions and 164 deletions

View File

@@ -1,6 +1,10 @@
package handler
import (
"net/http"
"strings"
"time"
"github.com/caos/oidc/pkg/client/rp"
"github.com/caos/oidc/pkg/oidc"
"golang.org/x/oauth2"
@@ -11,9 +15,6 @@ import (
"github.com/caos/zitadel/internal/errors"
caos_errors "github.com/caos/zitadel/internal/errors"
iam_model "github.com/caos/zitadel/internal/iam/model"
"net/http"
"strings"
"time"
)
const (
@@ -34,6 +35,7 @@ type externalNotFoundOptionFormData struct {
Link bool `schema:"link"`
AutoRegister bool `schema:"autoregister"`
ResetLinking bool `schema:"resetlinking"`
TermsConfirm bool `schema:"terms-confirm"`
}
type externalNotFoundOptionData struct {

View File

@@ -6,6 +6,7 @@ import (
"github.com/caos/oidc/pkg/client/rp"
"github.com/caos/oidc/pkg/oidc"
"golang.org/x/text/language"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
@@ -13,6 +14,42 @@ import (
iam_model "github.com/caos/zitadel/internal/iam/model"
)
const (
tmplExternalRegisterOverview = "externalregisteroverview"
)
type externalRegisterFormData struct {
ExternalIDPConfigID string `schema:"external-idp-config-id"`
ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"`
ExternalIDPDisplayName string `schema:"external-idp-display-name"`
ExternalEmail string `schema:"external-email"`
ExternalEmailVerified bool `schema:"external-email-verified"`
Email string `schema:"email"`
Username string `schema:"username"`
Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"`
Nickname string `schema:"nickname"`
ExternalPhone string `schema:"external-phone"`
ExternalPhoneVerified bool `schema:"external-phone-verified"`
Phone string `schema:"phone"`
Language string `schema:"language"`
TermsConfirm bool `schema:"terms-confirm"`
}
type externalRegisterData struct {
baseData
externalRegisterFormData
ExternalIDPID string
ExternalIDPUserID string
ExternalIDPUserDisplayName string
ShowUsername bool
OrgRegister bool
ExternalEmail string
ExternalEmailVerified bool
ExternalPhone string
ExternalPhoneVerified bool
}
func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPData)
authReq, err := l.getAuthRequestAndParseData(r, data)
@@ -76,18 +113,77 @@ func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Reques
return
}
resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleOrgProjectCreator}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil
resourceOwner = authReq.RequestedOrgID
}
orgIamPolicy, err := l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
user, externalIDP := l.mapTokenToLoginHumanAndExternalIDP(orgIamPolicy, tokens, idpConfig)
l.renderExternalRegisterOverview(w, r, authReq, orgIamPolicy, user, externalIDP, nil)
}
func (l *Login) renderExternalRegisterOverview(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *iam_model.OrgIAMPolicyView, human *domain.Human, idp *domain.ExternalIDP, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := externalRegisterData{
baseData: l.getBaseData(r, authReq, "ExternalRegisterOverview", errID, errMessage),
externalRegisterFormData: externalRegisterFormData{
Email: human.EmailAddress,
Username: human.PreferredLoginName,
Firstname: human.FirstName,
Lastname: human.LastName,
Nickname: human.NickName,
Language: human.PreferredLanguage.String(),
},
ExternalIDPID: idp.IDPConfigID,
ExternalIDPUserID: idp.ExternalUserID,
ExternalIDPUserDisplayName: idp.DisplayName,
ExternalEmail: human.EmailAddress,
ExternalEmailVerified: human.IsEmailVerified,
ShowUsername: orgIAMPolicy.UserLoginMustBeDomain,
OrgRegister: orgIAMPolicy.UserLoginMustBeDomain,
}
if human.Phone != nil {
data.Phone = human.PhoneNumber
data.ExternalPhone = human.PhoneNumber
data.ExternalPhoneVerified = human.IsPhoneVerified
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalRegisterOverview], data, nil)
}
func (l *Login) handleExternalRegisterCheck(w http.ResponseWriter, r *http.Request) {
data := new(externalRegisterFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
iam, err := l.authRepo.GetIAM(r.Context())
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleOrgProjectCreator}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil
resourceOwner = authReq.RequestedOrgID
}
externalIDP, err := l.getExternalIDP(data)
if externalIDP == nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
user, err := l.mapExternalRegisterDataToUser(r, data)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
_, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, memberRoles)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
@@ -140,6 +236,9 @@ func (l *Login) mapTokenToLoginHumanAndExternalIDP(orgIamPolicy *iam_model.OrgIA
displayName = tokens.IDTokenClaims.GetEmail()
}
}
if displayName == "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
externalIDP := &domain.ExternalIDP{
IDPConfigID: idpConfig.IDPConfigID,
@@ -148,3 +247,43 @@ func (l *Login) mapTokenToLoginHumanAndExternalIDP(orgIamPolicy *iam_model.OrgIA
}
return human, externalIDP
}
func (l *Login) mapExternalRegisterDataToUser(r *http.Request, data *externalRegisterFormData) (*domain.Human, error) {
human := &domain.Human{
Username: data.Username,
Profile: &domain.Profile{
FirstName: data.Firstname,
LastName: data.Lastname,
PreferredLanguage: language.Make(data.Language),
NickName: data.Nickname,
},
Email: &domain.Email{
EmailAddress: data.Email,
},
}
if data.ExternalEmail != data.Email {
human.IsEmailVerified = false
} else {
human.IsEmailVerified = data.ExternalEmailVerified
}
if data.ExternalPhone == "" {
return human, nil
}
human.Phone = &domain.Phone{
PhoneNumber: data.Phone,
}
if data.ExternalPhone != data.Phone {
human.IsPhoneVerified = false
} else {
human.IsPhoneVerified = data.ExternalPhoneVerified
}
return human, nil
}
func (l *Login) getExternalIDP(data *externalRegisterFormData) (*domain.ExternalIDP, error) {
return &domain.ExternalIDP{
IDPConfigID: data.ExternalIDPConfigID,
ExternalUserID: data.ExternalIDPExtUserID,
DisplayName: data.ExternalIDPDisplayName,
}, nil
}

View File

@@ -62,6 +62,7 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
tmplChangePasswordDone: "change_password_done.html",
tmplRegisterOption: "register_option.html",
tmplRegister: "register.html",
tmplExternalRegisterOverview: "external_register_overview.html",
tmplLogoutDone: "logout_done.html",
tmplRegisterOrg: "register_org.html",
tmplChangeUsername: "change_username.html",
@@ -181,6 +182,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
"orgRegistrationUrl": func() string {
return path.Join(r.pathPrefix, EndpointRegisterOrg)
},
"externalRegistrationUrl": func() string {
return path.Join(r.pathPrefix, EndpointExternalRegister)
},
"changeUsernameUrl": func() string {
return path.Join(r.pathPrefix, EndpointChangeUsername)
},

View File

@@ -82,6 +82,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointRegister, login.handleRegister).Methods(http.MethodGet)
router.HandleFunc(EndpointRegister, login.handleRegisterCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointExternalRegister, login.handleExternalRegister).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalRegister, login.handleExternalRegisterCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalRegisterCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).Methods(http.MethodGet)

View File

@@ -221,6 +221,26 @@ RegistrationUser:
BackButtonText: zurück
NextButtonText: weiter
ExternalRegistrationUserOverview:
Title: Externer Benutzer Registration
Description: Deine Benutzerangaben werden vom ausgewählten Provider übernommen. Du kannst sie hier ändern und ergänzen, bevor dein Benutzer angelegt wird.
EmailLabel: E-Mail
UsernameLabel: Benutzername
FirstnameLabel: Vorname
LastnameLabel: Nachname
NicknameLabel: Nachname
PhoneLabel: Telefonnummer
LanguageLabel: Sprache
German: Deutsch
English: English
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
TosConfirmAnd: und die
PrivacyLinkText: Datenschutzerklärung
BackButtonText: zurück
NextButtonText: speichern
RegistrationOrg:
Title: Organisations Registration
Description: Gib deinen Organisationsnamen und deine Benutzerangaben an.
@@ -260,6 +280,11 @@ ExternalNotFoundOption:
Description: Externer Benutzer konnte nicht gefunden werden. Willst du deinen Benutzer mit einem bestehenden verlinken oder diesen als neuen Benutzer registrieren.
LinkButtonText: Verlinken
AutoRegisterButtonText: Automatisches registrieren
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
TosConfirmAnd: und die
PrivacyLinkText: Datenschutzerklärung
Footer:
PoweredBy: Powered By
@@ -305,6 +330,8 @@ Errors:
GeneratorAlgNotSupported: Generator Algorithums wird nicht unterstützt
EmailVerify:
UserIDEmpty: UserID ist leer
ExternalData:
CouldNotRead: Externe Daten konnten nicht korrekt gelesen werden
MFA:
NoProviders: Es stehen keine Multifaktorprovider zur Verfügung
OTP:
@@ -318,6 +345,9 @@ Errors:
ExternalIDP:
IDPTypeNotImplemented: IDP Typ ist nicht implementiert
NotAllowed: Externer Login Provider ist nicht erlaubt
IDPConfigIDEmpty: Identity Provider ID ist leer
ExternalUserIDEmpty: Externe User ID ist leer
UserDisplayNameEmpty: Benutzer Anzeige Name ist leer
GrantRequired: Der Login an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte melde dich bei deinem Administrator.
IdentityProvider:
InvalidConfig: Identitäts Provider Konfiguration ist ungültig

View File

@@ -221,6 +221,27 @@ RegistrationUser:
BackButtonText: back
NextButtonText: next
ExternalRegistrationUserOverview:
Title: External User Registration
Description: We have taken your user details from the selected provider. You can now change or complete them.
EmailLabel: E-Mail
UsernameLabel: Username
FirstnameLabel: Firstname
LastnameLabel: Lastname
NicknameLabel: Nickname
PhoneLabel: Phonenumber
LanguageLabel: Language
German: Deutsch
English: English
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
TosConfirmAnd: and the
PrivacyLinkText: privacy policy
ExternalLogin: or register with an external user
BackButtonText: back
NextButtonText: save
RegistrationOrg:
Title: Organisation Registration
Description: Enter your organisationname and userdata.
@@ -260,6 +281,11 @@ ExternalNotFoundOption:
Description: External user not found. Do you want to link your user or auto register a new one.
LinkButtonText: Link
AutoRegisterButtonText: Auto register
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
TosConfirmAnd: and the
PrivacyLinkText: privacy policy
Footer:
PoweredBy: Powered By
@@ -305,6 +331,8 @@ Errors:
GeneratorAlgNotSupported: Unsupported generator algorithm
EmailVerify:
UserIDEmpty: UserID is empty
ExternalData:
CouldNotRead: External data could not be read correctly
MFA:
NoProviders: No available multifactor providers
OTP:
@@ -318,6 +346,9 @@ Errors:
ExternalIDP:
IDPTypeNotImplemented: IDP Type is not implemented
NotAllowed: External Login Provider not allowed
IDPConfigIDEmpty: Identity Provider ID is empty
ExternalUserIDEmpty: External User ID is empty
UserDisplayNameEmpty: User Display Name is empty
GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator.
IdentityProvider:
InvalidConfig: Identity Provider configuration is invalid

View File

@@ -0,0 +1,5 @@
let button1 = document.getElementById("link-button");
disableSubmit(undefined, button1);
let button2 = document.getElementById("auto-register-button");
disableSubmit(undefined, button2);

View File

@@ -3,32 +3,65 @@
<div class="lgn-head">
<h1>{{t "ExternalNotFoundOption.Title"}}</h1>
<p>{{t "ExternalNotFoundOption.Description"}}</p>
</div>
<form action="{{ externalNotFoundOptionUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<div class="lgn-register">
{{ if or .TOSLink .PrivacyLink }}
<div class="lgn-field">
<label class="lgn-label">{{t "ExternalNotFoundOption.TosAndPrivacyLabel"}}</label>
<div class="lgn-checkbox">
<input type="checkbox" id="register-term-confirmation"
name="register-term-confirmation" required>
<label for="register-term-confirmation">
{{t "ExternalNotFoundOption.TosConfirm"}}
{{ if .TOSLink }}
<a class="tos-link" target="_blank" href="{{ .TOSLink }}" rel="noopener noreferrer">
{{t "ExternalNotFoundOption.TosLinkText"}}
</a>
{{end}}
{{ if and .TOSLink .PrivacyLink }}
{{t "ExternalNotFoundOption.TosConfirmAnd"}}
{{ end }}
{{ if .PrivacyLink }}
<a class="tos-link" target="_blank" href="{{ .PrivacyLink}}" rel="noopener noreferrer">
{{t "ExternalNotFoundOption.PrivacyLinkText"}}
</a>
{{end}}
</label>
</div>
</div>
{{ end }}
</div>
{{template "error-message" .}}
<div class="lgn-actions">
<button class="lgn-icon-button lgn-left-action" name="resetlinking" value="true"
formnovalidate>
formnovalidate>
<i class="lgn-icon-arrow-left-solid"></i>
</button>
<button class="lgn-raised-button lgn-primary" name="link" value="true"
formnovalidate>{{t "ExternalNotFoundOption.LinkButtonText"}}</button>
<span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" name="autoregister" value="true"
formnovalidate>{{t "ExternalNotFoundOption.AutoRegisterButtonText"}}</button>
<button class="lgn-raised-button lgn-primary" name="link" value="true">
{{t "ExternalNotFoundOption.LinkButtonText"}}
</button>
<span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" name="autoregister" value="true">
{{t "ExternalNotFoundOption.AutoRegisterButtonText"}}
</button>
</div>
{{template "error-message" .}}
</form>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
<script src="{{ resourceUrl "scripts/external_not_found_check.js" }}"></script>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,114 @@
{{template "main-top" .}}
<div class="lgn-head">
<h1>{{t "ExternalRegistrationUserOverview.Title"}}</h1>
<p>{{t "ExternalRegistrationUserOverview.Description"}}</p>
</div>
<form action="{{ externalRegistrationUrl }}" method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
<input type="hidden" id="external-idp-config-id" name="external-idp-config-id" value="{{ .ExternalIDPID }}" />
<input type="hidden" id="external-idp-ext-user-id" name="external-idp-ext-user-id" value="{{ .ExternalIDPUserID }}" />
<input type="hidden" id="external-idp-display-name" name="external-idp-display-name" value="{{ .ExternalIDPUserDisplayName }}" />
<input type="hidden" id="external-email" name="external-email" value="{{ .ExternalEmail }}" />
<input type="hidden" id="external-email-verified" name="external-email-verified" value="{{ .ExternalEmailVerified }}" />
<input type="hidden" id="external-phone" name="external-phone" value="{{ .ExternalPhone }}" />
<input type="hidden" id="external-phone-verified" name="external-phone-verified" value="{{ .ExternalPhoneVerified }}" />
<div class="lgn-register">
<div class="double-col">
<div class="lgn-field">
<label class="lgn-label" for="firstname">{{t "ExternalRegistrationUserOverview.FirstnameLabel"}}</label>
<input class="lgn-input" type="text" id="firstname" name="firstname" autocomplete="given-name"
value="{{ .Firstname }}" autofocus required>
</div>
<div class="lgn-field">
<label class="lgn-label" for="lastname">{{t "ExternalRegistrationUserOverview.LastnameLabel"}}</label>
<input class="lgn-input" type="text" id="lastname" name="lastname" autocomplete="family-name"
value="{{ .Lastname }}" required>
</div>
</div>
<div class="lgn-field double">
<label class="lgn-label" for="email">{{t "ExternalRegistrationUserOverview.EmailLabel"}}</label>
<input class="lgn-input" type="text" id="email" name="email" autocomplete="email" value="{{ .Email }}" required>
</div>
<div class="lgn-field double">
<label class="lgn-label" for="phone">{{t "ExternalRegistrationUserOverview.PhoneLabel"}}</label>
<input class="lgn-input" type="text" id="phone" name="phone" autocomplete="tel" value="{{ .Phone }}">
</div>
{{if .ShowUsername}}
<div class="lgn-field double">
<label class="lgn-label" for="username">{{t "ExternalRegistrationUserOverview.UsernameLabel"}}</label>
<div class="lgn-suffix-wrapper">
<input class="lgn-input lgn-suffix-input" type="text" id="username" name="username" autocomplete="email" value="{{ .Email }}" required>
{{if .DisplayLoginNameSuffix}}
<span id="default-login-suffix" lgnsuffix class="loginname-suffix">@{{.PrimaryDomain}}</span>
{{end}}
</div>
</div>
{{end}}
<div class="double-col">
<div class="lgn-field">
<label class="lgn-label" for="languages">{{t "ExternalRegistrationUserOverview.LanguageLabel"}}</label>
<select id="languages" name="language">
<option value=""></option>
<option value="de" id="de" {{if (selectedLanguage "de")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.German"}}
</option>
<option value="en" id="en" {{if (selectedLanguage "en")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.English"}}
</option>
</select>
</div>
</div>
{{ if or .TOSLink .PrivacyLink }}
<div class="lgn-field">
<label class="lgn-label">{{t "ExternalRegistrationUserOverview.TosAndPrivacyLabel"}}</label>
<div class="lgn-checkbox">
<input type="checkbox" id="register-term-confirmation"
name="register-term-confirmation" required>
<label for="register-term-confirmation">
{{t "ExternalRegistrationUserOverview.TosConfirm"}}
{{ if .TOSLink }}
<a class="tos-link" target="_blank" href="{{ .TOSLink }}" rel="noopener noreferrer">
{{t "ExternalRegistrationUserOverview.TosLinkText"}}
</a>
{{end}}
{{ if and .TOSLink .PrivacyLink }}
{{t "ExternalRegistrationUserOverview.TosConfirmAnd"}}
{{ end }}
{{ if .PrivacyLink }}
<a class="tos-link" target="_blank" href="{{ .PrivacyLink}}" rel="noopener noreferrer">
{{t "ExternalRegistrationUserOverview.PrivacyLinkText"}}
</a>
{{end}}
</label>
</div>
</div>
{{ end }}
</div>
{{template "error-message" .}}
<div class="lgn-actions">
<a class="lgn-stroked-button lgn-primary" href="{{ registerOptionUrl }}">
{{t "ExternalRegistrationUserOverview.BackButtonText"}}
</a>
<span class="fill-space"></span>
<button class="lgn-raised-button lgn-primary" id="submit-button" type="submit">{{t "ExternalRegistrationUserOverview.NextButtonText"}}</button>
</div>
</form>
<script src="{{ resourceUrl "scripts/input_suffix_offset.js" }}"></script>
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
{{template "main-bottom" .}}

View File

@@ -39,7 +39,7 @@
{{if .ShowUsername}}
<div class="lgn-field double">
<label class="lgn-label" for="email">{{t "RegistrationUser.UsernameLabel"}}</label>
<label class="lgn-label" for="username">{{t "RegistrationUser.UsernameLabel"}}</label>
<div class="lgn-suffix-wrapper">
<input class="lgn-input lgn-suffix-input" type="text" id="username" name="username" autocomplete="email" value="{{ .Email }}" required>
{{if .DisplayLoginNameSuffix}}