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>
129
internal/api/ui/console/console.go
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
36
internal/api/ui/login/auth_request.go
Normal 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)
|
||||
}
|
72
internal/api/ui/login/change_password_handler.go
Normal 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)
|
||||
}
|
87
internal/api/ui/login/custom_action.go
Normal 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
|
||||
}
|
485
internal/api/ui/login/external_login_handler.go
Normal 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
|
||||
}
|
324
internal/api/ui/login/external_register_handler.go
Normal 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
|
||||
}
|
18
internal/api/ui/login/health_handler.go
Normal 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"))
|
||||
}
|
142
internal/api/ui/login/init_password_handler.go
Normal 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)
|
||||
}
|
132
internal/api/ui/login/init_user_handler.go
Normal 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)
|
||||
}
|
270
internal/api/ui/login/jwt_handler.go
Normal 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
|
||||
}
|
24
internal/api/ui/login/link_users_handler.go
Normal 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)
|
||||
}
|
152
internal/api/ui/login/login.go
Normal 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)
|
||||
}
|
86
internal/api/ui/login/login_handler.go
Normal 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)
|
||||
}
|
54
internal/api/ui/login/login_success_handler.go
Normal 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)
|
||||
}
|
18
internal/api/ui/login/logout_handler.go
Normal 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)
|
||||
}
|
96
internal/api/ui/login/mail_verify_handler.go
Normal 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)
|
||||
}
|
22
internal/api/ui/login/mfa_init_done_handler.go
Normal 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)
|
||||
}
|
64
internal/api/ui/login/mfa_init_u2f.go
Normal 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)
|
||||
}
|
100
internal/api/ui/login/mfa_init_verify_handler.go
Normal 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
|
||||
}
|
105
internal/api/ui/login/mfa_prompt_handler.go
Normal 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)
|
||||
}
|
88
internal/api/ui/login/mfa_verify_handler.go
Normal 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
|
||||
}
|
79
internal/api/ui/login/mfa_verify_u2f_handler.go
Normal 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)
|
||||
}
|
74
internal/api/ui/login/password_complexity_policy_handler.go
Normal 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
|
||||
}
|
50
internal/api/ui/login/password_handler.go
Normal 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)
|
||||
}
|
41
internal/api/ui/login/password_reset_handler.go
Normal 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)
|
||||
}
|
75
internal/api/ui/login/passwordless_login_handler.go
Normal 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)
|
||||
}
|
40
internal/api/ui/login/passwordless_prompt_handler.go
Normal 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)
|
||||
}
|
199
internal/api/ui/login/passwordless_registration_handler.go
Normal 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)
|
||||
}
|
23
internal/api/ui/login/policy_handler.go
Normal 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)
|
||||
}
|
185
internal/api/ui/login/register_handler.go
Normal 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,
|
||||
},
|
||||
}
|
||||
}
|
60
internal/api/ui/login/register_option_handler.go
Normal 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)
|
||||
}
|
142
internal/api/ui/login/register_org_handler.go
Normal 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,
|
||||
}
|
||||
}
|
594
internal/api/ui/login/renderer.go
Normal 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
|
||||
}
|
97
internal/api/ui/login/resources_handler.go
Normal 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
|
||||
}
|
98
internal/api/ui/login/router.go
Normal 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
|
||||
}
|
47
internal/api/ui/login/select_user_handler.go
Normal 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)
|
||||
}
|
369
internal/api/ui/login/static/i18n/de.yaml
Normal 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)
|
370
internal/api/ui/login/static/i18n/en.yaml
Normal 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)
|
370
internal/api/ui/login/static/i18n/it.yaml
Normal 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)
|
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Black.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-BlackItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Bold.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-BoldItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Italic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Light.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-LightItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Regular.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Thin.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-ThinItalic.ttf
Executable file
93
internal/api/ui/login/static/resources/fonts/lato/OFL.txt
Executable 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.
|
@@ -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";
|
||||
}
|
@@ -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=" " horiz-adv-x="512" d="" />
|
||||
<glyph unicode="" 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="" 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="" glyph-name="angle-left-solid" d="M609 823l-352-352-22-23 22-23 352-352 46 46-329 329 329 329z" />
|
||||
<glyph unicode="" glyph-name="angle-right-solid" d="M415 823l-46-46 329-329-329-329 46-46 352 352 22 23-22 23z" />
|
||||
<glyph unicode="" glyph-name="arrow-left-solid" d="M425 743l-272-272-22-23 22-23 272-272 46 46-217 217h642v64h-642l217 217z" />
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |