feat: run on a single port (#3163)

* start v2

* start

* run

* some cleanup

* remove v2 pkg again

* simplify

* webauthn

* remove unused config

* fix login path in Dockerfile

* fix asset_generator.go

* health handler

* fix grpc web

* refactor

* merge

* build new main.go

* run new main.go

* update logging pkg

* fix error msg

* update logging

* cleanup

* cleanup

* go mod tidy

* change localDevMode

* fix customEndpoints

* update logging

* comments

* change local flag to external configs

* fix location generated go code

* fix

Co-authored-by: fforootd <florian@caos.ch>
This commit is contained in:
Livio Amstutz
2022-02-14 17:22:30 +01:00
committed by GitHub
parent 2f3a482ade
commit 389eb4a27a
306 changed files with 1708 additions and 1567 deletions

View File

@@ -0,0 +1,129 @@
package console
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/http/middleware"
)
type Config struct {
ConsoleOverwriteDir string
ShortCache middleware.CacheConfig
LongCache middleware.CacheConfig
}
type spaHandler struct {
fileSystem http.FileSystem
}
const (
envRequestPath = "/assets/environment.json"
consoleDefaultDir = "./console/"
HandlerPrefix = "/ui/console"
)
var (
shortCacheFiles = []string{
"/",
"/index.html",
"/manifest.webmanifest",
"/ngsw.json",
"/ngsw-worker.js",
"/safety-worker.js",
"/worker-basic.min.js",
}
)
func (i *spaHandler) Open(name string) (http.File, error) {
ret, err := i.fileSystem.Open(name)
if !os.IsNotExist(err) || path.Ext(name) != "" {
return ret, err
}
return i.fileSystem.Open("/index.html")
}
func Start(config Config, domain, url, issuer, clientID string) (http.Handler, error) {
environmentJSON, err := createEnvironmentJSON(url, issuer, clientID)
if err != nil {
return nil, fmt.Errorf("unable to marshal env for console: %w", err)
}
consoleDir := consoleDefaultDir
if config.ConsoleOverwriteDir != "" {
consoleDir = config.ConsoleOverwriteDir
}
consoleHTTPDir := http.Dir(consoleDir)
cache := assetsCacheInterceptorIgnoreManifest(
config.ShortCache.MaxAge,
config.ShortCache.SharedMaxAge,
config.LongCache.MaxAge,
config.LongCache.SharedMaxAge,
)
security := middleware.SecurityHeaders(csp(domain), nil)
handler := &http.ServeMux{}
handler.Handle("/", cache(security(http.FileServer(&spaHandler{consoleHTTPDir}))))
handler.Handle(envRequestPath, cache(security(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write(environmentJSON)
logging.OnError(err).Error("error serving environment.json")
}))))
return handler, nil
}
func csp(zitadelDomain string) *middleware.CSP {
if !strings.HasPrefix(zitadelDomain, "*.") {
zitadelDomain = "*." + zitadelDomain
}
csp := middleware.DefaultSCP
csp.StyleSrc = csp.StyleSrc.AddInline()
csp.ScriptSrc = csp.ScriptSrc.AddEval()
csp.ConnectSrc = csp.ConnectSrc.AddHost(zitadelDomain)
csp.ImgSrc = csp.ImgSrc.AddHost(zitadelDomain).AddScheme("blob")
return &csp
}
func createEnvironmentJSON(url, issuer, clientID string) ([]byte, error) {
environment := struct {
AuthServiceUrl string `json:"authServiceUrl,omitempty"`
MgmtServiceUrl string `json:"mgmtServiceUrl,omitempty"`
AdminServiceUrl string `json:"adminServiceUrl,omitempty"`
SubscriptionServiceUrl string `json:"subscriptionServiceUrl,omitempty"`
AssetServiceUrl string `json:"assetServiceUrl,omitempty"`
Issuer string `json:"issuer,omitempty"`
ClientID string `json:"clientid,omitempty"`
}{
AuthServiceUrl: url,
MgmtServiceUrl: url,
AdminServiceUrl: url,
SubscriptionServiceUrl: url,
AssetServiceUrl: url,
Issuer: issuer,
ClientID: clientID,
}
return json.Marshal(environment)
}
func assetsCacheInterceptorIgnoreManifest(shortMaxAge, shortSharedMaxAge, longMaxAge, longSharedMaxAge time.Duration) func(http.Handler) http.Handler {
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, file := range shortCacheFiles {
if r.URL.Path == file {
middleware.AssetsCacheInterceptor(shortMaxAge, shortSharedMaxAge, handler).ServeHTTP(w, r)
return
}
}
middleware.AssetsCacheInterceptor(longMaxAge, longSharedMaxAge, handler).ServeHTTP(w, r)
return
})
}
}

View File

@@ -0,0 +1,36 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
)
const (
QueryAuthRequestID = "authRequestID"
queryUserAgentID = "userAgentID"
)
func (l *Login) getAuthRequest(r *http.Request) (*domain.AuthRequest, error) {
authRequestID := r.FormValue(QueryAuthRequestID)
if authRequestID == "" {
return nil, nil
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
return l.authRepo.AuthRequestByID(r.Context(), authRequestID, userAgentID)
}
func (l *Login) getAuthRequestAndParseData(r *http.Request, data interface{}) (*domain.AuthRequest, error) {
authReq, err := l.getAuthRequest(r)
if err != nil {
return authReq, err
}
err = l.parser.Parse(r, data)
return authReq, err
}
func (l *Login) getParseData(r *http.Request, data interface{}) error {
return l.parser.Parse(r, data)
}

View File

@@ -0,0 +1,72 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
)
const (
tmplChangePassword = "changepassword"
tmplChangePasswordDone = "changepassworddone"
)
type changePasswordData struct {
OldPassword string `schema:"change-old-password"`
NewPassword string `schema:"change-new-password"`
NewPasswordConfirmation string `schema:"change-password-confirmation"`
}
func (l *Login) handleChangePassword(w http.ResponseWriter, r *http.Request) {
data := new(changePasswordData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err = l.command.ChangePassword(setContext(r.Context(), authReq.UserOrgID), authReq.UserOrgID, authReq.UserID, data.OldPassword, data.NewPassword, userAgentID)
if err != nil {
l.renderChangePassword(w, r, authReq, err)
return
}
l.renderChangePasswordDone(w, r, authReq)
}
func (l *Login) renderChangePassword(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := passwordData{
baseData: l.getBaseData(r, authReq, "Change Password", errID, errMessage),
profileData: l.getProfileData(authReq),
}
policy, description, _ := l.getPasswordComplexityPolicy(r, authReq, authReq.UserOrgID)
if policy != nil {
data.PasswordPolicyDescription = description
data.MinLength = policy.MinLength
if policy.HasUppercase {
data.HasUppercase = UpperCaseRegex
}
if policy.HasLowercase {
data.HasLowercase = LowerCaseRegex
}
if policy.HasSymbol {
data.HasSymbol = SymbolRegex
}
if policy.HasNumber {
data.HasNumber = NumberRegex
}
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplChangePassword], data, nil)
}
func (l *Login) renderChangePasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
var errType, errMessage string
data := l.getUserData(r, authReq, "Password Change Done", errType, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplChangePasswordDone], data, nil)
}

View File

@@ -0,0 +1,87 @@
package login
import (
"context"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/zitadel/internal/actions"
"github.com/caos/zitadel/internal/domain"
iam_model "github.com/caos/zitadel/internal/iam/model"
)
func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.ExternalUser, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView) (*domain.ExternalUser, error) {
resourceOwner := req.RequestedOrgID
if resourceOwner == "" {
resourceOwner = config.AggregateID
}
if resourceOwner == domain.IAMID {
iam, err := l.query.IAMByID(ctx, domain.IAMID)
if err != nil {
return nil, err
}
resourceOwner = iam.GlobalOrgID
}
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner)
if err != nil {
return nil, err
}
actionCtx := (&actions.Context{}).SetToken(tokens)
api := (&actions.API{}).SetExternalUser(user).SetMetadata(&user.Metadatas)
for _, a := range triggerActions {
err = actions.Run(actionCtx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
if err != nil {
return nil, err
}
}
return user, err
}
func (l *Login) customExternalUserToLoginUserMapping(user *domain.Human, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, metadata []*domain.Metadata, resourceOwner string) (*domain.Human, []*domain.Metadata, error) {
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePreCreation, resourceOwner)
if err != nil {
return nil, nil, err
}
actionCtx := (&actions.Context{}).SetToken(tokens)
api := (&actions.API{}).SetHuman(user).SetMetadata(&metadata)
for _, a := range triggerActions {
err = actions.Run(actionCtx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
if err != nil {
return nil, nil, err
}
}
return user, metadata, err
}
func (l *Login) customGrants(userID string, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, resourceOwner string) ([]*domain.UserGrant, error) {
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(context.TODO(), domain.FlowTypeExternalAuthentication, domain.TriggerTypePostCreation, resourceOwner)
if err != nil {
return nil, err
}
actionCtx := (&actions.Context{}).SetToken(tokens)
actionUserGrants := make([]actions.UserGrant, 0)
api := (&actions.API{}).SetUserGrants(&actionUserGrants)
for _, a := range triggerActions {
err = actions.Run(actionCtx, api, a.Script, a.Name, a.Timeout, a.AllowedToFail)
if err != nil {
return nil, err
}
}
return actionUserGrantsToDomain(userID, actionUserGrants), err
}
func actionUserGrantsToDomain(userID string, actionUserGrants []actions.UserGrant) []*domain.UserGrant {
if actionUserGrants == nil {
return nil
}
userGrants := make([]*domain.UserGrant, len(actionUserGrants))
for i, grant := range actionUserGrants {
userGrants[i] = &domain.UserGrant{
UserID: userID,
ProjectID: grant.ProjectID,
ProjectGrantID: grant.ProjectGrantID,
RoleKeys: grant.Roles,
}
}
return userGrants
}

View File

@@ -0,0 +1,485 @@
package login
import (
"encoding/base64"
"net/http"
"net/url"
"strings"
"time"
"github.com/caos/oidc/pkg/client/rp"
"github.com/caos/oidc/pkg/oidc"
"golang.org/x/oauth2"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
caos_errors "github.com/caos/zitadel/internal/errors"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/query"
)
const (
queryIDPConfigID = "idpConfigID"
tmplExternalNotFoundOption = "externalnotfoundoption"
)
type externalIDPData struct {
IDPConfigID string `schema:"idpConfigID"`
}
type externalIDPCallbackData struct {
State string `schema:"state"`
Code string `schema:"code"`
}
type externalNotFoundOptionFormData struct {
externalRegisterFormData
Link bool `schema:"linkbutton"`
AutoRegister bool `schema:"autoregisterbutton"`
ResetLinking bool `schema:"resetlinking"`
TermsConfirm bool `schema:"terms-confirm"`
}
type externalNotFoundOptionData struct {
baseData
externalNotFoundOptionFormData
ExternalIDPID string
ExternalIDPUserID string
ExternalIDPUserDisplayName string
ShowUsername bool
OrgRegister bool
ExternalEmail string
ExternalEmailVerified bool
ExternalPhone string
ExternalPhoneVerified bool
}
func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPConfigID string) {
for _, idp := range authReq.AllowedExternalIDPs {
if idp.IDPConfigID == selectedIDPConfigID {
l.handleIDP(w, r, authReq, selectedIDPConfigID)
return
}
}
l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "VIEW-Fsj7f", "Errors.User.ExternalIDP.NotAllowed"))
}
func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if authReq == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
l.handleIDP(w, r, authReq, data.IDPConfigID)
}
func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPConfigID string) {
idpConfig, err := l.getIDPConfigByID(r, selectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
if !idpConfig.IsOIDC {
l.handleJWTAuthorize(w, r, authReq, idpConfig)
return
}
l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalLoginCallback)
}
func (l *Login) handleOIDCAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) {
provider, err := l.getRPConfig(idpConfig, callbackEndpoint)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
http.Redirect(w, r, rp.AuthURL(authReq.ID, provider, rp.WithPrompt(oidc.PromptSelectAccount)), http.StatusFound)
}
func (l *Login) handleJWTAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) {
redirect, err := url.Parse(idpConfig.JWTEndpoint)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
q := redirect.Query()
q.Set(QueryAuthRequestID, authReq.ID)
userAgentID, ok := http_mw.UserAgentIDFromCtx(r.Context())
if !ok {
l.renderLogin(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "LOGIN-dsgg3", "Errors.AuthRequest.UserAgentNotFound"))
return
}
nonce, err := l.IDPConfigAesCrypto.Encrypt([]byte(userAgentID))
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce))
redirect.RawQuery = q.Encode()
http.Redirect(w, r, redirect.String(), http.StatusFound)
}
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPCallbackData)
err := l.getParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if idpConfig.IsOIDC {
provider, err := l.getRPConfig(idpConfig, EndpointExternalLoginCallback)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
tokens, err := rp.CodeExchange(r.Context(), data.Code, provider)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens)
return
}
l.renderError(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "RP-asff2", "Errors.ExternalIDP.IDPTypeNotImplemented"))
}
func (l *Login) getRPConfig(idpConfig *iam_model.IDPConfigView, callbackEndpoint string) (rp.RelyingParty, error) {
oidcClientSecret, err := crypto.DecryptString(idpConfig.OIDCClientSecret, l.IDPConfigAesCrypto)
if err != nil {
return nil, err
}
if idpConfig.OIDCIssuer != "" {
return rp.NewRelyingPartyOIDC(idpConfig.OIDCIssuer, idpConfig.OIDCClientID, oidcClientSecret, l.baseURL+callbackEndpoint, idpConfig.OIDCScopes, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second)))
}
if idpConfig.OAuthAuthorizationEndpoint == "" || idpConfig.OAuthTokenEndpoint == "" {
return nil, caos_errors.ThrowPreconditionFailed(nil, "RP-4n0fs", "Errors.IdentityProvider.InvalidConfig")
}
oauth2Config := &oauth2.Config{
ClientID: idpConfig.OIDCClientID,
ClientSecret: oidcClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: idpConfig.OAuthAuthorizationEndpoint,
TokenURL: idpConfig.OAuthTokenEndpoint,
},
RedirectURL: l.baseURL + callbackEndpoint,
Scopes: idpConfig.OIDCScopes,
}
return rp.NewRelyingPartyOAuth(oauth2Config, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second)))
}
func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
externalUser, err := l.customExternalUserMapping(r.Context(), externalUser, tokens, authReq, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, userAgentID, externalUser, domain.BrowserInfoFromRequest(r))
if err != nil {
if errors.IsNotFound(err) {
err = nil
}
iam, err := l.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
return
}
resourceOwner := iam.GlobalOrgID
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
resourceOwner = authReq.RequestedOrgID
}
orgIAMPolicy, err := l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
return
}
human, idpLinking, _ := l.mapExternalUserToLoginUser(orgIAMPolicy, externalUser, idpConfig)
if !idpConfig.AutoRegister {
l.renderExternalNotFoundOption(w, r, authReq, iam, orgIAMPolicy, human, idpLinking, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, iam, orgIAMPolicy, human, idpLinking, err)
return
}
l.handleAutoRegister(w, r, authReq)
return
}
if len(externalUser.Metadatas) > 0 {
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID)
if err != nil {
return
}
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, iam *query.IAM, orgIAMPolicy *query.OrgIAMPolicy, human *domain.Human, externalIDP *domain.UserIDPLink, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if orgIAMPolicy == nil {
iam, err = l.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
resourceOwner := iam.GlobalOrgID
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
resourceOwner = authReq.RequestedOrgID
}
orgIAMPolicy, err = l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
}
if human == nil || externalIDP == nil {
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
human, externalIDP, _ = l.mapExternalUserToLoginUser(orgIAMPolicy, linkingUser, idpConfig)
}
data := externalNotFoundOptionData{
baseData: l.getBaseData(r, authReq, "ExternalNotFoundOption", errID, errMessage),
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
externalRegisterFormData: externalRegisterFormData{
Email: human.EmailAddress,
Username: human.Username,
Firstname: human.FirstName,
Lastname: human.LastName,
Nickname: human.NickName,
Language: human.PreferredLanguage.String(),
},
},
ExternalIDPID: externalIDP.IDPConfigID,
ExternalIDPUserID: externalIDP.ExternalUserID,
ExternalIDPUserDisplayName: externalIDP.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[tmplExternalNotFoundOption], data, nil)
}
func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) {
data := new(externalNotFoundOptionFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
return
}
if data.Link {
l.renderLogin(w, r, authReq, nil)
return
} else if data.ResetLinking {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.ResetLinkingUsers(r.Context(), authReq.ID, userAgentID)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
}
l.handleLogin(w, r)
return
}
l.handleAutoRegister(w, r, authReq)
}
func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
iam, err := l.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
return
}
resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleSelfManagementGlobal}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil
resourceOwner = authReq.RequestedOrgID
}
orgIamPolicy, err := l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, iam, orgIamPolicy, nil, nil, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
if len(authReq.LinkingUsers) == 0 {
l.renderError(w, r, authReq, caos_errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData"))
return
}
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, linkingUser, idpConfig)
user, metadata, err = l.customExternalUserToLoginUserMapping(user, nil, authReq, idpConfig, metadata, resourceOwner)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, iam, orgIamPolicy, nil, nil, err)
return
}
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, memberRoles, authReq.ID, userAgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, iam, orgIamPolicy, user, externalIDP, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userGrants, err := l.customGrants(authReq.UserID, nil, authReq, idpConfig, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) *domain.ExternalUser {
displayName := tokens.IDTokenClaims.GetPreferredUsername()
if displayName == "" && tokens.IDTokenClaims.GetEmail() != "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
}
externalUser := &domain.ExternalUser{
IDPConfigID: idpConfig.IDPConfigID,
ExternalUserID: tokens.IDTokenClaims.GetSubject(),
PreferredUsername: tokens.IDTokenClaims.GetPreferredUsername(),
DisplayName: displayName,
FirstName: tokens.IDTokenClaims.GetGivenName(),
LastName: tokens.IDTokenClaims.GetFamilyName(),
NickName: tokens.IDTokenClaims.GetNickname(),
Email: tokens.IDTokenClaims.GetEmail(),
IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(),
}
if tokens.IDTokenClaims.GetPhoneNumber() != "" {
externalUser.Phone = tokens.IDTokenClaims.GetPhoneNumber()
externalUser.IsPhoneVerified = tokens.IDTokenClaims.IsPhoneNumberVerified()
}
return externalUser
}
func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *query.OrgIAMPolicy, linkingUser *domain.ExternalUser, idpConfig *iam_model.IDPConfigView) (*domain.Human, *domain.UserIDPLink, []*domain.Metadata) {
username := linkingUser.PreferredUsername
switch idpConfig.OIDCUsernameMapping {
case iam_model.OIDCMappingFieldEmail:
if linkingUser.IsEmailVerified && linkingUser.Email != "" {
username = linkingUser.Email
}
}
if username == "" {
username = linkingUser.Email
}
if orgIamPolicy.UserLoginMustBeDomain {
splittedUsername := strings.Split(username, "@")
if len(splittedUsername) > 1 {
username = splittedUsername[0]
}
}
human := &domain.Human{
Username: username,
Profile: &domain.Profile{
FirstName: linkingUser.FirstName,
LastName: linkingUser.LastName,
PreferredLanguage: linkingUser.PreferredLanguage,
NickName: linkingUser.NickName,
},
Email: &domain.Email{
EmailAddress: linkingUser.Email,
IsEmailVerified: linkingUser.IsEmailVerified,
},
}
if linkingUser.Phone != "" {
human.Phone = &domain.Phone{
PhoneNumber: linkingUser.Phone,
IsPhoneVerified: linkingUser.IsPhoneVerified,
}
}
displayName := linkingUser.PreferredUsername
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if linkingUser.IsEmailVerified && linkingUser.Email != "" {
displayName = linkingUser.Email
}
}
if displayName == "" {
displayName = linkingUser.Email
}
externalIDP := &domain.UserIDPLink{
IDPConfigID: idpConfig.IDPConfigID,
ExternalUserID: linkingUser.ExternalUserID,
DisplayName: displayName,
}
return human, externalIDP, linkingUser.Metadatas
}

View File

@@ -0,0 +1,324 @@
package login
import (
"net/http"
"strings"
"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"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/query"
)
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)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if authReq == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
idpConfig, err := l.getIDPConfigByID(r, data.IDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
if !idpConfig.IsOIDC {
l.handleJWTAuthorize(w, r, authReq, idpConfig)
return
}
l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalRegisterCallback)
}
func (l *Login) handleExternalRegisterCallback(w http.ResponseWriter, r *http.Request) {
data := new(externalIDPCallbackData)
err := l.getParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
provider, err := l.getRPConfig(idpConfig, EndpointExternalRegisterCallback)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
tokens, err := rp.CodeExchange(r.Context(), data.Code, provider)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
l.handleExternalUserRegister(w, r, authReq, idpConfig, userAgentID, tokens)
}
func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
iam, err := l.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
resourceOwner := iam.GlobalOrgID
if authReq.RequestedOrgID != "" {
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)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
if !idpConfig.AutoRegister {
l.renderExternalRegisterOverview(w, r, authReq, orgIamPolicy, user, externalIDP, nil)
return
}
l.registerExternalUser(w, r, authReq, iam, user, externalIDP)
}
func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, iam *query.IAM, user *domain.Human, externalIDP *domain.UserIDPLink) {
resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleSelfManagementGlobal}
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
memberRoles = nil
resourceOwner = authReq.RequestedOrgID
}
_, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, externalIDP, memberRoles)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) renderExternalRegisterOverview(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.OrgIAMPolicy, human *domain.Human, idp *domain.UserIDPLink, 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.Username,
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.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
}
resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleSelfManagementGlobal}
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)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) mapTokenToLoginHumanAndExternalIDP(orgIamPolicy *query.OrgIAMPolicy, tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) (*domain.Human, *domain.UserIDPLink) {
username := tokens.IDTokenClaims.GetPreferredUsername()
switch idpConfig.OIDCUsernameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
username = tokens.IDTokenClaims.GetEmail()
}
}
if username == "" {
username = tokens.IDTokenClaims.GetEmail()
}
if orgIamPolicy.UserLoginMustBeDomain {
splittedUsername := strings.Split(username, "@")
if len(splittedUsername) > 1 {
username = splittedUsername[0]
}
}
human := &domain.Human{
Username: username,
Profile: &domain.Profile{
FirstName: tokens.IDTokenClaims.GetGivenName(),
LastName: tokens.IDTokenClaims.GetFamilyName(),
PreferredLanguage: tokens.IDTokenClaims.GetLocale(),
NickName: tokens.IDTokenClaims.GetNickname(),
},
Email: &domain.Email{
EmailAddress: tokens.IDTokenClaims.GetEmail(),
IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(),
},
}
if tokens.IDTokenClaims.GetPhoneNumber() != "" {
human.Phone = &domain.Phone{
PhoneNumber: tokens.IDTokenClaims.GetPhoneNumber(),
IsPhoneVerified: tokens.IDTokenClaims.IsPhoneNumberVerified(),
}
}
displayName := tokens.IDTokenClaims.GetPreferredUsername()
switch idpConfig.OIDCIDPDisplayNameMapping {
case iam_model.OIDCMappingFieldEmail:
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
}
if displayName == "" {
displayName = tokens.IDTokenClaims.GetEmail()
}
externalIDP := &domain.UserIDPLink{
IDPConfigID: idpConfig.IDPConfigID,
ExternalUserID: tokens.IDTokenClaims.GetSubject(),
DisplayName: displayName,
}
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.UserIDPLink, error) {
return &domain.UserIDPLink{
IDPConfigID: data.ExternalIDPConfigID,
ExternalUserID: data.ExternalIDPExtUserID,
DisplayName: data.ExternalIDPDisplayName,
}, nil
}

View File

@@ -0,0 +1,18 @@
package login
import (
"net/http"
)
func (l *Login) handleHealthz(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
func (l *Login) handleReadiness(w http.ResponseWriter, r *http.Request) {
err := l.authRepo.Health(r.Context())
if err != nil {
http.Error(w, "not ready", http.StatusInternalServerError)
return
}
w.Write([]byte("OK"))
}

View File

@@ -0,0 +1,142 @@
package login
import (
"net/http"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/query"
)
const (
queryInitPWCode = "code"
queryInitPWUserID = "userID"
tmplInitPassword = "initpassword"
tmplInitPasswordDone = "initpassworddone"
)
type initPasswordFormData struct {
Code string `schema:"code"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
}
type initPasswordData struct {
baseData
profileData
Code string
UserID string
PasswordPolicyDescription string
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
}
func (l *Login) handleInitPassword(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryInitPWUserID)
code := r.FormValue(queryInitPWCode)
l.renderInitPassword(w, r, nil, userID, code, nil)
}
func (l *Login) handleInitPasswordCheck(w http.ResponseWriter, r *http.Request) {
data := new(initPasswordFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if data.Resend {
l.resendPasswordSet(w, r, authReq)
return
}
l.checkPWCode(w, r, authReq, data, nil)
}
func (l *Login) checkPWCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *initPasswordFormData, err error) {
if data.Password != data.PasswordConfirm {
err := errors.ThrowInvalidArgument(nil, "VIEW-KaGue", "Errors.User.Password.ConfirmationWrong")
l.renderInitPassword(w, r, authReq, data.UserID, data.Code, err)
return
}
userOrg := ""
if authReq != nil {
userOrg = authReq.UserOrgID
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.command.SetPasswordWithVerifyCode(setContext(r.Context(), userOrg), userOrg, data.UserID, data.Code, data.Password, userAgentID)
if err != nil {
l.renderInitPassword(w, r, authReq, data.UserID, "", err)
return
}
l.renderInitPasswordDone(w, r, authReq)
}
func (l *Login) resendPasswordSet(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
if authReq == nil {
l.renderError(w, r, nil, errors.ThrowInternal(nil, "LOGIN-8sn7s", "Errors.AuthRequest.NotFound"))
return
}
userOrg := login
if authReq != nil {
userOrg = authReq.UserOrgID
}
loginName, err := query.NewUserLoginNamesSearchQuery(authReq.LoginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
user, err := l.query.GetUser(setContext(r.Context(), userOrg), loginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), userOrg), user.ID, user.ResourceOwner, domain.NotificationTypeEmail)
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
}
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
}
data := initPasswordData{
baseData: l.getBaseData(r, authReq, "Init Password", errID, errMessage),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
}
policy, description, _ := l.getPasswordComplexityPolicyByUserID(r, authReq, userID)
if policy != nil {
data.PasswordPolicyDescription = description
data.MinLength = policy.MinLength
if policy.HasUppercase {
data.HasUppercase = UpperCaseRegex
}
if policy.HasLowercase {
data.HasLowercase = LowerCaseRegex
}
if policy.HasSymbol {
data.HasSymbol = SymbolRegex
}
if policy.HasNumber {
data.HasNumber = NumberRegex
}
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplInitPassword], data, nil)
}
func (l *Login) renderInitPasswordDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
data := l.getUserData(r, authReq, "Password Init Done", "", "")
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplInitPasswordDone], data, nil)
}

View File

@@ -0,0 +1,132 @@
package login
import (
"net/http"
"strconv"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
)
const (
queryInitUserCode = "code"
queryInitUserUserID = "userID"
queryInitUserPassword = "passwordset"
tmplInitUser = "inituser"
tmplInitUserDone = "inituserdone"
)
type initUserFormData struct {
Code string `schema:"code"`
Password string `schema:"password"`
PasswordConfirm string `schema:"passwordconfirm"`
UserID string `schema:"userID"`
PasswordSet bool `schema:"passwordSet"`
Resend bool `schema:"resend"`
}
type initUserData struct {
baseData
profileData
Code string
UserID string
PasswordSet bool
PasswordPolicyDescription string
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
}
func (l *Login) handleInitUser(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryInitUserUserID)
code := r.FormValue(queryInitUserCode)
passwordSet, _ := strconv.ParseBool(r.FormValue(queryInitUserPassword))
l.renderInitUser(w, r, nil, userID, code, passwordSet, nil)
}
func (l *Login) handleInitUserCheck(w http.ResponseWriter, r *http.Request) {
data := new(initUserFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
if data.Resend {
l.resendUserInit(w, r, authReq, data.UserID, data.PasswordSet)
return
}
l.checkUserInitCode(w, r, authReq, data, nil)
}
func (l *Login) checkUserInitCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *initUserFormData, err error) {
if data.Password != data.PasswordConfirm {
err := caos_errs.ThrowInvalidArgument(nil, "VIEW-fsdfd", "Errors.User.Password.ConfirmationWrong")
l.renderInitUser(w, r, authReq, data.UserID, data.Code, data.PasswordSet, err)
return
}
userOrgID := ""
if authReq != nil {
userOrgID = authReq.UserOrgID
}
err = l.command.HumanVerifyInitCode(setContext(r.Context(), userOrgID), data.UserID, userOrgID, data.Code, data.Password)
if err != nil {
l.renderInitUser(w, r, authReq, data.UserID, "", data.PasswordSet, err)
return
}
l.renderInitUserDone(w, r, authReq)
}
func (l *Login) resendUserInit(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID string, showPassword bool) {
userOrgID := ""
if authReq != nil {
userOrgID = authReq.UserOrgID
}
_, err := l.command.ResendInitialMail(setContext(r.Context(), userOrgID), userID, "", userOrgID)
l.renderInitUser(w, r, authReq, userID, "", showPassword, err)
}
func (l *Login) renderInitUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, 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
}
data := initUserData{
baseData: l.getBaseData(r, authReq, "Init User", errID, errMessage),
profileData: l.getProfileData(authReq),
UserID: userID,
Code: code,
PasswordSet: passwordSet,
}
policy, description, _ := l.getPasswordComplexityPolicyByUserID(r, nil, userID)
if policy != nil {
data.PasswordPolicyDescription = description
data.MinLength = policy.MinLength
if policy.HasUppercase {
data.HasUppercase = UpperCaseRegex
}
if policy.HasLowercase {
data.HasLowercase = LowerCaseRegex
}
if policy.HasSymbol {
data.HasSymbol = SymbolRegex
}
if policy.HasNumber {
data.HasNumber = NumberRegex
}
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplInitUser], data, nil)
}
func (l *Login) renderInitUserDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
data := l.getUserData(r, authReq, "User Init Done", "", "")
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplInitUserDone], data, nil)
}

View File

@@ -0,0 +1,270 @@
package login
import (
"context"
"encoding/base64"
"net/http"
"net/url"
"strings"
"time"
"github.com/caos/logging"
"github.com/caos/oidc/pkg/client/rp"
"github.com/caos/oidc/pkg/oidc"
http_util "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
iam_model "github.com/caos/zitadel/internal/iam/model"
)
type jwtRequest struct {
AuthRequestID string `schema:"authRequestID"`
UserAgentID string `schema:"userAgentID"`
}
func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) {
data := new(jwtRequest)
err := l.getParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
if data.AuthRequestID == "" || data.UserAgentID == "" {
l.renderError(w, r, nil, errors.ThrowInvalidArgument(nil, "LOGIN-adfzz", "Errors.AuthRequest.MissingParameters"))
return
}
id, err := base64.RawURLEncoding.DecodeString(data.UserAgentID)
if err != nil {
l.renderError(w, r, nil, err)
return
}
userAgentID, err := l.IDPConfigAesCrypto.DecryptString(id, l.IDPConfigAesCrypto.EncryptionKeyID())
if err != nil {
l.renderError(w, r, nil, err)
return
}
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.AuthRequestID, userAgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if idpConfig.IsOIDC {
if err != nil {
l.renderError(w, r, nil, err)
return
}
}
l.handleJWTExtraction(w, r, authReq, idpConfig)
}
func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) {
token, err := getToken(r, idpConfig.JWTHeaderName)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
tokenClaims, err := validateToken(r.Context(), token, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
tokens := &oidc.Tokens{IDToken: token, IDTokenClaims: tokenClaims}
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
externalUser, err = l.customExternalUserMapping(r.Context(), externalUser, tokens, authReq, idpConfig)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
metadata := externalUser.Metadatas
err = l.authRepo.CheckExternalUserLogin(r.Context(), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r))
if err != nil {
l.jwtExtractionUserNotFound(w, r, authReq, idpConfig, tokens, err)
return
}
if len(metadata) > 0 {
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, metadata...)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
}
redirect, err := l.redirectToJWTCallback(authReq)
if err != nil {
l.renderError(w, r, nil, err)
return
}
http.Redirect(w, r, redirect, http.StatusFound)
}
func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, tokens *oidc.Tokens, err error) {
if errors.IsNotFound(err) {
err = nil
}
if !idpConfig.AutoRegister {
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
resourceOwner := l.getOrgID(authReq)
orgIamPolicy, err := l.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
user, metadata, err = l.customExternalUserToLoginUserMapping(user, tokens, authReq, idpConfig, metadata, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderError(w, r, authReq, err)
return
}
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userGrants, err := l.customGrants(authReq.UserID, tokens, authReq, idpConfig, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
redirect, err := l.redirectToJWTCallback(authReq)
if err != nil {
l.renderError(w, r, nil, err)
return
}
http.Redirect(w, r, redirect, http.StatusFound)
}
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 {
return nil
}
for _, grant := range userGrants {
_, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner)
if err != nil {
return err
}
}
return nil
}
func (l *Login) redirectToJWTCallback(authReq *domain.AuthRequest) (string, error) {
redirect, err := url.Parse(l.baseURL + EndpointJWTCallback)
if err != nil {
return "", err
}
q := redirect.Query()
q.Set(QueryAuthRequestID, authReq.ID)
nonce, err := l.IDPConfigAesCrypto.Encrypt([]byte(authReq.AgentID))
if err != nil {
return "", err
}
q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce))
redirect.RawQuery = q.Encode()
return redirect.String(), nil
}
func (l *Login) handleJWTCallback(w http.ResponseWriter, r *http.Request) {
data := new(jwtRequest)
err := l.getParseData(r, data)
if err != nil {
l.renderError(w, r, nil, err)
return
}
id, err := base64.RawURLEncoding.DecodeString(data.UserAgentID)
if err != nil {
l.renderError(w, r, nil, err)
return
}
userAgentID, err := l.IDPConfigAesCrypto.DecryptString(id, l.IDPConfigAesCrypto.EncryptionKeyID())
if err != nil {
l.renderError(w, r, nil, err)
return
}
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.AuthRequestID, userAgentID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if idpConfig.IsOIDC {
l.renderLogin(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
func validateToken(ctx context.Context, token string, config *iam_model.IDPConfigView) (oidc.IDTokenClaims, error) {
logging.Log("LOGIN-ADf42").Debug("begin token validation")
offset := 3 * time.Second
maxAge := time.Hour
claims := oidc.EmptyIDTokenClaims()
payload, err := oidc.ParseToken(token, claims)
if err != nil {
return nil, err
}
if err = oidc.CheckIssuer(claims, config.JWTIssuer); err != nil {
return nil, err
}
logging.Log("LOGIN-Dfg22").Debug("begin signature validation")
keySet := rp.NewRemoteKeySet(http.DefaultClient, config.JWTKeysEndpoint)
if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil {
return nil, err
}
if !claims.GetExpiration().IsZero() {
if err = oidc.CheckExpiration(claims, offset); err != nil {
return nil, err
}
}
if !claims.GetIssuedAt().IsZero() {
if err = oidc.CheckIssuedAt(claims, maxAge, offset); err != nil {
return nil, err
}
}
return claims, nil
}
func getToken(r *http.Request, headerName string) (string, error) {
if headerName == "" {
headerName = http_util.Authorization
}
auth := r.Header.Get(headerName)
if auth == "" {
return "", errors.ThrowInvalidArgument(nil, "LOGIN-adh42", "Errors.AuthRequest.TokenNotFound")
}
return strings.TrimPrefix(auth, oidc.PrefixBearer), nil
}

View File

@@ -0,0 +1,24 @@
package login
import (
"net/http"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplLinkUsersDone = "linkusersdone"
)
func (l *Login) linkUsers(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.LinkExternalUsers(setContext(r.Context(), authReq.UserOrgID), authReq.ID, userAgentID, domain.BrowserInfoFromRequest(r))
l.renderLinkUsersDone(w, r, authReq, err)
}
func (l *Login) renderLinkUsersDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errType, errMessage string
data := l.getUserData(r, authReq, "Linking Users Done", errType, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplLinkUsersDone], data, nil)
}

View File

@@ -0,0 +1,152 @@
package login
import (
"context"
"fmt"
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"github.com/rakyll/statik/fs"
"github.com/caos/zitadel/internal/api/authz"
http_utils "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/api/http/middleware"
_ "github.com/caos/zitadel/internal/api/ui/login/statik"
auth_repository "github.com/caos/zitadel/internal/auth/repository"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
"github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/form"
"github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/static"
)
type Login struct {
endpoint string
router http.Handler
renderer *Renderer
parser *form.Parser
command *command.Commands
query *query.Queries
staticStorage static.Storage
//staticCache cache.Cache //TODO: enable when storage is implemented again
authRepo auth_repository.Repository
baseURL string
zitadelURL string
oidcAuthCallbackURL string
IDPConfigAesCrypto crypto.EncryptionAlgorithm
iamDomain string
}
type Config struct {
LanguageCookieName string
CSRF CSRF
Cache middleware.CacheConfig
//StaticCache cache_config.CacheConfig //TODO: enable when storage is implemented again
}
type CSRF struct {
CookieName string
Key *crypto.KeyConfig
}
const (
login = "LOGIN"
HandlerPrefix = "/ui/login"
DefaultLoggedOutPath = HandlerPrefix + EndpointLogoutDone
)
func CreateLogin(config Config, command *command.Commands, query *query.Queries, authRepo *eventsourcing.EsRepository, staticStorage static.Storage, systemDefaults systemdefaults.SystemDefaults, zitadelURL, domain, oidcAuthCallbackURL string, externalSecure bool, userAgentCookie mux.MiddlewareFunc) (*Login, error) {
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.IDPConfigVerificationKey)
if err != nil {
return nil, fmt.Errorf("error create new aes crypto: %w", err)
}
login := &Login{
oidcAuthCallbackURL: oidcAuthCallbackURL,
baseURL: HandlerPrefix,
zitadelURL: zitadelURL,
command: command,
query: query,
staticStorage: staticStorage,
authRepo: authRepo,
IDPConfigAesCrypto: aesCrypto,
iamDomain: domain,
}
//TODO: enable when storage is implemented again
//login.staticCache, err = config.StaticCache.Config.NewCache()
//if err != nil {
// return nil, fmt.Errorf("unable to create storage cache: %w", err)
//}
statikFS, err := fs.NewWithNamespace("login")
if err != nil {
return nil, fmt.Errorf("unable to create filesystem: %w", err)
}
csrfInterceptor, err := createCSRFInterceptor(config.CSRF, externalSecure, login.csrfErrorHandler())
if err != nil {
return nil, fmt.Errorf("unable to create csrfInterceptor: %w", err)
}
cacheInterceptor, err := middleware.DefaultCacheInterceptor(EndpointResources, config.Cache.MaxAge, config.Cache.SharedMaxAge)
if err != nil {
return nil, fmt.Errorf("unable to create cacheInterceptor: %w", err)
}
security := middleware.SecurityHeaders(csp(), login.cspErrorHandler)
login.router = CreateRouter(login, statikFS, csrfInterceptor, cacheInterceptor, security, userAgentCookie, middleware.TelemetryHandler(EndpointResources))
login.renderer = CreateRenderer(HandlerPrefix, statikFS, staticStorage, config.LanguageCookieName, systemDefaults.DefaultLanguage)
login.parser = form.NewParser()
return login, nil
}
func csp() *middleware.CSP {
csp := middleware.DefaultSCP
csp.ObjectSrc = middleware.CSPSourceOptsSelf()
csp.StyleSrc = csp.StyleSrc.AddNonce()
csp.ScriptSrc = csp.ScriptSrc.AddNonce()
return &csp
}
func createCSRFInterceptor(config CSRF, externalSecure bool, errorHandler http.Handler) (func(http.Handler) http.Handler, error) {
csrfKey, err := crypto.LoadKey(config.Key, config.Key.EncryptionKeyID)
if err != nil {
return nil, err
}
path := "/"
return csrf.Protect([]byte(csrfKey),
csrf.Secure(externalSecure),
csrf.CookieName(http_utils.SetCookiePrefix(config.CookieName, "", path, externalSecure)),
csrf.Path(path),
csrf.ErrorHandler(errorHandler),
), nil
}
func (l *Login) Handler() http.Handler {
return l.router
}
func (l *Login) getClaimedUserIDsOfOrgDomain(ctx context.Context, orgName string) ([]string, error) {
loginName, err := query.NewUserPreferredLoginNameSearchQuery("@"+domain.NewIAMDomainName(orgName, l.iamDomain), query.TextEndsWithIgnoreCase)
if err != nil {
return nil, err
}
users, err := l.query.SearchUsers(ctx, &query.UserSearchQueries{Queries: []query.SearchQuery{loginName}})
if err != nil {
return nil, err
}
userIDs := make([]string, len(users.Users))
for i, user := range users.Users {
userIDs[i] = user.ID
}
return userIDs, nil
}
func setContext(ctx context.Context, resourceOwner string) context.Context {
data := authz.CtxData{
UserID: login,
OrgID: resourceOwner,
}
return authz.SetCtxData(ctx, data)
}

View File

@@ -0,0 +1,86 @@
package login
import (
"net/http"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
)
const (
tmplLogin = "login"
)
type loginData struct {
LoginName string `schema:"loginName"`
Register bool `schema:"register"`
}
func (l *Login) handleLogin(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if authReq == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) handleLoginName(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderLogin(w, r, authReq, nil)
}
func (l *Login) handleLoginNameCheck(w http.ResponseWriter, r *http.Request) {
data := new(loginData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
if data.Register {
if authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0 {
l.handleRegisterOption(w, r)
return
}
l.handleRegister(w, r)
return
}
if authReq == nil {
l.renderLogin(w, r, nil, errors.ThrowInvalidArgument(nil, "LOGIN-adrg3", "Errors.AuthRequest.NotFound"))
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
loginName := data.LoginName
err = l.authRepo.CheckLoginName(r.Context(), authReq.ID, loginName, userAgentID)
if err != nil {
l.renderLogin(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
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)
}
data := l.getUserData(r, authReq, "Login", errID, errMessage)
funcs := map[string]interface{}{
"hasUsernamePasswordLogin": func() bool {
return authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowUsernamePassword
},
"hasExternalLogin": func() bool {
return authReq.LoginPolicy != nil && authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0
},
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplLogin], data, funcs)
}

View File

@@ -0,0 +1,54 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplLoginSuccess = "login_success"
)
type loginSuccessData struct {
userData
RedirectURI string `schema:"redirect-uri"`
}
func (l *Login) redirectToLoginSuccess(w http.ResponseWriter, r *http.Request, id string) {
http.Redirect(w, r, l.renderer.pathPrefix+EndpointLoginSuccess+"?authRequestID="+id, http.StatusFound)
}
func (l *Login) handleLoginSuccess(w http.ResponseWriter, r *http.Request) {
authRequest, _ := l.getAuthRequest(r)
if authRequest == nil {
l.renderSuccessAndCallback(w, r, nil, nil)
return
}
for _, step := range authRequest.PossibleSteps {
if step.Type() != domain.NextStepLoginSucceeded && step.Type() != domain.NextStepRedirectToCallback {
l.renderNextStep(w, r, authRequest)
return
}
}
l.renderSuccessAndCallback(w, r, authRequest, nil)
}
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)
}
data := loginSuccessData{
userData: l.getUserData(r, authReq, "Login Successful", errID, errMessage),
}
if authReq != nil {
data.RedirectURI = l.oidcAuthCallbackURL
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplLoginSuccess], data, nil)
}
func (l *Login) redirectToCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
callback := l.oidcAuthCallbackURL + authReq.ID
http.Redirect(w, r, callback, http.StatusFound)
}

View File

@@ -0,0 +1,18 @@
package login
import (
"net/http"
)
const (
tmplLogoutDone = "logoutdone"
)
func (l *Login) handleLogoutDone(w http.ResponseWriter, r *http.Request) {
l.renderLogoutDone(w, r)
}
func (l *Login) renderLogoutDone(w http.ResponseWriter, r *http.Request) {
data := l.getUserData(r, nil, "Logout Done", "", "")
l.renderer.RenderTemplate(w, r, l.getTranslator(nil), l.renderer.Templates[tmplLogoutDone], data, nil)
}

View File

@@ -0,0 +1,96 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
)
const (
queryCode = "code"
queryUserID = "userID"
tmplMailVerification = "mail_verification"
tmplMailVerified = "mail_verified"
)
type mailVerificationFormData struct {
Code string `schema:"code"`
UserID string `schema:"userID"`
Resend bool `schema:"resend"`
}
type mailVerificationData struct {
baseData
profileData
UserID string
}
func (l *Login) handleMailVerification(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue(queryUserID)
code := r.FormValue(queryCode)
if code != "" {
l.checkMailCode(w, r, nil, userID, code)
return
}
l.renderMailVerification(w, r, nil, userID, nil)
}
func (l *Login) handleMailVerificationCheck(w http.ResponseWriter, r *http.Request) {
data := new(mailVerificationFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if !data.Resend {
l.checkMailCode(w, r, authReq, data.UserID, data.Code)
return
}
userOrg := ""
if authReq != nil {
userOrg = authReq.UserOrgID
}
_, err = l.command.CreateHumanEmailVerificationCode(setContext(r.Context(), userOrg), data.UserID, userOrg)
l.renderMailVerification(w, r, authReq, data.UserID, err)
}
func (l *Login) checkMailCode(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID, code string) {
userOrg := ""
if authReq != nil {
userID = authReq.UserID
userOrg = authReq.UserOrgID
}
_, err := l.command.VerifyHumanEmail(setContext(r.Context(), userOrg), userID, code, userOrg)
if err != nil {
l.renderMailVerification(w, r, authReq, userID, err)
return
}
l.renderMailVerified(w, r, authReq)
}
func (l *Login) renderMailVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userID string, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if userID == "" {
userID = authReq.UserID
}
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "Mail Verification", errID, errMessage),
UserID: userID,
profileData: l.getProfileData(authReq),
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMailVerification], data, nil)
}
func (l *Login) renderMailVerified(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
data := mailVerificationData{
baseData: l.getBaseData(r, authReq, "Mail Verified", "", ""),
profileData: l.getProfileData(authReq),
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMailVerified], data, nil)
}

View File

@@ -0,0 +1,22 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplMFAInitDone = "mfainitdone"
)
type mfaInitDoneData struct {
}
func (l *Login) renderMFAInitDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaDoneData) {
var errType, errMessage string
data.baseData = l.getBaseData(r, authReq, "MFA Init Done", errType, errMessage)
data.profileData = l.getProfileData(authReq)
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitDone], data, nil)
}

View File

@@ -0,0 +1,64 @@
package login
import (
"encoding/base64"
"net/http"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplMFAU2FInit = "mfainitu2f"
)
type u2fInitData struct {
webAuthNData
MFAType domain.MFAType
}
func (l *Login) renderRegisterU2F(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage, credentialData string
var u2f *domain.WebAuthNToken
if err == nil {
u2f, err = l.command.HumanAddU2FSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, true)
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
if u2f != nil {
credentialData = base64.RawURLEncoding.EncodeToString(u2f.CredentialCreationData)
}
data := &u2fInitData{
webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, "Register WebAuthNToken", errID, errMessage),
CredentialCreationData: credentialData,
},
MFAType: domain.MFATypeU2F,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplMFAU2FInit], data, nil)
}
func (l *Login) handleRegisterU2F(w http.ResponseWriter, r *http.Request) {
data := new(webAuthNFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
credData, err := base64.URLEncoding.DecodeString(data.CredentialData)
if err != nil {
l.renderRegisterU2F(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
if _, err = l.command.HumanVerifyU2FSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, data.Name, userAgentID, credData); err != nil {
l.renderRegisterU2F(w, r, authReq, err)
return
}
done := &mfaDoneData{
MFAType: domain.MFATypeU2F,
}
l.renderMFAInitDone(w, r, authReq, done)
}

View File

@@ -0,0 +1,100 @@
package login
import (
"bytes"
"net/http"
"github.com/caos/zitadel/internal/domain"
svg "github.com/ajstarks/svgo"
"github.com/boombuler/barcode/qr"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/qrcode"
)
const (
tmplMFAInitVerify = "mfainitverify"
)
type mfaInitVerifyData struct {
MFAType domain.MFAType `schema:"mfaType"`
Code string `schema:"code"`
URL string `schema:"url"`
Secret string `schema:"secret"`
}
func (l *Login) handleMFAInitVerify(w http.ResponseWriter, r *http.Request) {
data := new(mfaInitVerifyData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
var verifyData *mfaVerifyData
switch data.MFAType {
case domain.MFATypeOTP:
verifyData = l.handleOTPVerify(w, r, authReq, data)
}
if verifyData != nil {
l.renderMFAInitVerify(w, r, authReq, verifyData, err)
return
}
done := &mfaDoneData{
MFAType: data.MFAType,
}
l.renderMFAInitDone(w, r, authReq, done)
}
func (l *Login) handleOTPVerify(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaInitVerifyData) *mfaVerifyData {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
_, err := l.command.HumanCheckMFAOTPSetup(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, data.Code, userAgentID, authReq.UserOrgID)
if err == nil {
return nil
}
mfadata := &mfaVerifyData{
MFAType: data.MFAType,
otpData: otpData{
Secret: data.Secret,
Url: data.URL,
},
}
return mfadata
}
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)
}
data.baseData = l.getBaseData(r, authReq, "MFA Init Verify", errID, errMessage)
data.profileData = l.getProfileData(authReq)
if data.MFAType == domain.MFATypeOTP {
code, err := generateQrCode(data.otpData.Url)
if err == nil {
data.otpData.QrCode = code
}
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAInitVerify], data, nil)
}
func generateQrCode(url string) (string, error) {
var b bytes.Buffer
s := svg.New(&b)
qrCode, err := qr.Encode(url, qr.M, qr.Auto)
if err != nil {
return "", err
}
qs := qrcode.NewQrSVG(qrCode, 5)
qs.StartQrSVG(s)
qs.WriteQrSVG(s)
s.End()
return string(b.Bytes()), nil
}

View File

@@ -0,0 +1,105 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
)
const (
tmplMFAPrompt = "mfaprompt"
)
type mfaPromptData struct {
MFAProvider domain.MFAType `schema:"provider"`
Skip bool `schema:"skip"`
}
func (l *Login) handleMFAPrompt(w http.ResponseWriter, r *http.Request) {
data := new(mfaPromptData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if !data.Skip {
mfaVerifyData := new(mfaVerifyData)
mfaVerifyData.MFAType = data.MFAProvider
l.handleMFACreation(w, r, authReq, mfaVerifyData)
return
}
err = l.command.HumanSkipMFAInit(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.handleLogin(w, r)
}
func (l *Login) handleMFAPromptSelection(w http.ResponseWriter, r *http.Request) {
data := new(mfaPromptData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}
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)
}
data := mfaData{
baseData: l.getBaseData(r, authReq, "MFA Prompt", errID, errMessage),
profileData: l.getProfileData(authReq),
}
if mfaPromptData == nil {
l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-XU0tj", "Errors.User.MFA.NoProviders"))
return
}
data.MFAProviders = mfaPromptData.MFAProviders
data.MFARequired = mfaPromptData.Required
if len(mfaPromptData.MFAProviders) == 1 && mfaPromptData.Required {
data := &mfaVerifyData{
MFAType: mfaPromptData.MFAProviders[0],
}
l.handleMFACreation(w, r, authReq, data)
return
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplMFAPrompt], data, nil)
}
func (l *Login) handleMFACreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
switch data.MFAType {
case domain.MFATypeOTP:
l.handleOTPCreation(w, r, authReq, data)
return
case domain.MFATypeU2F:
l.renderRegisterU2F(w, r, authReq, nil)
return
}
l.renderError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-Or3HO", "Errors.User.MFA.NoProviders"))
}
func (l *Login) handleOTPCreation(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, data *mfaVerifyData) {
otp, err := l.command.AddHumanOTP(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
data.otpData = otpData{
Secret: otp.SecretString,
Url: otp.Url,
}
l.renderMFAInitVerify(w, r, authReq, data, nil)
}

View File

@@ -0,0 +1,88 @@
package login
import (
"net/http"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplMFAVerify = "mfaverify"
)
type mfaVerifyFormData struct {
MFAType domain.MFAType `schema:"mfaType"`
Code string `schema:"code"`
SelectedProvider domain.MFAType `schema:"provider"`
}
func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
data := new(mfaVerifyFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
step, ok := authReq.PossibleSteps[0].(*domain.MFAVerificationStep)
if !ok {
l.renderError(w, r, authReq, err)
return
}
if data.Code == "" {
l.renderMFAVerifySelected(w, r, authReq, step, data.SelectedProvider, nil)
return
}
if data.MFAType == domain.MFATypeOTP {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Code, userAgentID, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderMFAVerifySelected(w, r, authReq, step, domain.MFATypeOTP, err)
return
}
}
l.renderNextStep(w, r, authReq)
}
func (l *Login) renderMFAVerify(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, verificationStep *domain.MFAVerificationStep, err error) {
if verificationStep == nil {
l.renderError(w, r, authReq, err)
return
}
provider := verificationStep.MFAProviders[len(verificationStep.MFAProviders)-1]
l.renderMFAVerifySelected(w, r, authReq, verificationStep, provider, err)
}
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)
}
data := l.getUserData(r, authReq, "MFA Verify", errID, errMessage)
if verificationStep == nil {
l.renderError(w, r, authReq, err)
return
}
switch selectedProvider {
case domain.MFATypeU2F:
l.renderU2FVerification(w, r, authReq, removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeU2F), nil)
return
case domain.MFATypeOTP:
data.MFAProviders = removeSelectedProviderFromList(verificationStep.MFAProviders, domain.MFATypeOTP)
data.SelectedMFAProvider = domain.MFATypeOTP
default:
l.renderError(w, r, authReq, err)
return
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplMFAVerify], data, nil)
}
func removeSelectedProviderFromList(providers []domain.MFAType, selected domain.MFAType) []domain.MFAType {
for i := len(providers) - 1; i >= 0; i-- {
if providers[i] == selected {
copy(providers[i:], providers[i+1:])
return providers[:len(providers)-1]
}
}
return providers
}

View File

@@ -0,0 +1,79 @@
package login
import (
"encoding/base64"
"net/http"
"github.com/caos/zitadel/internal/domain"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
)
const (
tmplU2FVerification = "u2fverification"
)
type mfaU2FData struct {
webAuthNData
MFAProviders []domain.MFAType
SelectedProvider domain.MFAType
}
type mfaU2FFormData struct {
webAuthNFormData
SelectedProvider domain.MFAType `schema:"provider"`
}
func (l *Login) renderU2FVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, providers []domain.MFAType, err error) {
var errID, errMessage, 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)
}
data := &mfaU2FData{
webAuthNData: webAuthNData{
userData: l.getUserData(r, authReq, "Login WebAuthNToken", errID, errMessage),
CredentialCreationData: credentialData,
},
MFAProviders: providers,
SelectedProvider: -1,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplU2FVerification], data, nil)
}
func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {
formData := new(mfaU2FFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
step, ok := authReq.PossibleSteps[0].(*domain.MFAVerificationStep)
if !ok {
l.renderError(w, r, authReq, err)
return
}
if formData.CredentialData == "" {
l.renderMFAVerifySelected(w, r, authReq, step, formData.SelectedProvider, nil)
return
}
credData, err := base64.URLEncoding.DecodeString(formData.CredentialData)
if err != nil {
l.renderU2FVerification(w, r, authReq, step.MFAProviders, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyMFAU2F(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, userAgentID, credData, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderU2FVerification(w, r, authReq, step.MFAProviders, err)
return
}
l.renderNextStep(w, r, authReq)
}

View File

@@ -0,0 +1,74 @@
package login
import (
"net/http"
"regexp"
"strconv"
"github.com/caos/zitadel/internal/domain"
iam_model "github.com/caos/zitadel/internal/iam/model"
)
const (
LowerCaseRegex = `[a-z]`
UpperCaseRegex = `[A-Z]`
NumberRegex = `[0-9]`
SymbolRegex = `[^A-Za-z0-9]`
)
var (
hasStringLowerCase = regexp.MustCompile(LowerCaseRegex).MatchString
hasStringUpperCase = regexp.MustCompile(UpperCaseRegex).MatchString
hasNumber = regexp.MustCompile(NumberRegex).MatchString
hasSymbol = regexp.MustCompile(SymbolRegex).MatchString
)
func (l *Login) getPasswordComplexityPolicy(r *http.Request, authReq *domain.AuthRequest, orgID string) (*iam_model.PasswordComplexityPolicyView, string, error) {
policy, err := l.authRepo.GetMyPasswordComplexityPolicy(setContext(r.Context(), orgID))
if err != nil {
return nil, err.Error(), err
}
description, err := l.generatePolicyDescription(r, authReq, policy)
return policy, description, nil
}
func (l *Login) getPasswordComplexityPolicyByUserID(r *http.Request, authReq *domain.AuthRequest, userID string) (*iam_model.PasswordComplexityPolicyView, string, error) {
user, err := l.query.GetUserByID(r.Context(), userID)
if err != nil {
return nil, "", nil
}
policy, err := l.authRepo.GetMyPasswordComplexityPolicy(setContext(r.Context(), user.ResourceOwner))
if err != nil {
return nil, err.Error(), err
}
description, err := l.generatePolicyDescription(r, authReq, policy)
return policy, description, nil
}
func (l *Login) generatePolicyDescription(r *http.Request, authReq *domain.AuthRequest, policy *iam_model.PasswordComplexityPolicyView) (string, error) {
description := "<ul class=\"lgn-no-dots lgn-policy\" id=\"passwordcomplexity\">"
translator := l.getTranslator(authReq)
minLength := l.renderer.LocalizeFromRequest(translator, r, "Password.MinLength", nil)
description += "<li id=\"minlength\" class=\"invalid\"><i class=\"lgn-icon-times-solid lgn-warn\"></i><span>" + minLength + " " + strconv.Itoa(int(policy.MinLength)) + "</span></li>"
if policy.HasUppercase {
uppercase := l.renderer.LocalizeFromRequest(translator, r, "Password.HasUppercase", nil)
description += "<li id=\"uppercase\" class=\"invalid\"><i class=\"lgn-icon-times-solid lgn-warn\"></i><span>" + uppercase + "</span></li>"
}
if policy.HasLowercase {
lowercase := l.renderer.LocalizeFromRequest(translator, r, "Password.HasLowercase", nil)
description += "<li id=\"lowercase\" class=\"invalid\"><i class=\"lgn-icon-times-solid lgn-warn\"></i><span>" + lowercase + "</span></li>"
}
if policy.HasNumber {
hasnumber := l.renderer.LocalizeFromRequest(translator, r, "Password.HasNumber", nil)
description += "<li id=\"number\" class=\"invalid\"><i class=\"lgn-icon-times-solid lgn-warn\"></i><span>" + hasnumber + "</span></li>"
}
if policy.HasSymbol {
hassymbol := l.renderer.LocalizeFromRequest(translator, r, "Password.HasSymbol", nil)
description += "<li id=\"symbol\" class=\"invalid\"><i class=\"lgn-icon-times-solid lgn-warn\"></i><span>" + hassymbol + "</span></li>"
}
confirmation := l.renderer.LocalizeFromRequest(translator, r, "Password.Confirmation", nil)
description += "<li id=\"confirmation\" class=\"invalid\"><i class=\"lgn-icon-times-solid lgn-warn\"></i><span>" + confirmation + "</span></li>"
description += "</ul>"
return description, nil
}

View File

@@ -0,0 +1,50 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
)
const (
tmplPassword = "password"
)
type passwordFormData struct {
Password string `schema:"password"`
}
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)
}
data := l.getUserData(r, authReq, "Password", errID, errMessage)
funcs := map[string]interface{}{
"showPasswordReset": func() bool {
if authReq.LoginPolicy != nil {
return !authReq.LoginPolicy.HidePasswordReset
}
return true
},
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplPassword], data, funcs)
}
func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
data := new(passwordFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Password, userAgentID, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderPassword(w, r, authReq, err)
return
}
l.renderNextStep(w, r, authReq)
}

View File

@@ -0,0 +1,41 @@
package login
import (
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/query"
"net/http"
)
const (
tmplPasswordResetDone = "passwordresetdone"
)
func (l *Login) handlePasswordReset(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
loginName, err := query.NewUserLoginNamesSearchQuery(authReq.LoginName)
if err != nil {
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
return
}
user, err := l.query.GetUser(setContext(r.Context(), authReq.UserOrgID), loginName)
if err != nil {
l.renderPasswordResetDone(w, r, authReq, err)
return
}
_, err = l.command.RequestSetPassword(setContext(r.Context(), authReq.UserOrgID), user.ID, authReq.UserOrgID, domain.NotificationTypeEmail)
l.renderPasswordResetDone(w, r, authReq, err)
}
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)
}
data := l.getUserData(r, authReq, "Password Reset Done", errID, errMessage)
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplPasswordResetDone], data, nil)
}

View File

@@ -0,0 +1,75 @@
package login
import (
"encoding/base64"
"net/http"
"github.com/caos/zitadel/internal/domain"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
)
const (
tmplPasswordlessVerification = "passwordlessverification"
)
type passwordlessData struct {
webAuthNData
PasswordLogin bool
}
type passwordlessFormData struct {
webAuthNFormData
PasswordLogin bool `schema:"passwordlogin"`
}
func (l *Login) renderPasswordlessVerification(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, passwordSet bool, err error) {
var errID, errMessage, credentialData string
var webAuthNLogin *domain.WebAuthNLogin
if err == nil {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
webAuthNLogin, err = l.authRepo.BeginPasswordlessLogin(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)
}
if passwordSet && authReq.LoginPolicy != nil {
passwordSet = authReq.LoginPolicy.AllowUsernamePassword
}
data := &passwordlessData{
webAuthNData{
userData: l.getUserData(r, authReq, "Login Passwordless", errID, errMessage),
CredentialCreationData: credentialData,
},
passwordSet,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplPasswordlessVerification], data, nil)
}
func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Request) {
formData := new(passwordlessFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if formData.PasswordLogin {
l.renderPassword(w, r, authReq, nil)
return
}
credData, err := base64.URLEncoding.DecodeString(formData.CredentialData)
if err != nil {
l.renderPasswordlessVerification(w, r, authReq, formData.PasswordLogin, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyPasswordless(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, userAgentID, credData, domain.BrowserInfoFromRequest(r))
if err != nil {
l.renderPasswordlessVerification(w, r, authReq, formData.PasswordLogin, err)
return
}
l.renderNextStep(w, r, authReq)
}

View File

@@ -0,0 +1,40 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplPasswordlessPrompt = "passwordlessprompt"
)
type passwordlessPromptData struct {
userData
}
type passwordlessPromptFormData struct{}
func (l *Login) handlePasswordlessPrompt(w http.ResponseWriter, r *http.Request) {
data := new(passwordlessPromptFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.renderPasswordlessRegistration(w, r, authReq, "", "", "", "", 0, nil)
}
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)
}
data := &passwordlessPromptData{
userData: l.getUserData(r, authReq, "Passwordless Prompt", errID, errMessage),
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessPrompt], data, nil)
}

View File

@@ -0,0 +1,199 @@
package login
import (
"encoding/base64"
"net/http"
"github.com/caos/logging"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/query"
)
const (
tmplPasswordlessRegistration = "passwordlessregistration"
tmplPasswordlessRegistrationDone = "passwordlessregistrationdone"
)
type passwordlessRegistrationData struct {
webAuthNData
Code string
CodeID string
UserID string
OrgID string
RequestPlatformType authPlatform
Disabled bool
}
type passwordlessRegistrationDoneDate struct {
userData
HideNextButton bool
}
type passwordlessRegistrationFormData struct {
webAuthNFormData
passwordlessRegistrationQueries
TokenName string `schema:"name"`
}
type passwordlessRegistrationQueries struct {
Code string `schema:"code"`
CodeID string `schema:"codeID"`
UserID string `schema:"userID"`
OrgID string `schema:"orgID"`
RequestPlatformType authPlatform `schema:"requestPlatformType"`
}
type authPlatform domain.AuthenticatorAttachment
func (a authPlatform) MarshalText() (text []byte, err error) {
switch domain.AuthenticatorAttachment(a) {
case domain.AuthenticatorAttachmentPlattform:
return []byte("platform"), nil
case domain.AuthenticatorAttachmentCrossPlattform:
return []byte("crossPlatform"), nil
default:
return []byte("unspecified"), nil
}
}
func (a *authPlatform) UnmarshalText(text []byte) (err error) {
switch string(text) {
case "platform",
"1":
*a = authPlatform(domain.AuthenticatorAttachmentPlattform)
case "crossPlatform",
"2":
*a = authPlatform(domain.AuthenticatorAttachmentCrossPlattform)
}
return nil
}
func (l *Login) handlePasswordlessRegistration(w http.ResponseWriter, r *http.Request) {
queries := new(passwordlessRegistrationQueries)
err := l.parser.Parse(r, queries)
l.renderPasswordlessRegistration(w, r, nil, queries.UserID, queries.OrgID, queries.CodeID, queries.Code, queries.RequestPlatformType, err)
}
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 disabled bool
if authReq != nil {
userID = authReq.UserID
orgID = authReq.UserOrgID
}
var webAuthNToken *domain.WebAuthNToken
if err == nil {
if authReq != nil {
webAuthNToken, err = l.authRepo.BeginPasswordlessSetup(setContext(r.Context(), authReq.UserOrgID), userID, authReq.UserOrgID, domain.AuthenticatorAttachment(requestedPlatformType))
} else {
webAuthNToken, err = l.authRepo.BeginPasswordlessInitCodeSetup(setContext(r.Context(), orgID), userID, orgID, codeID, code, domain.AuthenticatorAttachment(requestedPlatformType))
}
}
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
disabled = true
}
if webAuthNToken != nil {
credentialData = base64.RawURLEncoding.EncodeToString(webAuthNToken.CredentialCreationData)
}
data := &passwordlessRegistrationData{
webAuthNData{
userData: l.getUserData(r, authReq, "Login Passwordless", errID, errMessage),
CredentialCreationData: credentialData,
},
code,
codeID,
userID,
orgID,
requestedPlatformType,
disabled,
}
translator := l.getTranslator(authReq)
if authReq == nil {
policy, err := l.query.ActiveLabelPolicyByOrg(r.Context(), orgID)
logging.Log("HANDL-XjWKE").OnError(err).Error("unable to get active label policy")
data.LabelPolicy = labelPolicyToDomain(policy)
translator, err = l.renderer.NewTranslator()
if err == nil {
texts, err := l.authRepo.GetLoginText(r.Context(), orgID)
logging.Log("LOGIN-HJK4t").OnError(err).Warn("could not get custom texts")
l.addLoginTranslations(translator, texts)
}
}
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplPasswordlessRegistration], data, nil)
}
func labelPolicyToDomain(p *query.LabelPolicy) *domain.LabelPolicy {
return &domain.LabelPolicy{
ObjectRoot: models.ObjectRoot{
AggregateID: p.ID,
Sequence: p.Sequence,
ResourceOwner: p.ResourceOwner,
CreationDate: p.CreationDate,
ChangeDate: p.ChangeDate,
},
State: p.State,
Default: p.IsDefault,
PrimaryColor: p.Light.PrimaryColor,
BackgroundColor: p.Light.BackgroundColor,
WarnColor: p.Light.WarnColor,
FontColor: p.Light.FontColor,
LogoURL: p.Light.LogoURL,
IconURL: p.Light.IconURL,
PrimaryColorDark: p.Dark.PrimaryColor,
BackgroundColorDark: p.Dark.BackgroundColor,
WarnColorDark: p.Dark.WarnColor,
FontColorDark: p.Dark.FontColor,
LogoDarkURL: p.Dark.LogoURL,
IconDarkURL: p.Dark.IconURL,
Font: p.FontURL,
HideLoginNameSuffix: p.HideLoginNameSuffix,
ErrorMsgPopup: p.ShouldErrorPopup,
DisableWatermark: p.WatermarkDisabled,
}
}
func (l *Login) handlePasswordlessRegistrationCheck(w http.ResponseWriter, r *http.Request) {
formData := new(passwordlessRegistrationFormData)
authReq, err := l.getAuthRequestAndParseData(r, formData)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
l.checkPasswordlessRegistration(w, r, authReq, formData)
}
func (l *Login) checkPasswordlessRegistration(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, formData *passwordlessRegistrationFormData) {
credData, err := base64.URLEncoding.DecodeString(formData.CredentialData)
if err != nil {
l.renderPasswordlessRegistration(w, r, authReq, formData.UserID, formData.OrgID, formData.CodeID, formData.Code, formData.RequestPlatformType, err)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
if authReq != nil {
err = l.authRepo.VerifyPasswordlessSetup(setContext(r.Context(), authReq.UserOrgID), formData.UserID, authReq.UserOrgID, userAgentID, formData.TokenName, credData)
} else {
err = l.authRepo.VerifyPasswordlessInitCodeSetup(setContext(r.Context(), formData.OrgID), formData.UserID, formData.OrgID, userAgentID, formData.TokenName, formData.CodeID, formData.Code, credData)
}
if err != nil {
l.renderPasswordlessRegistration(w, r, authReq, formData.UserID, formData.OrgID, formData.CodeID, formData.Code, formData.RequestPlatformType, err)
return
}
l.renderPasswordlessRegistrationDone(w, r, authReq, nil)
}
func (l *Login) renderPasswordlessRegistrationDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
data := passwordlessRegistrationDoneDate{
userData: l.getUserData(r, authReq, "Passwordless Registration Done", errID, errMessage),
HideNextButton: authReq == nil,
}
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplPasswordlessRegistrationDone], data, nil)
}

View File

@@ -0,0 +1,23 @@
package login
import (
"net/http"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/query"
)
func (l *Login) getDefaultOrgIamPolicy(r *http.Request) (*query.OrgIAMPolicy, error) {
return l.query.DefaultOrgIAMPolicy(r.Context())
}
func (l *Login) getOrgIamPolicy(r *http.Request, orgID string) (*query.OrgIAMPolicy, error) {
if orgID == "" {
return l.query.DefaultOrgIAMPolicy(r.Context())
}
return l.query.OrgIAMPolicyByOrg(r.Context(), orgID)
}
func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) {
return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID)
}

View File

@@ -0,0 +1,185 @@
package login
import (
"net/http"
"golang.org/x/text/language"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
)
const (
tmplRegister = "register"
)
type registerFormData struct {
Email string `schema:"email"`
Username string `schema:"username"`
Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"`
Language string `schema:"language"`
Gender int32 `schema:"gender"`
Password string `schema:"register-password"`
Password2 string `schema:"register-password-confirmation"`
TermsConfirm bool `schema:"terms-confirm"`
}
type registerData struct {
baseData
registerFormData
PasswordPolicyDescription string
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
ShowUsername bool
OrgRegister bool
}
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
}
l.renderRegister(w, r, authRequest, data, nil)
}
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
}
if data.Password != data.Password2 {
err := caos_errs.ThrowInvalidArgument(nil, "VIEW-KaGue", "Errors.User.Password.ConfirmationWrong")
l.renderRegister(w, r, authRequest, data, err)
return
}
iam, err := l.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
resourceOwner := iam.GlobalOrgID
memberRoles := []string{domain.RoleSelfManagementGlobal}
if authRequest != nil && authRequest.RequestedOrgID != "" && authRequest.RequestedOrgID != iam.GlobalOrgID {
memberRoles = nil
resourceOwner = authRequest.RequestedOrgID
}
user, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, data.toHumanDomain(), nil, memberRoles)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
if authRequest == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectUser(r.Context(), authRequest.ID, user.AggregateID, userAgentID)
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) {
var errID, errMessage string
if err != nil {
errID, errMessage = l.getErrorMessage(r, err)
}
translator := l.getTranslator(authRequest)
if formData == nil {
formData = new(registerFormData)
}
if formData.Language == "" {
formData.Language = l.renderer.ReqLang(translator, r).String()
}
data := registerData{
baseData: l.getBaseData(r, authRequest, "Register", errID, errMessage),
registerFormData: *formData,
}
var resourceOwner string
if authRequest != nil {
resourceOwner = authRequest.RequestedOrgID
}
if resourceOwner == "" {
iam, err := l.query.IAMByID(r.Context(), domain.IAMID)
if err != nil {
l.renderRegister(w, r, authRequest, formData, err)
return
}
resourceOwner = iam.GlobalOrgID
}
pwPolicy, description, _ := l.getPasswordComplexityPolicy(r, authRequest, resourceOwner)
if pwPolicy != nil {
data.PasswordPolicyDescription = description
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.getOrgIamPolicy(r, resourceOwner)
if err != nil {
l.renderRegister(w, r, authRequest, formData, err)
return
}
data.ShowUsername = orgIAMPolicy.UserLoginMustBeDomain
data.OrgRegister = orgIAMPolicy.UserLoginMustBeDomain
funcs := map[string]interface{}{
"selectedLanguage": func(l string) bool {
if formData == nil {
return false
}
return formData.Language == l
},
"selectedGender": func(g int32) bool {
if formData == nil {
return false
}
return formData.Gender == g
},
}
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),
Gender: domain.Gender(d.Gender),
},
Password: &domain.Password{
SecretString: d.Password,
},
Email: &domain.Email{
EmailAddress: d.Email,
},
}
}

View File

@@ -0,0 +1,60 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
)
const (
tmplRegisterOption = "registeroption"
)
type registerOptionFormData struct {
UsernamePassword bool `schema:"usernamepassword"`
}
type registerOptionData struct {
baseData
}
func (l *Login) handleRegisterOption(w http.ResponseWriter, r *http.Request) {
data := new(registerOptionFormData)
authRequest, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authRequest, err)
return
}
l.renderRegisterOption(w, r, authRequest, nil)
}
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)
}
data := registerOptionData{
baseData: l.getBaseData(r, authReq, "RegisterOption", errID, errMessage),
}
funcs := map[string]interface{}{
"hasExternalLogin": func() bool {
return authReq.LoginPolicy.AllowExternalIDP && authReq.AllowedExternalIDPs != nil && len(authReq.AllowedExternalIDPs) > 0
},
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOption], data, funcs)
}
func (l *Login) handleRegisterOptionCheck(w http.ResponseWriter, r *http.Request) {
data := new(registerOptionFormData)
authReq, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authReq, err)
return
}
if data.UsernamePassword {
l.handleRegister(w, r)
return
}
l.handleRegisterOption(w, r)
}

View File

@@ -0,0 +1,142 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
)
const (
tmplRegisterOrg = "registerorg"
)
type registerOrgFormData struct {
RegisterOrgName string `schema:"orgname"`
Email string `schema:"email"`
Username string `schema:"username"`
Firstname string `schema:"firstname"`
Lastname string `schema:"lastname"`
Password string `schema:"register-password"`
Password2 string `schema:"register-password-confirmation"`
TermsConfirm bool `schema:"terms-confirm"`
}
type registerOrgData struct {
baseData
registerOrgFormData
PasswordPolicyDescription string
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
UserLoginMustBeDomain bool
IamDomain string
}
func (l *Login) handleRegisterOrg(w http.ResponseWriter, r *http.Request) {
data := new(registerOrgFormData)
authRequest, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authRequest, err)
return
}
l.renderRegisterOrg(w, r, authRequest, data, nil)
}
func (l *Login) handleRegisterOrgCheck(w http.ResponseWriter, r *http.Request) {
data := new(registerOrgFormData)
authRequest, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authRequest, err)
return
}
if data.Password != data.Password2 {
err := caos_errs.ThrowInvalidArgument(nil, "VIEW-KaGue", "Errors.User.Password.ConfirmationWrong")
l.renderRegisterOrg(w, r, authRequest, data, err)
return
}
ctx := setContext(r.Context(), "")
userIDs, err := l.getClaimedUserIDsOfOrgDomain(ctx, data.RegisterOrgName)
if err != nil {
l.renderRegisterOrg(w, r, authRequest, data, err)
return
}
_, err = l.command.SetUpOrg(ctx, data.toOrgDomain(), data.toUserDomain(), userIDs, true)
if err != nil {
l.renderRegisterOrg(w, r, authRequest, data, err)
return
}
if authRequest == nil {
http.Redirect(w, r, l.zitadelURL, http.StatusFound)
return
}
l.renderNextStep(w, r, authRequest)
}
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)
}
data := registerOrgData{
baseData: l.getBaseData(r, authRequest, "Register", errID, errMessage),
registerOrgFormData: *formData,
}
pwPolicy, description, _ := l.getPasswordComplexityPolicy(r, authRequest, "0")
if pwPolicy != nil {
data.PasswordPolicyDescription = description
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
}
}
orgPolicy, _ := l.getDefaultOrgIamPolicy(r)
if orgPolicy != nil {
data.UserLoginMustBeDomain = orgPolicy.UserLoginMustBeDomain
data.IamDomain = l.iamDomain
}
translator := l.getTranslator(authRequest)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplRegisterOrg], data, nil)
}
func (d registerOrgFormData) toUserDomain() *domain.Human {
if d.Username == "" {
d.Username = d.Email
}
return &domain.Human{
Username: d.Username,
Profile: &domain.Profile{
FirstName: d.Firstname,
LastName: d.Lastname,
},
Password: &domain.Password{
SecretString: d.Password,
},
Email: &domain.Email{
EmailAddress: d.Email,
},
}
}
func (d registerOrgFormData) toOrgDomain() *domain.Org {
return &domain.Org{
Name: d.RegisterOrgName,
}
}

View File

@@ -0,0 +1,594 @@
package login
import (
"errors"
"fmt"
"html/template"
"net/http"
"path"
"strings"
"github.com/caos/logging"
"github.com/gorilla/csrf"
"golang.org/x/text/language"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/internal/renderer"
"github.com/caos/zitadel/internal/static"
)
const (
tmplError = "error"
)
type Renderer struct {
*renderer.Renderer
pathPrefix string
staticStorage static.Storage
}
func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage static.Storage, cookieName string, defaultLanguage language.Tag) *Renderer {
r := &Renderer{
pathPrefix: pathPrefix,
staticStorage: staticStorage,
}
tmplMapping := map[string]string{
tmplError: "error.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
tmplPasswordlessVerification: "passwordless.html",
tmplPasswordlessRegistration: "passwordless_registration.html",
tmplPasswordlessRegistrationDone: "passwordless_registration_done.html",
tmplPasswordlessPrompt: "passwordless_prompt.html",
tmplMFAVerify: "mfa_verify_otp.html",
tmplMFAPrompt: "mfa_prompt.html",
tmplMFAInitVerify: "mfa_init_otp.html",
tmplMFAU2FInit: "mfa_init_u2f.html",
tmplU2FVerification: "mfa_verification_u2f.html",
tmplMFAInitDone: "mfa_init_done.html",
tmplMailVerification: "mail_verification.html",
tmplMailVerified: "mail_verified.html",
tmplInitPassword: "init_password.html",
tmplInitPasswordDone: "init_password_done.html",
tmplInitUser: "init_user.html",
tmplInitUserDone: "init_user_done.html",
tmplPasswordResetDone: "password_reset_done.html",
tmplChangePassword: "change_password.html",
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",
tmplChangeUsernameDone: "change_username_done.html",
tmplLinkUsersDone: "link_users_done.html",
tmplExternalNotFoundOption: "external_not_found_option.html",
tmplLoginSuccess: "login_success.html",
}
funcs := map[string]interface{}{
"resourceUrl": func(file string) string {
return path.Join(r.pathPrefix, EndpointResources, file)
},
"resourceThemeUrl": func(file, theme string) string {
return path.Join(r.pathPrefix, EndpointResources, "themes", theme, file)
},
"hasCustomPolicy": func(policy *domain.LabelPolicy) bool {
if policy != nil {
return true
}
return false
},
"hasWatermark": func(policy *domain.LabelPolicy) bool {
if policy != nil && policy.DisableWatermark {
return false
}
return true
},
"variablesCssFileUrl": func(orgID string, policy *domain.LabelPolicy) string {
cssFile := domain.CssPath + "/" + domain.CssVariablesFileName
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%v&%s=%s", EndpointDynamicResources, "orgId", orgID, "default-policy", policy.Default, "filename", cssFile))
},
"customLogoResource": func(orgID string, policy *domain.LabelPolicy, darkMode bool) string {
fileName := policy.LogoURL
if darkMode && policy.LogoDarkURL != "" {
fileName = policy.LogoDarkURL
}
if fileName == "" {
return ""
}
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%v&%s=%s", EndpointDynamicResources, "orgId", orgID, "default-policy", policy.Default, "filename", fileName))
},
"avatarResource": func(orgID, avatar string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%v&%s=%s", EndpointDynamicResources, "orgId", orgID, "default-policy", false, "filename", avatar))
},
"loginUrl": func() string {
return path.Join(r.pathPrefix, EndpointLogin)
},
"externalIDPAuthURL": func(authReqID, idpConfigID string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%s", EndpointExternalLogin, QueryAuthRequestID, authReqID, queryIDPConfigID, idpConfigID))
},
"externalIDPRegisterURL": func(authReqID, idpConfigID string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s&%s=%s", EndpointExternalRegister, QueryAuthRequestID, authReqID, queryIDPConfigID, idpConfigID))
},
"registerUrl": func(id string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointRegister, QueryAuthRequestID, id))
},
"loginNameUrl": func() string {
return path.Join(r.pathPrefix, EndpointLoginName)
},
"loginNameChangeUrl": func(id string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointLoginName, QueryAuthRequestID, id))
},
"userSelectionUrl": func() string {
return path.Join(r.pathPrefix, EndpointUserSelection)
},
"passwordLessVerificationUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessLogin)
},
"passwordLessRegistrationUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessRegistration)
},
"passwordlessPromptUrl": func() string {
return path.Join(r.pathPrefix, EndpointPasswordlessPrompt)
},
"passwordResetUrl": func(id string) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s", EndpointPasswordReset, QueryAuthRequestID, id))
},
"passwordUrl": func() string {
return path.Join(r.pathPrefix, EndpointPassword)
},
"mfaVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAVerify)
},
"mfaPromptUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAPrompt)
},
"mfaPromptChangeUrl": func(id string, provider domain.MFAType) string {
return path.Join(r.pathPrefix, fmt.Sprintf("%s?%s=%s;%s=%v", EndpointMFAPrompt, QueryAuthRequestID, id, "provider", provider))
},
"mfaInitVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAInitVerify)
},
"mfaInitU2FVerifyUrl": func() string {
return path.Join(r.pathPrefix, EndpointMFAInitU2FVerify)
},
"mfaInitU2FLoginUrl": func() string {
return path.Join(r.pathPrefix, EndpointU2FVerification)
},
"mailVerificationUrl": func() string {
return path.Join(r.pathPrefix, EndpointMailVerification)
},
"initPasswordUrl": func() string {
return path.Join(r.pathPrefix, EndpointInitPassword)
},
"initUserUrl": func() string {
return path.Join(r.pathPrefix, EndpointInitUser)
},
"changePasswordUrl": func() string {
return path.Join(r.pathPrefix, EndpointChangePassword)
},
"registerOptionUrl": func() string {
return path.Join(r.pathPrefix, EndpointRegisterOption)
},
"registrationUrl": func() string {
return path.Join(r.pathPrefix, EndpointRegister)
},
"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)
},
"externalNotFoundOptionUrl": func(action string) string {
return path.Join(r.pathPrefix, EndpointExternalNotFoundOption+"?"+action+"=true")
},
"selectedLanguage": func(l string) bool {
return false
},
"selectedGender": func(g int32) bool {
return false
},
"hasUsernamePasswordLogin": func() bool {
return false
},
"showPasswordReset": func() bool {
return true
},
"hasExternalLogin": func() bool {
return false
},
"idpProviderClass": func(stylingType domain.IDPConfigStylingType) string {
return stylingType.GetCSSClass()
},
}
var err error
r.Renderer, err = renderer.NewRenderer(
staticDir,
tmplMapping, funcs,
i18n.TranslatorConfig{DefaultLanguage: defaultLanguage, CookieName: cookieName},
)
logging.Log("APP-40tSoJ").OnError(err).WithError(err).Panic("error creating renderer")
return r
}
func (l *Login) renderNextStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
if authReq == nil {
l.renderInternalError(w, r, nil, caos_errs.ThrowInvalidArgument(nil, "LOGIN-Df3f2", "Errors.AuthRequest.NotFound"))
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID)
if err != nil {
l.renderInternalError(w, r, authReq, err)
return
}
if len(authReq.PossibleSteps) == 0 {
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-9sdp4", "no possible steps"))
return
}
l.chooseNextStep(w, r, authReq, 0, nil)
}
func (l *Login) renderError(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
if err != nil {
l.renderInternalError(w, r, authReq, err)
return
}
if authReq == nil || len(authReq.PossibleSteps) == 0 {
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(err, "APP-OVOiT", "no possible steps"))
return
}
l.chooseNextStep(w, r, authReq, 0, err)
}
func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, stepNumber int, err error) {
switch step := authReq.PossibleSteps[stepNumber].(type) {
case *domain.LoginStep:
if len(authReq.PossibleSteps) > 1 {
l.chooseNextStep(w, r, authReq, 1, err)
return
}
l.renderLogin(w, r, authReq, err)
case *domain.RegistrationStep:
l.renderRegisterOption(w, r, authReq, nil)
case *domain.SelectUserStep:
l.renderUserSelection(w, r, authReq, step)
case *domain.RedirectToExternalIDPStep:
l.handleIDP(w, r, authReq, authReq.SelectedIDPConfigID)
case *domain.InitPasswordStep:
l.renderInitPassword(w, r, authReq, authReq.UserID, "", err)
case *domain.PasswordStep:
l.renderPassword(w, r, authReq, nil)
case *domain.PasswordlessStep:
l.renderPasswordlessVerification(w, r, authReq, step.PasswordSet, nil)
case *domain.PasswordlessRegistrationPromptStep:
l.renderPasswordlessPrompt(w, r, authReq, nil)
case *domain.MFAVerificationStep:
l.renderMFAVerify(w, r, authReq, step, err)
case *domain.RedirectToCallbackStep:
if len(authReq.PossibleSteps) > 1 {
l.chooseNextStep(w, r, authReq, 1, err)
return
}
l.redirectToCallback(w, r, authReq)
case *domain.LoginSucceededStep:
l.redirectToLoginSuccess(w, r, authReq.ID)
case *domain.ChangePasswordStep:
l.renderChangePassword(w, r, authReq, err)
case *domain.VerifyEMailStep:
l.renderMailVerification(w, r, authReq, "", err)
case *domain.MFAPromptStep:
l.renderMFAPrompt(w, r, authReq, step, err)
case *domain.InitUserStep:
l.renderInitUser(w, r, authReq, "", "", step.PasswordSet, nil)
case *domain.ChangeUsernameStep:
l.renderChangeUsername(w, r, authReq, nil)
case *domain.LinkUsersStep:
l.linkUsers(w, r, authReq, err)
case *domain.ExternalNotFoundOptionStep:
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, nil, err)
case *domain.ExternalLoginStep:
l.handleExternalLoginStep(w, r, authReq, step.SelectedIDPConfigID)
case *domain.GrantRequiredStep:
l.renderInternalError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-asb43", "Errors.User.GrantRequired"))
case *domain.ProjectRequiredStep:
l.renderInternalError(w, r, authReq, caos_errs.ThrowPreconditionFailed(nil, "APP-m92d", "Errors.User.ProjectRequired"))
default:
l.renderInternalError(w, r, authReq, caos_errs.ThrowInternal(nil, "APP-ds3QF", "step no possible"))
}
}
func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var msg string
if err != nil {
_, msg = l.getErrorMessage(r, err)
}
data := l.getBaseData(r, authReq, "Error", "Internal", msg)
l.renderer.RenderTemplate(w, r, l.getTranslator(authReq), l.renderer.Templates[tmplError], data, nil)
}
func (l *Login) getUserData(r *http.Request, authReq *domain.AuthRequest, title string, errType, errMessage string) userData {
userData := userData{
baseData: l.getBaseData(r, authReq, title, errType, errMessage),
profileData: l.getProfileData(authReq),
}
if authReq != nil && authReq.LinkingUsers != nil {
userData.Linking = len(authReq.LinkingUsers) > 0
}
return userData
}
func (l *Login) getBaseData(r *http.Request, authReq *domain.AuthRequest, title string, errType, errMessage string) baseData {
baseData := baseData{
errorData: errorData{
ErrID: errType,
ErrMessage: errMessage,
},
Lang: l.renderer.ReqLang(l.getTranslator(authReq), r).String(),
Title: title,
Theme: l.getTheme(r),
ThemeMode: l.getThemeMode(r),
DarkMode: l.isDarkMode(r),
PrivateLabelingOrgID: l.getPrivateLabelingID(authReq),
OrgID: l.getOrgID(authReq),
OrgName: l.getOrgName(authReq),
PrimaryDomain: l.getOrgPrimaryDomain(authReq),
DisplayLoginNameSuffix: l.isDisplayLoginNameSuffix(authReq),
AuthReqID: getRequestID(authReq, r),
CSRF: csrf.TemplateField(r),
Nonce: http_mw.GetNonce(r),
}
if authReq != nil {
baseData.LoginPolicy = authReq.LoginPolicy
baseData.LabelPolicy = authReq.LabelPolicy
baseData.IDPProviders = authReq.AllowedExternalIDPs
if authReq.PrivacyPolicy != nil {
baseData.TOSLink = authReq.PrivacyPolicy.TOSLink
baseData.PrivacyLink = authReq.PrivacyPolicy.PrivacyLink
}
} else {
privacyPolicy, err := l.query.DefaultPrivacyPolicy(r.Context())
if err != nil {
return baseData
}
if privacyPolicy != nil {
baseData.TOSLink = privacyPolicy.TOSLink
baseData.PrivacyLink = privacyPolicy.PrivacyLink
}
}
return baseData
}
func (l *Login) getTranslator(authReq *domain.AuthRequest) *i18n.Translator {
translator, _ := l.renderer.NewTranslator()
if authReq != nil {
l.addLoginTranslations(translator, authReq.DefaultTranslations)
l.addLoginTranslations(translator, authReq.OrgTranslations)
translator.SetPreferredLanguages(authReq.UiLocales...)
}
return translator
}
func (l *Login) getProfileData(authReq *domain.AuthRequest) profileData {
var userName, loginName, displayName, avatar string
if authReq != nil {
userName = authReq.UserName
loginName = authReq.LoginName
displayName = authReq.DisplayName
avatar = authReq.AvatarKey
}
return profileData{
UserName: userName,
LoginName: loginName,
DisplayName: displayName,
AvatarKey: avatar,
}
}
func (l *Login) getErrorMessage(r *http.Request, err error) (errID, errMsg string) {
caosErr := new(caos_errs.CaosError)
if errors.As(err, &caosErr) {
localized := l.renderer.LocalizeFromRequest(l.getTranslator(nil), r, caosErr.Message, nil)
return caosErr.ID, localized
}
return "", err.Error()
}
func (l *Login) getTheme(r *http.Request) string {
return "zitadel" //TODO: impl
}
func (l *Login) getThemeMode(r *http.Request) string {
if l.isDarkMode(r) {
return "lgn-dark-theme"
}
return "lgn-light-theme"
}
func (l *Login) isDarkMode(r *http.Request) bool {
cookie, err := r.Cookie("mode")
if err != nil {
return false
}
return strings.HasSuffix(cookie.Value, "dark")
}
func (l *Login) getOrgID(authReq *domain.AuthRequest) string {
if authReq == nil {
return ""
}
if authReq.RequestedOrgID != "" {
return authReq.RequestedOrgID
}
return authReq.UserOrgID
}
func (l *Login) getPrivateLabelingID(authReq *domain.AuthRequest) string {
privateLabelingOrgID := domain.IAMID
if authReq == nil {
return privateLabelingOrgID
}
if authReq.PrivateLabelingSetting != domain.PrivateLabelingSettingUnspecified {
privateLabelingOrgID = authReq.ApplicationResourceOwner
}
if authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || authReq.PrivateLabelingSetting == domain.PrivateLabelingSettingUnspecified {
if authReq.UserOrgID != "" {
privateLabelingOrgID = authReq.UserOrgID
}
}
if authReq.RequestedOrgID != "" {
privateLabelingOrgID = authReq.RequestedOrgID
}
return privateLabelingOrgID
}
func (l *Login) getOrgName(authReq *domain.AuthRequest) string {
if authReq == nil {
return ""
}
return authReq.RequestedOrgName
}
func (l *Login) getOrgPrimaryDomain(authReq *domain.AuthRequest) string {
if authReq == nil {
return ""
}
return authReq.RequestedPrimaryDomain
}
func (l *Login) isDisplayLoginNameSuffix(authReq *domain.AuthRequest) bool {
if authReq == nil {
return false
}
if authReq.RequestedOrgID == "" {
return false
}
return authReq.LabelPolicy != nil && !authReq.LabelPolicy.HideLoginNameSuffix
}
func (l *Login) addLoginTranslations(translator *i18n.Translator, customTexts []*domain.CustomText) {
for _, text := range customTexts {
msg := i18n.Message{
ID: text.Key,
Text: text.Text,
}
err := l.renderer.AddMessages(translator, text.Language, msg)
logging.Log("HANDLE-GD3g2").OnError(err).Warn("could no add message to translator")
}
}
func getRequestID(authReq *domain.AuthRequest, r *http.Request) string {
if authReq != nil {
return authReq.ID
}
return r.FormValue(QueryAuthRequestID)
}
func (l *Login) csrfErrorHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := csrf.FailureReason(r)
l.renderInternalError(w, r, nil, err)
})
}
func (l *Login) cspErrorHandler(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
l.renderInternalError(w, r, nil, err)
})
}
type baseData struct {
errorData
Lang string
Title string
Theme string
ThemeMode string
DarkMode bool
PrivateLabelingOrgID string
OrgID string
OrgName string
PrimaryDomain string
DisplayLoginNameSuffix bool
TOSLink string
PrivacyLink string
AuthReqID string
CSRF template.HTML
Nonce string
LoginPolicy *domain.LoginPolicy
IDPProviders []*domain.IDPProvider
LabelPolicy *domain.LabelPolicy
LoginTexts []*domain.CustomLoginText
}
type errorData struct {
ErrID string
ErrMessage string
}
type userData struct {
baseData
profileData
PasswordChecked string
MFAProviders []domain.MFAType
SelectedMFAProvider domain.MFAType
Linking bool
}
type profileData struct {
LoginName string
UserName string
DisplayName string
AvatarKey string
}
type passwordData struct {
baseData
profileData
PasswordPolicyDescription string
MinLength uint64
HasUppercase string
HasLowercase string
HasNumber string
HasSymbol string
}
type userSelectionData struct {
baseData
Users []domain.UserSelection
Linking bool
}
type mfaData struct {
baseData
profileData
MFAProviders []domain.MFAType
MFARequired bool
}
type mfaVerifyData struct {
baseData
profileData
MFAType domain.MFAType
otpData
}
type mfaDoneData struct {
baseData
profileData
MFAType domain.MFAType
}
type otpData struct {
Url string
Secret string
QrCode string
}

View File

@@ -0,0 +1,97 @@
package login
import (
"context"
"net/http"
"github.com/caos/zitadel/internal/domain"
)
type dynamicResourceData struct {
OrgID string `schema:"orgId"`
DefaultPolicy bool `schema:"default-policy"`
FileName string `schema:"filename"`
}
func (l *Login) handleResources(staticDir http.FileSystem) http.Handler {
return http.FileServer(staticDir)
}
func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {
data := new(dynamicResourceData)
err := l.getParseData(r, data)
if err != nil {
return
}
bucketName := domain.IAMID
if data.OrgID != "" && !data.DefaultPolicy {
bucketName = data.OrgID
}
etag := r.Header.Get("If-None-Match")
asset, info, err := l.getStatic(r.Context(), bucketName, data.FileName)
if info != nil && info.ETag == etag {
w.WriteHeader(304)
return
}
if err != nil {
return
}
//TODO: enable again when assets are implemented again
_ = asset
//w.Header().Set("content-length", strconv.FormatInt(info.Size, 10))
//w.Header().Set("content-type", info.ContentType)
//w.Header().Set("ETag", info.ETag)
//w.Write(asset)
}
func (l *Login) getStatic(ctx context.Context, bucketName, fileName string) ([]byte, *domain.AssetInfo, error) {
s := new(staticAsset)
//TODO: enable again when assets are implemented again
//key := bucketName + "-" + fileName
//err := l.staticCache.Get(key, s)
//if err == nil && s.Info != nil && (s.Info.Expiration.After(time.Now().Add(-1 * time.Minute))) { //TODO: config?
// return s.Data, s.Info, nil
//}
//info, err := l.staticStorage.GetObjectInfo(ctx, bucketName, fileName)
//if err != nil {
// if caos_errs.IsNotFound(err) {
// return nil, nil, err
// }
// return s.Data, s.Info, err
//}
//if s.Info != nil && s.Info.ETag == info.ETag {
// if info.Expiration.After(s.Info.Expiration) {
// s.Info = info
// //l.cacheStatic(bucketName, fileName, s)
// }
// return s.Data, s.Info, nil
//}
//
//reader, _, err := l.staticStorage.GetObject(ctx, bucketName, fileName)
//if err != nil {
// return s.Data, s.Info, err
//}
//s.Data, err = ioutil.ReadAll(reader)
//if err != nil {
// return nil, nil, err
//}
//s.Info = info
//l.cacheStatic(bucketName, fileName, s)
return s.Data, s.Info, nil
}
//TODO: enable again when assets are implemented again
//
//func (l *Login) cacheStatic(bucketName, fileName string, s *staticAsset) {
// key := bucketName + "-" + fileName
// err := l.staticCache.Set(key, &s)
// logging.Log("HANDLER-dfht2").OnError(err).Warnf("caching of asset %s: %s failed", bucketName, fileName)
//}
type staticAsset struct {
Data []byte
Info *domain.AssetInfo
}

View File

@@ -0,0 +1,98 @@
package login
import (
"net/http"
"github.com/gorilla/mux"
)
const (
EndpointRoot = "/"
EndpointHealthz = "/healthz"
EndpointReadiness = "/ready"
EndpointLogin = "/login"
EndpointExternalLogin = "/login/externalidp"
EndpointExternalLoginCallback = "/login/externalidp/callback"
EndpointJWTAuthorize = "/login/jwt/authorize"
EndpointJWTCallback = "/login/jwt/callback"
EndpointPasswordlessLogin = "/login/passwordless"
EndpointPasswordlessRegistration = "/login/passwordless/init"
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
EndpointLoginName = "/loginname"
EndpointUserSelection = "/userselection"
EndpointChangeUsername = "/username/change"
EndpointPassword = "/password"
EndpointInitPassword = "/password/init"
EndpointChangePassword = "/password/change"
EndpointPasswordReset = "/password/reset"
EndpointInitUser = "/user/init"
EndpointMFAVerify = "/mfa/verify"
EndpointMFAPrompt = "/mfa/prompt"
EndpointMFAInitVerify = "/mfa/init/verify"
EndpointMFAInitU2FVerify = "/mfa/init/u2f/verify"
EndpointU2FVerification = "/mfa/u2f/verify"
EndpointMailVerification = "/mail/verification"
EndpointMailVerified = "/mail/verified"
EndpointRegisterOption = "/register/option"
EndpointRegister = "/register"
EndpointExternalRegister = "/register/externalidp"
EndpointExternalRegisterCallback = "/register/externalidp/callback"
EndpointRegisterOrg = "/register/org"
EndpointLogoutDone = "/logout/done"
EndpointLoginSuccess = "/login/success"
EndpointExternalNotFoundOption = "/externaluser/option"
EndpointResources = "/resources"
EndpointDynamicResources = "/resources/dynamic"
)
func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.MiddlewareFunc) *mux.Router {
router := mux.NewRouter()
router.Use(interceptors...)
router.HandleFunc(EndpointRoot, login.handleLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointHealthz, login.handleHealthz).Methods(http.MethodGet)
router.HandleFunc(EndpointReadiness, login.handleReadiness).Methods(http.MethodGet)
router.HandleFunc(EndpointLogin, login.handleLogin).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointExternalLogin, login.handleExternalLogin).Methods(http.MethodGet)
router.HandleFunc(EndpointExternalLoginCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointJWTAuthorize, login.handleJWTRequest).Methods(http.MethodGet)
router.HandleFunc(EndpointJWTCallback, login.handleJWTCallback).Methods(http.MethodGet)
router.HandleFunc(EndpointPasswordlessLogin, login.handlePasswordlessVerification).Methods(http.MethodPost)
router.HandleFunc(EndpointPasswordlessRegistration, login.handlePasswordlessRegistration).Methods(http.MethodGet)
router.HandleFunc(EndpointPasswordlessRegistration, login.handlePasswordlessRegistrationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointPasswordlessPrompt, login.handlePasswordlessPrompt).Methods(http.MethodPost)
router.HandleFunc(EndpointLoginName, login.handleLoginName).Methods(http.MethodGet)
router.HandleFunc(EndpointLoginName, login.handleLoginNameCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointUserSelection, login.handleSelectUser).Methods(http.MethodPost)
router.HandleFunc(EndpointChangeUsername, login.handleChangeUsername).Methods(http.MethodPost)
router.HandleFunc(EndpointPassword, login.handlePasswordCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointInitPassword, login.handleInitPassword).Methods(http.MethodGet)
router.HandleFunc(EndpointInitPassword, login.handleInitPasswordCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointPasswordReset, login.handlePasswordReset).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUser).Methods(http.MethodGet)
router.HandleFunc(EndpointInitUser, login.handleInitUserCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAVerify, login.handleMFAVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAPrompt, login.handleMFAPromptSelection).Methods(http.MethodGet)
router.HandleFunc(EndpointMFAPrompt, login.handleMFAPrompt).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAInitVerify, login.handleMFAInitVerify).Methods(http.MethodPost)
router.HandleFunc(EndpointMFAInitU2FVerify, login.handleRegisterU2F).Methods(http.MethodPost)
router.HandleFunc(EndpointU2FVerification, login.handleU2FVerification).Methods(http.MethodPost)
router.HandleFunc(EndpointMailVerification, login.handleMailVerification).Methods(http.MethodGet)
router.HandleFunc(EndpointMailVerification, login.handleMailVerificationCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointChangePassword, login.handleChangePassword).Methods(http.MethodPost)
router.HandleFunc(EndpointRegisterOption, login.handleRegisterOption).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOption, login.handleRegisterOptionCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointExternalNotFoundOption, login.handleExternalNotFoundOptionCheck).Methods(http.MethodPost)
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)
router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet)
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost)
router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet)
return router
}

View File

@@ -0,0 +1,47 @@
package login
import (
"net/http"
"github.com/caos/zitadel/internal/domain"
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
)
const (
tmplUserSelection = "userselection"
)
type userSelectionFormData struct {
UserID string `schema:"userID"`
}
func (l *Login) renderUserSelection(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectionData *domain.SelectUserStep) {
data := userSelectionData{
baseData: l.getBaseData(r, authReq, "Select User", "", ""),
Users: selectionData.Users,
Linking: len(authReq.LinkingUsers) > 0,
}
translator := l.getTranslator(authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplUserSelection], data, nil)
}
func (l *Login) handleSelectUser(w http.ResponseWriter, r *http.Request) {
data := new(userSelectionFormData)
authSession, err := l.getAuthRequestAndParseData(r, data)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
if data.UserID == "0" {
l.renderLogin(w, r, authSession, nil)
return
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.SelectUser(r.Context(), authSession.ID, data.UserID, userAgentID)
if err != nil {
l.renderError(w, r, authSession, err)
return
}
l.renderNextStep(w, r, authSession)
}

View File

@@ -0,0 +1,369 @@
Login:
Title: Anmeldung
Description: Mit ZITADEL-Konto anmelden.
TitleLinking: Anmeldung für Benutzer Linking
DescriptionLinking: Gib deine Benutzerdaten ein um den externen Benutzer mit einem ZITADEL Benutzer zu linken.
LoginNameLabel: Loginname
UsernamePlaceHolder: username
LoginnamePlaceHolder: username@domain
ExternalUserDescription: Melde dich mit einem externen Benutzer an
MustBeMemberOfOrg: Der Benutzer muss der Organisation {{.OrgName}} angehören.
RegisterButtonText: registrieren
NextButtonText: weiter
SelectAccount:
Title: Account auswählen
Description: Wähle deinen Account aus.
TitleLinking: Account auswählen um zu verlinken
DescriptionLinking: Wähle deinen Account, um diesen mit deinem externen Benutzer zu verlinken.
OtherUser: Anderer Benutzer
SessionState0: aktiv
SessionState1: inaktiv
MustBeMemberOfOrg: Der Benutzer muss der Organisation {{.OrgName}} angehören.
Password:
Title: Willkommen zurück!
Description: Gib deine Benutzerdaten ein.
PasswordLabel: Passwort
MinLength: Mindestlänge
HasUppercase: Grossbuchstaben
HasLowercase: Kleinbuchstaben
HasNumber: Nummer
HasSymbol: Symbol
Confirmation: Bestätigung stimmt überein
ResetLinkText: Password zurücksetzen
BackButtonText: zurück
NextButtonText: weiter
UsernameChange:
Title: Usernamen ändern
Description: Wähle deinen neuen Benutzernamen
UsernameLabel: Benutzernamen
CancelButtonText: abbrechen
NextButtonText: weiter
UsernameChangeDone:
Title: Username geändert
Description: Der Username wurde erfolgreich geändert.
NextButtonText: next
InitPassword:
Title: Passwort setzen
Description: Du hast einen Code erhalten, welcher im untenstehenden Formular eingegeben werden muss um ein neues Passwort zu setzen.
CodeLabel: Code
NewPasswordLabel: Neues Passwort
NewPasswordConfirmLabel: Passwort bestätigen
ResendButtonText: erneut senden
NextButtonText: weiter
InitPasswordDone:
Title: Passwort gesetzt
Description: Passwort erfolgreich gesetzt
NextButtonText: weiter
CancelButtonText: abbrechen
InitUser:
Title: User aktivieren
Description: Du hast einen Code erhalten, welcher im untenstehenden Formular eingegeben werden muss um deine EMail zu verifizieren und ein neues Passwort zu setzen.
CodeLabel: Code
NewPasswordLabel: Neues Passwort
NewPasswordConfirmLabel: Passwort bestätigen
NextButtonText: weiter
ResendButtonText: erneut senden
InitUserDone:
Title: User aktiviert
Description: EMail verifiziert und Passwort erfolgreich gesetzt
NextButtonText: weiter
CancelButtonText: abbrechen
InitMFAPrompt:
Title: Multifaktor hinzufügen
Description: Möchtest du einen Mulitfaktor hinzufügen?
Provider0: OTP (One Time Password)
Provider1: U2F (Universal 2nd Factor)
NextButtonText: weiter
SkipButtonText: überspringen
InitMFAOTP:
Title: Multifaktor Verifizierung
Description: Verifiziere deinen Multifaktor
OTPDescription: Scanne den Code mit einem Authentifizierungs-App (z.B Google Authenticator) oder kopiere das Secret und gib anschliessend den Code ein.
SecretLabel: Secret
CodeLabel: Code
NextButtonText: weiter
CancelButtonText: abbrechen
InitMFAU2F:
Title: Multifaktor U2F / WebAuthN hinzufügen
Description: Füge dein Token hinzu, indem du einen Namen eingibst und den 'Token registrieren' Button drückst.
TokenNameLabel: Name des Tokens / Geräts
NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze einen anderen (z.B. Chrome, Safari, Firefox)
RegisterTokenButtonText: Token registrieren
ErrorRetry: Versuche es erneut, erstelle eine neue Abfrage oder wähle einen andere Methode.
InitMFADone:
Title: Multifaktor Verifizierung erstellt
Description: Multifikator Verifizierung erfolgreich abgeschlossen. Der Multifaktor muss bei jeder Anmeldung eingegeben werden.
NextButtonText: weiter
CancelButtonText: abbrechen
MFAProvider:
Provider0: OTP (One Time Password)
Provider1: U2F (Universal 2nd Factor)
ChooseOther: oder wähle eine andere Option aus
VerifyMFAOTP:
Title: Multifaktor verifizieren
Description: Verifiziere deinen Multifaktor
CodeLabel: Code
NextButtonText: next
VerifyMFAU2F:
Title: Multifaktor Verifizierung
Description: Verifiziere deinen Multifaktor U2F / WebAuthN Token
NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze einen anderen (z.B. Chrome, Safari, Firefox)
ErrorRetry: Versuche es erneut, erstelle eine neue Abfrage oder wähle einen andere Methode.
ValidateTokenButtonText: Token validieren
Passwordless:
Title: Passwortlos einloggen
Description: Verifiziere dein Token
NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze einen anderen (z.B. Chrome, Safari, Firefox)
ErrorRetry: Versuche es erneut, erstelle eine neue Abfrage oder wähle einen andere Methode.
LoginWithPwButtonText: Mit Passwort anmelden
ValidateTokenButtonText: Token validieren
PasswordlessPrompt:
Title: Passwortloser Login hinzufügen
Description: Möchtest du einen passwortlosen Login hinzufügen?
DescriptionInit: Du musst zuerst den Passwortlosen Login hinzufügen. Nutze dazu den Link, den du erhalten hast um dein Gerät zu registrieren.
PasswordlessButtonText: Werde Passwortlos
NextButtonText: weiter
SkipButtonText: überspringen
PasswordlessRegistration:
Title: Passwortloser Login hinzufügen
Description: Füge dein Token hinzu, indem du einen Namen eingibst und den 'Token registrieren' Button drückst.
TokenNameLabel: Name des Tokens / Geräts
NotSupported: WebAuthN wird durch deinen Browser nicht unterstützt. Stelle sicher, dass du die aktuelle Version installiert hast oder nutze einen anderen (z.B. Chrome, Safari, Firefox)
RegisterTokenButtonText: Token registrieren
ErrorRetry: Versuche es erneut, erstelle eine neue Abfrage oder wähle eine andere Methode.
PasswordlessRegistrationDone:
Title: Passwortloser Login erstellt
Description: Token für passwortlosen Login erfolgreich hinzugefügt.
DescriptionClose: Du kannst das Fenster nun schliessen.
NextButtonText: weiter
CancelButtonText: abbrechen
PasswordChange:
Title: Passwort ändern
Description: Ändere dein Password in dem du dein altes und dann dein neuen Passwort eingibst.
OldPasswordLabel: Altes Passwort
NewPasswordLabel: Neues Passwort
NewPasswordConfirmLabel: Passwort Bestätigung
CancelButtonText: abbrechen
NextButtonText: weiter
PasswordChangeDone:
Title: Passwort ändern
Description: Das Passwort wurde erfolgreich geändert.
NextButtonText: weiter
PasswordResetDone:
Title: Resetlink versendet
Description: Prüfe dein E-Mail Postfach, um ein neues Passwort zu setzen.
NextButtonText: weiter
EmailVerification:
Title: E-Mail Verifizierung
Description: Du hast ein E-Mail zur Verifizierung deiner E-Mail Adresse bekommen. Gib den Code im untenstehenden Formular ein. Mit erneut versenden, wird dir ein neues E-Mail zugestellt.
CodeLabel: Code
NextButtonText: weiter
ResendButtonText: erneut senden
EmailVerificationDone:
Title: E-Mail Verifizierung
Description: Deine E-Mail Adresse wurde erfolgreich verifiziert.
NextButtonText: weiter
CancelButtonText: abbrechen
LoginButtonText: anmelden
RegisterOption:
Title: Registrations Möglichkeiten
Description: Wähle aus wie du dich registrieren möchtest.
RegisterUsernamePasswordButtonText: Mit Benutzername Passwort
ExternalLoginDescription: oder registriere dich mit einem externen Benutzer
RegistrationUser:
Title: Registration
Description: Gib deine Benutzerangaben an. Die E-Mail Adresse wird als Benutzernamen verwendet.
DescriptionOrgRegister: Gib deine Benutzerangaben an.
EmailLabel: E-Mail
UsernameLabel: Benutzername
FirstnameLabel: Vorname
LastnameLabel: Nachname
LanguageLabel: Sprache
German: Deutsch
English: English
Italian: Italiano
GenderLabel: Geschlecht
Female: weiblich
Male: männlich
Diverse: diverse
PasswordLabel: Passwort
PasswordConfirmLabel: Passwort wiederholen
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
TosConfirmAnd: und die
PrivacyLinkText: Datenschutzerklärung
ExternalLogin: oder registriere dich mit einem externen Benutzer
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
Italian: Italiano
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.
OrgNameLabel: Organisationsname
EmailLabel: E-Mail
UsernameLabel: Benutzername
FirstnameLabel: Vorname
LastnameLabel: Nachname
PasswordLabel: Passwort
PasswordConfirmLabel: Passwort wiederholen
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
TosConfirmAnd: und die
PrivacyLinkText: Datenschutzerklärung
SaveButtonText: Organisation speichern
LoginSuccess:
Title: Erfolgreich eingeloggt
AutoRedirectDescription: Du wirst automatisch zurück in die Applikation geleitet. Danach kannst du diese Fenster schliessen.
RedirectedDescription: Du kannst diese Fenster nun schliessen.
NextButtonText: weiter
LogoutDone:
Title: Ausgeloggt
Description: Du wurdest erfolgreich ausgeloggt.
LoginButtonText: anmelden
LinkingUsersDone:
Title: Benutzerlinking
Description: Benuzterlinking erledigt.
CancelButtonText: abbrechen
NextButtonText: weiter
ExternalNotFoundOption:
Title: Externer Benutzer
Description: Externer Benutzer konnte nicht gefunden werden. Willst du deinen Benutzer mit einem bestehenden verlinken oder diesen als neuen Benutzer registrieren.
LinkButtonText: Verlinken
AutoRegisterButtonText: registrieren
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
TosConfirmAnd: und die
PrivacyLinkText: Datenschutzerklärung
German: Deutsch
English: English
Italian: Italiano
Footer:
PoweredBy: Powered By
Tos: AGB
PrivacyPolicy: Datenschutzerklärung
Help: Hilfe
HelpLink: https://docs.zitadel.ch/docs/manuals/user-login
Errors:
Internal: Es ist ein interner Fehler aufgetreten
AuthRequest:
NotFound: AuthRequest konnte nicht gefunden werden
UserAgentNotCorresponding: User Agent stimmt nicht überein
UserAgentNotFound: User Agent ID nicht gefunden
TokenNotFound: Token nicht gefunden
RequestTypeNotSupported: Requesttyp wird nicht unterstürzt
MissingParameters: Benötigte Parameter fehlen
User:
NotFound: Benutzer konnte nicht gefunden werden
Inactive: Benutzer ist inaktiv
NotFoundOnOrg: Benutzer konnte in der gewünschten Organisation nicht gefunden werden
NotAllowedOrg: Benutzer gehört nicht der benötigten Organisation an
NotMatchingUserID: User stimm nicht mit User in Auth Request überein
UserIDMissing: UserID ist leer
Invalid: Userdaten sind ungültig
DomainNotAllowedAsUsername: Domäne ist bereits reserviert und kann nicht verwendet werden
NotAllowedToLink: Der Benutzer darf nicht mit einem externen Login Provider verlinkt werden
Password:
ConfirmationWrong: Passwort Bestätigung stimmt nicht überein
Empty: Passwort ist leer
Invalid: Passwort ungültig
InvalidAndLocked: Password ist undgültig und Benutzer wurde gesperrt, melden Sie sich bei ihrem Administrator.
PasswordComplexityPolicy:
NotFound: Passwort Policy konnte nicht gefunden werden
MinLength: Passwort ist zu kurz
HasLower: Passwort beinhaltet keinen klein Buchstaben
HasUpper: Passwort beinhaltet keinen gross Buchstaben
HasNumber: Passwort beinhaltet keine Nummer
HasSymbol: Passwort beinhaltet kein Symbol
Code:
Expired: Code ist abgelaufen
Invalid: Code ist ungültig
Empty: Code ist leer
CryptoCodeNil: Crypto Code ist nil
NotFound: Code konnte nicht gefunden werden
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:
AlreadyReady: Multifaktor OTP (OneTimePassword) ist bereits eingerichtet
NotExisting: Multifaktor OTP (OneTimePassword) existiert nicht
InvalidCode: Code ist ungültig
NotReady: Multifaktor OTP (OneTimePassword) ist nicht bereit
Locked: Benutzer ist gesperrt
SomethingWentWrong: Irgendetwas ist schief gelaufen
NotActive: Benutzer ist nicht aktiv
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
NoExternalUserData: Keine externe User Daten erhalten
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.
ProjectRequired: Der Login an diese Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte melde dich bei deinem Administrator.
IdentityProvider:
InvalidConfig: Identitäts Provider Konfiguration ist ungültig
IAM:
LockoutPolicy:
NotExisting: Lockout Policy existiert nicht
optional: (optional)

View File

@@ -0,0 +1,370 @@
Login:
Title: Welcome back!
Description: Enter your login data.
TitleLinking: Login for user linking
DescriptionLinking: Enter your login data to link your external user with a ZITADEL user.
LoginNameLabel: Loginname
UsernamePlaceHolder: username
LoginnamePlaceHolder: username@domain
ExternalUserDescription: Login with an external user.
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organisation.
RegisterButtonText: register
NextButtonText: next
SelectAccount:
Title: Select account
Description: Use your ZITADEL-Account
TitleLinking: Select account for user linking
DescriptionLinking: Select your account to link with your external user.
OtherUser: Other User
SessionState0: active
SessionState1: inactive
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organisation.
Password:
Title: Password
Description: Enter your login data.
PasswordLabel: Password
MinLength: Minimum length
HasUppercase: Uppercase letter
HasLowercase: Lowercase letter
HasNumber: Number
HasSymbol: Symbol
Confirmation: Confirmation match
ResetLinkText: reset password
BackButtonText: back
NextButtonText: next
UsernameChange:
Title: Change Username
Description: Set your new username
UsernameLabel: Username
CancelButtonText: cancel
NextButtonText: next
UsernameChangeDone:
Title: Username changed
Description: Your username was changed successfully.
NextButtonText: next
InitPassword:
Title: Set Password
Description: You have received a code, which you have to enter in the form below, to set your new password.
CodeLabel: Code
NewPasswordLabel: New Password
NewPasswordConfirmLabel: Confirm Password
ResendButtonText: resend
NextButtonText: next
InitPasswordDone:
Title: Password set
Description: Password successfully set
NextButtonText: next
CancelButtonText: cancel
InitUser:
Title: Activate User
Description: You have received a code, which you have to enter in the form below, to verify your email and set your new password.
CodeLabel: Code
NewPasswordLabel: New Password
NewPasswordConfirmLabel: Confirm Password
NextButtonText: next
ResendButtonText: resend
InitUserDone:
Title: User activated
Description: Email verified and Password successfully set
NextButtonText: next
CancelButtonText: cancel
InitMFAPrompt:
Title: Multifactor Setup
Description: Would you like to setup multifactor authentication?
Provider0: OTP (One Time Password)
Provider1: U2F (Universal 2nd Factor)
NextButtonText: next
SkipButtonText: skip
InitMFAOTP:
Title: Multifactor Verification
Description: Verify your multifactor.
OTPDescription: Scan the code with your authenticator app (e.g Google Authenticator) or copy the secret and insert the generated code below.
SecretLabel: Secret
CodeLabel: Code
NextButtonText: next
CancelButtonText: cancel
InitMFAU2F:
Title: Multifactor Setup U2F / WebAuthN
Description: Add your Token by providing a name and then clicking on the 'Register Token' button below.
TokenNameLabel: Name of the token / machine
NotSupported: WebAuthN is not supported by your browser. Please ensure it is up to date or use a different one (e.g. Chrome, Safari, Firefox)
RegisterTokenButtonText: Register Token
ErrorRetry: Retry, create a new challenge or choose a different method.
InitMFADone:
Title: Multifactor Verification done
Description: Multifactor verification successfully done. The multifactor has to be entered on each login.
NextButtonText: next
CancelButtonText: cancel
MFAProvider:
Provider0: OTP (One Time Password)
Provider1: U2F (Universal 2nd Factor)
ChooseOther: or choose an other option
VerifyMFAOTP:
Title: Verify Multifactor
Description: Verify your multifactor
CodeLabel: Code
NextButtonText: next
VerifyMFAU2F:
Title: Multifactor Verification
Description: Verify your multifactor U2F / WebAuthN token
NotSupported: WebAuthN is not supported by your browser. Make sure you are using the newest version or change your browser to a supported one (Chrome, Safari, Firefox)
ErrorRetry: Retry, create a new request or choose a other method.
ValidateTokenButtonText: Validate Token
Passwordless:
Title: Login passwordless
Description: Verify your token
NotSupported: WebAuthN is not supported by your browser. Please ensure it is up to date or use a different one (e.g. Chrome, Safari, Firefox)
ErrorRetry: Retry, create a new challenge or choose a different method.
LoginWithPwButtonText: Login with password
ValidateTokenButtonText: Validate Token
PasswordlessPrompt:
Title: Passwordless setup
Description: Would you like to setup passwordless login?
DescriptionInit: You need to set up passwordless login. Use the link you were given to register your device.
PasswordlessButtonText: Go passwordless
NextButtonText: next
SkipButtonText: skip
PasswordlessRegistration:
Title: Passwordless setup
Description: Add your Token by providing a name and then clicking on the 'Register Token' button below.
TokenNameLabel: Name of the token / machine
NotSupported: WebAuthN is not supported by your browser. Please ensure it is up to date or use a different one (e.g. Chrome, Safari, Firefox)
RegisterTokenButtonText: Register Token
ErrorRetry: Retry, create a new challenge or choose a different method.
PasswordlessRegistrationDone:
Title: Passwordless set up
Description: Token for passwordless successfully added.
DescriptionClose: You can now close this window.
NextButtonText: next
CancelButtonText: cancel
PasswordChange:
Title: Change Password
Description: Change your password. Enter your old and new password.
OldPasswordLabel: Old Password
NewPasswordLabel: New Password
NewPasswordConfirmLabel: Password confirmation
CancelButtonText: cancel
NextButtonText: next
PasswordChangeDone:
Title: Change Password
Description: Your password was changed successfully.
NextButtonText: next
PasswordResetDone:
Title: Reset link set
Description: Check your email to reset your password.
NextButtonText: next
EmailVerification:
Title: E-Mail Verification
Description: We have sent you an email to verify your address. Please enter the code in the form below.
CodeLabel: Code
NextButtonText: next
ResendButtonText: resend
EmailVerificationDone:
Title: E-Mail Verification
Description: Your email address has been successfully verified.
NextButtonText: next
CancelButtonText: cancel
LoginButtonText: login
RegisterOption:
Title: Registration Options
Description: Choose how you'd like to register
RegisterUsernamePasswordButtonText: With username password
ExternalLoginDescription: or register with an external user
RegistrationUser:
Title: Registration
Description: Enter your Userdata. Your email address will be used as loginname.
DescriptionOrgRegister: Enter your Userdata.
EmailLabel: E-Mail
UsernameLabel: Username
FirstnameLabel: Firstname
LastnameLabel: Lastname
LanguageLabel: Language
German: Deutsch
English: English
Italian: Italiano
GenderLabel: Gender
Female: Female
Male: Male
Diverse: diverse / X
PasswordLabel: Password
PasswordConfirmLabel: Password confirmation
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: 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
Italian: Italiano
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.
OrgNameLabel: Organisationname
EmailLabel: E-Mail
UsernameLabel: Username
FirstnameLabel: Firstname
LastnameLabel: Lastname
PasswordLabel: Password
PasswordConfirmLabel: Password confirmation
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
TosConfirmAnd: and the
PrivacyLinkText: privacy policy
SaveButtonText: Create organization
LoginSuccess:
Title: Login successful
AutoRedirectDescription: You will be directed back to your application automatically. If not, click on the button below. You can close the window afterwards.
RedirectedDescription: You can now close this window.
NextButtonText: next
LogoutDone:
Title: Logged out
Description: You have logged out successfully.
LoginButtonText: login
LinkingUsersDone:
Title: Userlinking
Description: Userlinking done.
CancelButtonText: cancel
NextButtonText: next
ExternalNotFoundOption:
Title: External User
Description: External user not found. Do you want to link your user or auto register a new one.
LinkButtonText: Link
AutoRegisterButtonText: register
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
TosConfirmAnd: and the
PrivacyLinkText: privacy policy
German: Deutsch
English: English
Italian: Italiano
Footer:
PoweredBy: Powered By
Tos: TOS
PrivacyPolicy: Privacy policy
Help: Help
HelpLink: https://docs.zitadel.ch/docs/manuals/user-login
Errors:
Internal: An internal error occured
AuthRequest:
NotFound: Could not find authrequest
UserAgentNotCorresponding: User Agent does not correspond
UserAgentNotFound: User Agent ID not found
TokenNotFound: Token not found
RequestTypeNotSupported: Request type is not supported
MissingParameters: Required parameters missing
User:
NotFound: User could not be found
Inactive: User is inactive
NotFoundOnOrg: User could not be found on chosen organisation
NotAllowedOrg: User is no member of the required organisation
NotMatchingUserID: User and user in authrequest don't match
UserIDMissing: UserID is empty
Invalid: Invalid userdata
DomainNotAllowedAsUsername: Domain is already reserved and cannot be used
NotAllowedToLink: User is not allowed to link with external login provider
Password:
ConfirmationWrong: Passwordconfirmation is wrong
Empty: Password is empty
Invalid: Password is invalid
InvalidAndLocked: Password is invalid and user is locked, contact your administrator.
PasswordComplexityPolicy:
NotFound: Password policy not found
MinLength: Password is to short
HasLower: Password must contain lower letter
HasUpper: Password must contain upper letter
HasNumber: Password must contain number
HasSymbol: Password must contain symbol
Code:
Expired: Code is expired
Invalid: Code is invalid
Empty: Code is empty
CryptoCodeNil: Crypto code is nil
NotFound: Could not find code
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:
AlreadyReady: Multifactor OTP (OneTimePassword) is already setup
NotExisting: Multifactor OTP (OneTimePassword) doesn't exist
InvalidCode: Invalid code
NotReady: Multifactor OTP (OneTimePassword) isn't ready
Locked: User is locked
SomethingWentWrong: Something went wrong
NotActive: User is not active
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
NoExternalUserData: No external User Data received
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 organisation of the user must be granted to the project. Please contact your administrator.
IdentityProvider:
InvalidConfig: Identity Provider configuration is invalid
IAM:
LockoutPolicy:
NotExisting: Lockout Policy not existing
optional: (optional)

View File

@@ -0,0 +1,370 @@
Login:
Title: Bentornato!
Description: Inserisci i tuoi dati di accesso.
TitleLinking: Accesso per il collegamento degli utenti
DescriptionLinking: Inserisci i tuoi dati di accesso per collegare il tuo utente esterno con un utente ZITADEL.
LoginNameLabel: Nome di accesso
UsernamePlaceHolder: nome utente
LoginnamePlaceHolder: nomeutente@dominio
ExternalUserDescription: Accedi con un utente esterno.
MustBeMemberOfOrg: 'L''utente deve essere membro dell''organizzazione {{.OrgName}}.'
RegisterButtonText: registrare
NextButtonText: Avanti
SelectAccount:
Title: Seleziona l'account
Description: Usa il tuo account ZITADEL
TitleLinking: Seleziona l'account per il collegamento dell'utente
DescriptionLinking: Seleziona il tuo account da collegare al tuo utente esterno.
OtherUser: Altro utente
SessionState0: attivo
SessionState1: inattivo
MustBeMemberOfOrg: 'L''utente deve essere membro dell''organizzazione {{.OrgName}}.'
Password:
Title: Password
Description: Inserisci i tuoi dati di accesso.
PasswordLabel: Password
MinLength: Lunghezza minima
HasUppercase: Lettera maiuscola
HasLowercase: Lettera minuscola
HasNumber: Numero
HasSymbol: Simbolo
Confirmation: Conferma password
ResetLinkText: Password dimenticata?
BackButtonText: indietro
NextButtonText: Avanti
UsernameChange:
Title: Cambia nome utente
Description: Imposta il tuo nuovo nome utente
UsernameLabel: Nome utente
CancelButtonText: annulla
NextButtonText: Avanti
UsernameChangeDone:
Title: Nome utente cambiato
Description: Il tuo nome utente è stato cambiato con successo.
NextButtonText: Avanti
InitPassword:
Title: Impostare la password
Description: Hai ricevuto un codice, che devi inserire nel modulo sottostante, per impostare la tua nuova password.
CodeLabel: Codice
NewPasswordLabel: Nuova password
NewPasswordConfirmLabel: Conferma la password
ResendButtonText: rispedisci
NextButtonText: Avanti
InitPasswordDone:
Title: Set di password
Description: Password impostata con successo
NextButtonText: Avanti
CancelButtonText: annulla
InitUser:
Title: Attivare l'utente
Description: Hai ricevuto un codice, che devi inserire nel modulo sottostante, per verificare la tua email e impostare la tua nuova password.
CodeLabel: Codice
NewPasswordLabel: Nuova password
NewPasswordConfirmLabel: Conferma la password
NextButtonText: Avanti
ResendButtonText: rispedisci
InitUserDone:
Title: Utente attivato
Description: Email verificata e password impostata con successo
NextButtonText: Avanti
CancelButtonText: annulla
InitMFAPrompt:
Title: Configurazione a più fattori
Description: Vuoi impostare l'autenticazione a più fattori?
Provider0: OTP (One Time Password)
Provider1: U2F (2° fattore universale)
NextButtonText: Avanti
SkipButtonText: salta
InitMFAOTP:
Title: Verifica a più fattori
Description: Verifica il tuo multifattore.
OTPDescription: Scannerizza il codice con la tua app di autenticazione (ad esempio Google Authenticator) o copia la chiave segreta e inserisci il codice generato nel campo sottostante.
SecretLabel: Chiave
CodeLabel: Codice
NextButtonText: Avanti
CancelButtonText: annulla
InitMFAU2F:
Title: Configurazione a più fattori U2F / WebAuthN
Description: Aggiungi il tuo Token fornendo un nome e cliccando sul pulsante 'Registra'.
TokenNameLabel: Nome del token / dispositivo
NotSupported: WebAuthN non è supportato dal tuo browser. Assicurati che sia aggiornato o usane uno diverso (ad esempio Chrome, Safari, Firefox)
RegisterTokenButtonText: Registra
ErrorRetry: Riprova, crea una nuova richiesta o scegli un metodo diverso.
InitMFADone:
Title: Verificazione a più fattori effettuata
Description: La verificazione del multifattore eseguita con successo. Il multifattore è richiesto ad ogni login.
NextButtonText: Avanti
CancelButtonText: annulla
MFAProvider:
Provider0: OTP (One Time Password)
Provider1: U2F (2° fattore universale)
ChooseOther: o scegli un'altra opzione
VerifyMFAOTP:
Title: Verificazione del Multificator
Description: Verifica il tuo multifattore
CodeLabel: Codice
NextButtonText: Avanti
VerifyMFAU2F:
Title: Verificazione a più fattori
Description: Verifica il tuo token U2F / WebAuthN
NotSupported: WebAuthN non è supportato dal tuo browser. Assicurati di avere l'ultima versione installata o usane una diversa (per esempio Chrome, Safari, Firefox).
ErrorRetry: Prova di nuovo, crea una nuova richiesta o scegli un metodo diverso.
ValidateTokenButtonText: Verifica
Passwordless:
Title: Accesso senza password
Description: Verifica il tuo token
NotSupported: WebAuthN non è supportato dal tuo browser. Assicurati che sia aggiornato o usane uno diverso (ad esempio Chrome, Safari, Firefox)
ErrorRetry: Riprova, crea una nuova richiesta o scegli un metodo diverso.
LoginWithPwButtonText: Accedi con password
ValidateTokenButtonText: Verifica
PasswordlessPrompt:
Title: Autenticazione passwordless
Description: Vuoi impostare il login senza password?
DescriptionInit: Devi impostare il login senza password. Usa il link che ti è stato inviato per registrare il tuo dispositivo.
PasswordlessButtonText: Continua
NextButtonText: Avanti
SkipButtonText: salta
PasswordlessRegistration:
Title: Configurazione dell'autenticazione senza password
Description: Aggiungi il tuo Token fornendo un nome e poi cliccando sul pulsante 'Registra'.
TokenNameLabel: Nome del token / dispositivo
NotSupported: WebAuthN non è supportato dal tuo browser. Assicurati che sia aggiornato o usane uno diverso (ad esempio Chrome, Safari, Firefox)
RegisterTokenButtonText: Registra
ErrorRetry: Riprova, crea una nuova richiesta o scegli un metodo diverso.
PasswordlessRegistrationDone:
Title: Configurazione dell'autenticazione senza password
Description: Token per lautenticazione passwordless aggiunto con successo.
DescriptionClose: Ora puoi chiudere questa finestra.
NextButtonText: Avanti
CancelButtonText: annulla
PasswordChange:
Title: Reimposta password
Description: Cambia la tua password. Inserisci la tua vecchia e la nuova password.
OldPasswordLabel: Vecchia password
NewPasswordLabel: Nuova password
NewPasswordConfirmLabel: Conferma della password
CancelButtonText: annulla
NextButtonText: Avanti
PasswordChangeDone:
Title: Reimposta password
Description: La tua password è stata cambiata con successo.
NextButtonText: Avanti
PasswordResetDone:
Title: Link per il cambiamento inviato
Description: Controlla la tua email per reimpostare la tua password.
NextButtonText: Avanti
EmailVerification:
Title: Verifica email
Description: Ti abbiamo inviato un'e-mail per verificare il tuo indirizzo. Inserisci il codice nel campo sottostante.
CodeLabel: Codice
NextButtonText: Avanti
ResendButtonText: rispedisci
EmailVerificationDone:
Title: Verificazione email effettuata
Description: La tua email è stata verificata con successo.
NextButtonText: Avanti
CancelButtonText: annulla
LoginButtonText: Accedi
RegisterOption:
Title: Opzioni di registrazione
Description: Scegli come vuoi registrarti
RegisterUsernamePasswordButtonText: Con nome utente e password
ExternalLoginDescription: o registrarsi con un utente esterno
RegistrationUser:
Title: Registrazione
Description: Inserisci i tuoi dati utente. La tua email sarà usata come nome di accesso.
DescriptionOrgRegister: Inserisci i tuoi dati utente.
EmailLabel: email
UsernameLabel: Nome utente
FirstnameLabel: Nome
LastnameLabel: Cognome
LanguageLabel: Lingua
German: Deutsch
English: English
Italian: Italiano
GenderLabel: Genere
Female: Femminile
Male: Maschile
Diverse: diverso / X
PasswordLabel: Password
PasswordConfirmLabel: Conferma della password
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
TosConfirmAnd: e
PrivacyLinkText: l'informativa sulla privacy
ExternalLogin: o registrati con un utente esterno
BackButtonText: indietro
NextButtonText: Avanti
ExternalRegistrationUserOverview:
Title: Registrazione utente esterno
Description: Abbiamo preso i tuoi dati utente dal provider selezionato. Ora puoi cambiarli o completarli.
EmailLabel: E-mail
UsernameLabel: Nome utente
FirstnameLabel: Nome
LastnameLabel: Cognome
NicknameLabel: Soprannome
PhoneLabel: Numero di telefono
LanguageLabel: Lingua
German: Deutsch
English: English
Italian: Italiano
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
TosConfirmAnd: e
PrivacyLinkText: l'informativa sulla privacy
ExternalLogin: o registrati con un utente esterno
BackButtonText: indietro
NextButtonText: salva
RegistrationOrg:
Title: Registrazione dell'organizzazione
Description: Inserisci il tuo nome di organizzazione e i tuoi dati utente.
OrgNameLabel: Nome dell'organizzazione
EmailLabel: E-mail
UsernameLabel: Nome utente
FirstnameLabel: Nome
LastnameLabel: Cognome
PasswordLabel: Password
PasswordConfirmLabel: Conferma della password
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
TosConfirmAnd: e
PrivacyLinkText: l'informativa sulla privacy
SaveButtonText: Creare organizzazione
LoginSuccess:
Title: Accesso riuscito
AutoRedirectDescription: Sarai reindirizzato automaticamente alla tua applicazione. In caso contrario, clicca sul pulsante sottostante. Dopo puoi chiudere la finestra.
RedirectedDescription: Ora puoi chiudere la finestra.
NextButtonText: Avanti
LogoutDone:
Title: Disconnesso
Description: Ti sei disconnesso con successo.
LoginButtonText: Accedi
LinkingUsersDone:
Title: Collegamento utente
Description: Collegamento fatto.
CancelButtonText: annulla
NextButtonText: Avanti
ExternalNotFoundOption:
Title: Utente esterno
Description: Utente esterno non trovato. Vuoi collegare il tuo utente o registrarne uno nuovo automaticamente.
LinkButtonText: Link
AutoRegisterButtonText: Registra
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
TosConfirmAnd: e
PrivacyLinkText: l'informativa sulla privacy
German: Deutsch
English: English
Italian: Italiano
Footer:
PoweredBy: Alimentato da
Tos: Termini di servizio
PrivacyPolicy: l'informativa sulla privacy
Help: Aiuto
HelpLink: 'https://docs.zitadel.ch/docs/manuals/user-login'
Errors:
Internal: Si è verificato un errore interno
AuthRequest:
NotFound: Impossibile trovare authrequest
UserAgentNotCorresponding: User Agent non corrisponde
UserAgentNotFound: User Agent ID non trovato
TokenNotFound: Token non trovato
RequestTypeNotSupported: Il tipo di richiesta non è supportato
MissingParameters: Mancano i parametri richiesti
User:
NotFound: L'utente non è stato trovato
Inactive: L'utente è inattivo
NotFoundOnOrg: L'utente non è stato trovato nell'organizzazione scelta
NotAllowedOrg: L'utente non è membro dell'organizzazione richiesta
NotMatchingUserID: Utente e authrequest non corrispondono
UserIDMissing: UserID è vuoto
Invalid: I dati del utente non sono validi
DomainNotAllowedAsUsername: Il dominio è già riservato e non può essere utilizzato
NotAllowedToLink: L'utente non è autorizzato a collegarsi con un provider di accesso esterno
Password:
ConfirmationWrong: La conferma della password è sbagliata
Empty: La password è vuota
Invalid: La password non è valida
InvalidAndLocked: La password non è valida e l'utente è bloccato, contatta il tuo amministratore.
PasswordComplexityPolicy:
NotFound: Impostazioni della password non trovate
MinLength: La password è troppo corta
HasLower: La password deve contenere una lettera minuscola
HasUpper: La password deve contenere la lettera maiuscola
HasNumber: La password deve contenere un numero
HasSymbol: La password deve contenere il simbolo
Code:
Expired: Il codice è scaduto
Invalid: Il codice non è valido
Empty: Il codice è vuoto
CryptoCodeNil: Il codice criptato è null
NotFound: Impossibile trovare il codice
GeneratorAlgNotSupported: Algoritmo generatore non supportato
EmailVerify:
UserIDEmpty: UserID è vuoto
ExternalData:
CouldNotRead: I dati esterni non possono essere letti correttamente
MFA:
NoProviders: Nessun fornitore multifattore disponibile
OTP:
AlreadyReady: Multifactor OTP (OneTimePassword) è già impostato
NotExisting: Multifactor OTP (OneTimePassword) non esiste
InvalidCode: Codice non valido
NotReady: Multifattore OTP (OneTimePassword) non è pronto
Locked: L'utente è bloccato
SomethingWentWrong: Qualcosa è andato storto
NotActive: L'utente non è attivo
ExternalIDP:
IDPTypeNotImplemented: Il tipo di IDP non è implementato
NotAllowed: Provider di accesso esterno non consentito
IDPConfigIDEmpty: L'ID del fornitore di identità è vuoto
ExternalUserIDEmpty: L'ID utente esterno è vuoto
UserDisplayNameEmpty: Il nome visualizzato dell'utente è vuoto
NoExternalUserData: Nessun dato utente esterno ricevuto
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:
InvalidConfig: La configurazione dell'Identity Provider non è valida
IAM:
LockoutPolicy:
NotExisting: Impostazioni di blocco non esistenti
optional: (opzionale)

View File

@@ -0,0 +1,93 @@
Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato"
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,57 @@
@font-face {
font-family: 'lgn-icons';
src: url('../fonts/lgn-icons.eot?p68sys');
src: url('../fonts/lgn-icons.eot?p68sys#iefix') format('embedded-opentype'),
url('../fonts/lgn-icons.ttf?p68sys') format('truetype'),
url('../fonts/lgn-icons.woff?p68sys') format('woff'),
url('../fonts/lgn-icons.svg?p68sys#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
[class^="lgn-icon-"], [class*=" lgn-icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'lgn-icons' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.lgn-icon-check-solid:before {
content: "\e909";
}
.lgn-icon-times-solid:before {
content: "\e900";
}
.lgn-icon-user-plus-solid:before {
content: "\e901";
}
.lgn-icon-angle-left-solid:before {
content: "\e902";
}
.lgn-icon-angle-right-solid:before {
content: "\e903";
}
.lgn-icon-arrow-left-solid:before {
content: "\e904";
}
.lgn-icon-arrow-right-solid:before {
content: "\e905";
}
.lgn-icon-clipboard-check-solid:before {
content: "\e906";
}
.lgn-icon-clipboard:before {
content: "\e907";
}
.lgn-icon-exclamation-circle-solid:before {
content: "\e908";
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="times-solid" d="M231 775l-46-46 281-281-281-281 46-46 281 281 281-281 46 46-281 281 281 281-46 46-281-281z" />
<glyph unicode="&#xe901;" glyph-name="user-plus-solid" d="M384 896c-123.375 0-224-100.625-224-224 0-77.125 39.375-145.625 99-186-114.125-49-195-162.25-195-294h64c0 141.75 114.25 256 256 256 44 0 85-11.5 121-31-35.5-44-57-100.25-57-161 0-141 115-256 256-256s256 115 256 256c0 141-115 256-256 256-55.875 0-107.875-18.375-150-49-14.125 8.875-29.5 16.375-45 23 59.625 40.375 99 108.875 99 186 0 123.375-100.625 224-224 224zM384 832c88.75 0 160-71.25 160-160s-71.25-160-160-160c-88.75 0-160 71.25-160 160s71.25 160 160 160zM704 448c106.375 0 192-85.625 192-192s-85.625-192-192-192c-106.375 0-192 85.625-192 192s85.625 192 192 192zM672 384v-96h-96v-64h96v-96h64v96h96v64h-96v96z" />
<glyph unicode="&#xe902;" glyph-name="angle-left-solid" d="M609 823l-352-352-22-23 22-23 352-352 46 46-329 329 329 329z" />
<glyph unicode="&#xe903;" glyph-name="angle-right-solid" d="M415 823l-46-46 329-329-329-329 46-46 352 352 22 23-22 23z" />
<glyph unicode="&#xe904;" glyph-name="arrow-left-solid" d="M425 743l-272-272-22-23 22-23 272-272 46 46-217 217h642v64h-642l217 217z" />
<glyph unicode="&#xe905;" glyph-name="arrow-right-solid" d="M599 743l-46-46 217-217h-642v-64h642l-217-217 46-46 272 272 22 23-22 23z" />
<glyph unicode="&#xe906;" glyph-name="clipboard-check-solid" d="M512 896c-40.25 0-68.875-28.5-83-64h-269v-800h704v800h-269c-14.125 35.5-42.75 64-83 64zM512 832c17.75 0 32-14.25 32-32v-32h96v-64h-256v64h96v32c0 17.75 14.25 32 32 32zM224 768h96v-128h384v128h96v-672h-576zM681 535l-201-201-105 105-46-46 128-128 23-22 23 22 224 224z" />
<glyph unicode="&#xe907;" glyph-name="clipboard" d="M512 864c-40.25 0-68.875-28.5-83-64h-237v-736h640v736h-237c-14.125 35.5-42.75 64-83 64zM512 800c17.75 0 32-14.25 32-32v-32h96v-64h-256v64h96v32c0 17.75 14.25 32 32 32zM256 736h64v-128h384v128h64v-608h-512z" />
<glyph unicode="&#xe908;" glyph-name="exclamation-circle-solid" d="M512 832c-211.75 0-384-172.25-384-384s172.25-384 384-384c211.75 0 384 172.25 384 384s-172.25 384-384 384zM512 768c177.125 0 320-142.875 320-320s-142.875-320-320-320c-177.125 0-320 142.875-320 320s142.875 320 320 320zM480 640v-256h64v256zM480 320v-64h64v64z" />
<glyph unicode="&#xe909;" glyph-name="check-solid" d="M905 759l-553-553-233 233-46-46 256-256 23-22 23 22 576 576z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 19.03125 4.28125 L 8.03125 15.28125 L 7.34375 16 L 8.03125 16.71875 L 19.03125 27.71875 L 20.46875 26.28125 L 10.1875 16 L 20.46875 5.71875 Z"/></svg>

After

Width:  |  Height:  |  Size: 221 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 12.96875 4.28125 L 11.53125 5.71875 L 21.8125 16 L 11.53125 26.28125 L 12.96875 27.71875 L 23.96875 16.71875 L 24.65625 16 L 23.96875 15.28125 Z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 13.28125 6.78125 L 4.78125 15.28125 L 4.09375 16 L 4.78125 16.71875 L 13.28125 25.21875 L 14.71875 23.78125 L 7.9375 17 L 28 17 L 28 15 L 7.9375 15 L 14.71875 8.21875 Z"/></svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 18.71875 6.78125 L 17.28125 8.21875 L 24.0625 15 L 4 15 L 4 17 L 24.0625 17 L 17.28125 23.78125 L 18.71875 25.21875 L 27.21875 16.71875 L 27.90625 16 L 27.21875 15.28125 Z"/></svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 28.28125 6.28125 L 11 23.5625 L 3.71875 16.28125 L 2.28125 17.71875 L 10.28125 25.71875 L 11 26.40625 L 11.71875 25.71875 L 29.71875 7.71875 Z"/></svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 16 2 C 14.742188 2 13.847656 2.890625 13.40625 4 L 5 4 L 5 29 L 27 29 L 27 4 L 18.59375 4 C 18.152344 2.890625 17.257813 2 16 2 Z M 16 4 C 16.554688 4 17 4.445313 17 5 L 17 6 L 20 6 L 20 8 L 12 8 L 12 6 L 15 6 L 15 5 C 15 4.445313 15.445313 4 16 4 Z M 7 6 L 10 6 L 10 10 L 22 10 L 22 6 L 25 6 L 25 27 L 7 27 Z M 21.28125 13.28125 L 15 19.5625 L 11.71875 16.28125 L 10.28125 17.71875 L 14.28125 21.71875 L 15 22.40625 L 15.71875 21.71875 L 22.71875 14.71875 Z"/></svg>

After

Width:  |  Height:  |  Size: 538 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 15 3 C 13.742188 3 12.847656 3.890625 12.40625 5 L 5 5 L 5 28 L 13 28 L 13 30 L 27 30 L 27 14 L 25 14 L 25 5 L 17.59375 5 C 17.152344 3.890625 16.257813 3 15 3 Z M 15 5 C 15.554688 5 16 5.445313 16 6 L 16 7 L 19 7 L 19 9 L 11 9 L 11 7 L 14 7 L 14 6 C 14 5.445313 14.445313 5 15 5 Z M 7 7 L 9 7 L 9 11 L 21 11 L 21 7 L 23 7 L 23 14 L 13 14 L 13 26 L 7 26 Z M 15 16 L 25 16 L 25 28 L 15 28 Z"/></svg>

After

Width:  |  Height:  |  Size: 469 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 16 4 C 9.382813 4 4 9.382813 4 16 C 4 22.617188 9.382813 28 16 28 C 22.617188 28 28 22.617188 28 16 C 28 9.382813 22.617188 4 16 4 Z M 16 6 C 21.535156 6 26 10.464844 26 16 C 26 21.535156 21.535156 26 16 26 C 10.464844 26 6 21.535156 6 16 C 6 10.464844 10.464844 6 16 6 Z M 15 10 L 15 18 L 17 18 L 17 10 Z M 15 20 L 15 22 L 17 22 L 17 20 Z"/></svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 7.21875 5.78125 L 5.78125 7.21875 L 14.5625 16 L 5.78125 24.78125 L 7.21875 26.21875 L 16 17.4375 L 24.78125 26.21875 L 26.21875 24.78125 L 17.4375 16 L 26.21875 7.21875 L 24.78125 5.78125 L 16 14.5625 Z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M 12 2 C 8.144531 2 5 5.144531 5 9 C 5 11.410156 6.230469 13.550781 8.09375 14.8125 C 4.527344 16.34375 2 19.882813 2 24 L 4 24 C 4 19.570313 7.570313 16 12 16 C 13.375 16 14.65625 16.359375 15.78125 16.96875 C 14.671875 18.34375 14 20.101563 14 22 C 14 26.40625 17.59375 30 22 30 C 26.40625 30 30 26.40625 30 22 C 30 17.59375 26.40625 14 22 14 C 20.253906 14 18.628906 14.574219 17.3125 15.53125 C 16.871094 15.253906 16.390625 15.019531 15.90625 14.8125 C 17.769531 13.550781 19 11.410156 19 9 C 19 5.144531 15.855469 2 12 2 Z M 12 4 C 14.773438 4 17 6.226563 17 9 C 17 11.773438 14.773438 14 12 14 C 9.226563 14 7 11.773438 7 9 C 7 6.226563 9.226563 4 12 4 Z M 22 16 C 25.324219 16 28 18.675781 28 22 C 28 25.324219 25.324219 28 22 28 C 18.675781 28 16 25.324219 16 22 C 16 18.675781 18.675781 16 22 16 Z M 21 18 L 21 21 L 18 21 L 18 23 L 21 23 L 21 26 L 23 26 L 23 23 L 26 23 L 26 21 L 23 21 L 23 18 Z"/></svg>

After

Width:  |  Height:  |  Size: 983 B

Some files were not shown because too many files have changed in this diff Show More