mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:07:31 +00:00
feat: run on a single port (#3163)
* start v2 * start * run * some cleanup * remove v2 pkg again * simplify * webauthn * remove unused config * fix login path in Dockerfile * fix asset_generator.go * health handler * fix grpc web * refactor * merge * build new main.go * run new main.go * update logging pkg * fix error msg * update logging * cleanup * cleanup * go mod tidy * change localDevMode * fix customEndpoints * update logging * comments * change local flag to external configs * fix location generated go code * fix Co-authored-by: fforootd <florian@caos.ch>
This commit is contained in:
@@ -3,100 +3,101 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/logging"
|
||||
sentryhttp "github.com/getsentry/sentry-go/http"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
admin_es "github.com/caos/zitadel/internal/admin/repository/eventsourcing"
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
grpc_util "github.com/caos/zitadel/internal/api/grpc"
|
||||
internal_authz "github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/api/grpc/server"
|
||||
http_util "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/api/oidc"
|
||||
auth_es "github.com/caos/zitadel/internal/auth/repository/eventsourcing"
|
||||
authz_repo "github.com/caos/zitadel/internal/authz/repository"
|
||||
"github.com/caos/zitadel/internal/authz/repository"
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/domain"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/query"
|
||||
"github.com/caos/zitadel/internal/telemetry/metrics"
|
||||
"github.com/caos/zitadel/internal/telemetry/metrics/otel"
|
||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||
view_model "github.com/caos/zitadel/internal/view/model"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
GRPC grpc_util.Config
|
||||
OIDC oidc.OPHandlerConfig
|
||||
Domain string
|
||||
}
|
||||
|
||||
type API struct {
|
||||
port uint16
|
||||
grpcServer *grpc.Server
|
||||
gatewayHandler *server.GatewayHandler
|
||||
verifier *authz.TokenVerifier
|
||||
serverPort string
|
||||
verifier *internal_authz.TokenVerifier
|
||||
health health
|
||||
auth auth
|
||||
admin admin
|
||||
router *mux.Router
|
||||
externalSecure bool
|
||||
}
|
||||
|
||||
type health interface {
|
||||
Health(ctx context.Context) error
|
||||
IAMByID(ctx context.Context, id string) (*query.IAM, error)
|
||||
VerifierClientID(ctx context.Context, appName string) (string, string, error)
|
||||
}
|
||||
|
||||
type auth interface {
|
||||
ActiveUserSessionCount() int64
|
||||
}
|
||||
|
||||
type admin interface {
|
||||
GetViews() ([]*view_model.View, error)
|
||||
GetSpoolerDiv(database, viewName string) int64
|
||||
}
|
||||
|
||||
func Create(config Config, authZ authz.Config, q *query.Queries, authZRepo authz_repo.Repository, authRepo *auth_es.EsRepository, adminRepo *admin_es.EsRepository, sd systemdefaults.SystemDefaults) *API {
|
||||
func New(
|
||||
port uint16,
|
||||
router *mux.Router,
|
||||
repo *struct {
|
||||
repository.Repository
|
||||
*query.Queries
|
||||
},
|
||||
authZ internal_authz.Config,
|
||||
sd systemdefaults.SystemDefaults,
|
||||
externalSecure bool,
|
||||
) *API {
|
||||
verifier := internal_authz.Start(repo)
|
||||
api := &API{
|
||||
serverPort: config.GRPC.ServerPort,
|
||||
port: port,
|
||||
verifier: verifier,
|
||||
health: repo,
|
||||
router: router,
|
||||
externalSecure: externalSecure,
|
||||
}
|
||||
|
||||
repo := struct {
|
||||
authz_repo.Repository
|
||||
query.Queries
|
||||
}{
|
||||
authZRepo,
|
||||
*q,
|
||||
}
|
||||
|
||||
api.verifier = authz.Start(&repo)
|
||||
api.health = &repo
|
||||
api.auth = authRepo
|
||||
api.admin = adminRepo
|
||||
api.grpcServer = server.CreateServer(api.verifier, authZ, sd.DefaultLanguage)
|
||||
api.gatewayHandler = server.CreateGatewayHandler(config.GRPC)
|
||||
api.RegisterHandler("", api.healthHandler())
|
||||
api.routeGRPC()
|
||||
|
||||
api.RegisterHandler("/debug", api.healthHandler())
|
||||
|
||||
return api
|
||||
}
|
||||
|
||||
func (a *API) RegisterServer(ctx context.Context, server server.Server) {
|
||||
server.RegisterServer(a.grpcServer)
|
||||
a.gatewayHandler.RegisterGateway(ctx, server)
|
||||
a.verifier.RegisterServer(server.AppName(), server.MethodPrefix(), server.AuthMethods())
|
||||
func (a *API) RegisterServer(ctx context.Context, grpcServer server.Server) error {
|
||||
grpcServer.RegisterServer(a.grpcServer)
|
||||
handler, prefix, err := server.CreateGateway(ctx, grpcServer, a.port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.RegisterHandler(prefix, handler)
|
||||
a.verifier.RegisterServer(grpcServer.AppName(), grpcServer.MethodPrefix(), grpcServer.AuthMethods())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *API) RegisterHandler(prefix string, handler http.Handler) {
|
||||
prefix = strings.TrimSuffix(prefix, "/")
|
||||
sentryHandler := sentryhttp.New(sentryhttp.Options{})
|
||||
a.gatewayHandler.RegisterHandler(prefix, sentryHandler.Handle(handler))
|
||||
subRouter := a.router.PathPrefix(prefix).Subrouter()
|
||||
subRouter.PathPrefix("/").Handler(http.StripPrefix(prefix, sentryHandler.Handle(handler)))
|
||||
}
|
||||
|
||||
func (a *API) Start(ctx context.Context) {
|
||||
server.Serve(ctx, a.grpcServer, a.serverPort)
|
||||
a.gatewayHandler.Serve(ctx)
|
||||
func (a *API) routeGRPC() {
|
||||
http2Route := a.router.Methods(http.MethodPost).
|
||||
MatcherFunc(func(r *http.Request, _ *mux.RouteMatch) bool {
|
||||
return r.ProtoMajor == 2
|
||||
}).
|
||||
Subrouter()
|
||||
http2Route.Headers("Content-Type", "application/grpc").Handler(a.grpcServer)
|
||||
|
||||
if !a.externalSecure {
|
||||
a.routeGRPCWeb(a.router)
|
||||
return
|
||||
}
|
||||
a.routeGRPCWeb(http2Route)
|
||||
}
|
||||
|
||||
func (a *API) routeGRPCWeb(router *mux.Router) {
|
||||
router.NewRoute().HeadersRegexp("Content-Type", "application/grpc-web.*").Handler(grpcweb.WrapServer(a.grpcServer))
|
||||
}
|
||||
|
||||
func (a *API) healthHandler() http.Handler {
|
||||
@@ -125,99 +126,46 @@ func (a *API) healthHandler() http.Handler {
|
||||
handler.HandleFunc("/healthz", handleHealth)
|
||||
handler.HandleFunc("/ready", handleReadiness(checks))
|
||||
handler.HandleFunc("/validate", handleValidate(checks))
|
||||
handler.HandleFunc("/clientID", a.handleClientID)
|
||||
handler.Handle("/metrics", a.handleMetrics())
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := w.Write([]byte("ok"))
|
||||
logging.Log("API-Hfss2").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(r.Context())).Error("error writing ok for health")
|
||||
logging.WithFields("traceID", tracing.TraceIDFromCtx(r.Context())).OnError(err).Error("error writing ok for health")
|
||||
}
|
||||
|
||||
func handleReadiness(checks []ValidationFunction) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
errors := validate(r.Context(), checks)
|
||||
if len(errors) == 0 {
|
||||
errs := validate(r.Context(), checks)
|
||||
if len(errs) == 0 {
|
||||
http_util.MarshalJSON(w, "ok", nil, http.StatusOK)
|
||||
return
|
||||
}
|
||||
http_util.MarshalJSON(w, nil, errors[0], http.StatusPreconditionFailed)
|
||||
http_util.MarshalJSON(w, nil, errs[0], http.StatusPreconditionFailed)
|
||||
}
|
||||
}
|
||||
|
||||
func handleValidate(checks []ValidationFunction) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
errors := validate(r.Context(), checks)
|
||||
if len(errors) == 0 {
|
||||
errs := validate(r.Context(), checks)
|
||||
if len(errs) == 0 {
|
||||
http_util.MarshalJSON(w, "ok", nil, http.StatusOK)
|
||||
return
|
||||
}
|
||||
http_util.MarshalJSON(w, errors, nil, http.StatusOK)
|
||||
http_util.MarshalJSON(w, errs, nil, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *API) handleClientID(w http.ResponseWriter, r *http.Request) {
|
||||
id, _, err := a.health.VerifierClientID(r.Context(), "Zitadel Console")
|
||||
if err != nil {
|
||||
http_util.MarshalJSON(w, nil, err, http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
http_util.MarshalJSON(w, id, nil, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a *API) handleMetrics() http.Handler {
|
||||
a.registerActiveSessionCounters()
|
||||
a.registerSpoolerDivCounters()
|
||||
return metrics.GetExporter()
|
||||
}
|
||||
|
||||
func (a *API) registerActiveSessionCounters() {
|
||||
metrics.RegisterValueObserver(
|
||||
metrics.ActiveSessionCounter,
|
||||
metrics.ActiveSessionCounterDescription,
|
||||
func(ctx context.Context, result metric.Int64ObserverResult) {
|
||||
result.Observe(
|
||||
a.auth.ActiveUserSessionCount(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (a *API) registerSpoolerDivCounters() {
|
||||
views, err := a.admin.GetViews()
|
||||
if err != nil {
|
||||
logging.Log("API-3M8sd").WithError(err).Error("could not read views for metrics")
|
||||
return
|
||||
}
|
||||
metrics.RegisterValueObserver(
|
||||
metrics.SpoolerDivCounter,
|
||||
metrics.SpoolerDivCounterDescription,
|
||||
func(ctx context.Context, result metric.Int64ObserverResult) {
|
||||
for _, view := range views {
|
||||
labels := map[string]attribute.Value{
|
||||
metrics.Database: attribute.StringValue(view.Database),
|
||||
metrics.ViewName: attribute.StringValue(view.ViewName),
|
||||
}
|
||||
result.Observe(
|
||||
a.admin.GetSpoolerDiv(view.Database, view.ViewName),
|
||||
otel.MapToKeyValue(labels)...,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
type ValidationFunction func(ctx context.Context) error
|
||||
|
||||
func validate(ctx context.Context, validations []ValidationFunction) []error {
|
||||
errors := make([]error, 0)
|
||||
errs := make([]error, 0)
|
||||
for _, validation := range validations {
|
||||
if err := validation(ctx); err != nil {
|
||||
logging.Log("API-vf823").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Error("validation failed")
|
||||
errors = append(errors, err)
|
||||
logging.WithFields("traceID", tracing.TraceIDFromCtx(ctx)).WithError(err).Error("validation failed")
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return errors
|
||||
return errs
|
||||
}
|
||||
|
@@ -21,6 +21,10 @@ import (
|
||||
"github.com/caos/zitadel/internal/static"
|
||||
)
|
||||
|
||||
const (
|
||||
HandlerPrefix = "/assets/v1"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
errorHandler ErrorHandler
|
||||
storage static.Storage
|
||||
@@ -66,14 +70,7 @@ func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, code
|
||||
http.Error(w, err.Error(), code)
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
commands *command.Commands,
|
||||
verifier *authz.TokenVerifier,
|
||||
authConfig authz.Config,
|
||||
idGenerator id.Generator,
|
||||
storage static.Storage,
|
||||
queries *query.Queries,
|
||||
) http.Handler {
|
||||
func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries) http.Handler {
|
||||
h := &Handler{
|
||||
commands: commands,
|
||||
errorHandler: DefaultErrorHandler,
|
||||
|
@@ -12,20 +12,21 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
directory = flag.String("directory", "./", "working directory: asset.yaml must be in this directory, files will be generated into parent directory")
|
||||
assets = flag.String("assets", "../../../../docs/docs/apis/assets/assets.md", "path where the assets.md will be generated")
|
||||
directory = flag.String("directory", "./", "working directory: asset.yaml must be in this directory, files will be generated into parent directory")
|
||||
assetsDocs = flag.String("assets", "../../../../docs/docs/apis/assets/assets.md", "path where the assets.md will be generated")
|
||||
assetPrefix = flag.String("handler-prefix", "/assets/v1", "prefix of the handler paths")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
configFile := *directory + "asset.yaml"
|
||||
authz, err := os.OpenFile(*directory+"../authz.go", os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logging.Log("ASSETS-Gn31f").OnError(err).Fatal("cannot open authz file")
|
||||
logging.OnError(err).Fatal("cannot open authz file")
|
||||
router, err := os.OpenFile(*directory+"../router.go", os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logging.Log("ASSETS-ABen3").OnError(err).Fatal("cannot open router file")
|
||||
docs, err := os.OpenFile(*assets, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logging.Log("ASSETS-Dfvsd").OnError(err).Fatal("cannot open docs file")
|
||||
GenerateAssetHandler(configFile, authz, router, docs)
|
||||
logging.OnError(err).Fatal("cannot open router file")
|
||||
docs, err := os.OpenFile(*assetsDocs, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0755)
|
||||
logging.OnError(err).Fatal("cannot open docs file")
|
||||
GenerateAssetHandler(configFile, *assetPrefix, authz, router, docs)
|
||||
}
|
||||
|
||||
type Method struct {
|
||||
@@ -97,7 +98,7 @@ type Service struct {
|
||||
Methods map[string]Method
|
||||
}
|
||||
|
||||
func GenerateAssetHandler(configFilePath string, authz, router, docs io.Writer) {
|
||||
func GenerateAssetHandler(configFilePath, handlerPrefix string, authz, router, docs io.Writer) {
|
||||
conf := new(struct {
|
||||
Services Services
|
||||
})
|
||||
@@ -117,7 +118,7 @@ func GenerateAssetHandler(configFilePath string, authz, router, docs io.Writer)
|
||||
}{
|
||||
GoPkgName: "assets",
|
||||
Name: "AssetsService",
|
||||
Prefix: "/assets/v1",
|
||||
Prefix: handlerPrefix,
|
||||
Services: conf.Services,
|
||||
}
|
||||
err = tmplAuthz.Execute(authz, data)
|
||||
|
@@ -2,52 +2,51 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/logging"
|
||||
grpc_util "github.com/caos/zitadel/internal/api/grpc"
|
||||
client_middleware "github.com/caos/zitadel/internal/api/grpc/client/middleware"
|
||||
http_util "github.com/caos/zitadel/internal/api/http"
|
||||
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
|
||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
|
||||
client_middleware "github.com/caos/zitadel/internal/api/grpc/client/middleware"
|
||||
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultGatewayPort = "8080"
|
||||
mimeWildcard = "*/*"
|
||||
mimeWildcard = "*/*"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultJSONMarshaler = &runtime.JSONPb{
|
||||
customHeaders = []string{
|
||||
"x-zitadel-",
|
||||
}
|
||||
jsonMarshaler = &runtime.JSONPb{
|
||||
UnmarshalOptions: protojson.UnmarshalOptions{
|
||||
DiscardUnknown: true,
|
||||
},
|
||||
}
|
||||
|
||||
DefaultServeMuxOptions = func(customHeaders ...string) []runtime.ServeMuxOption {
|
||||
return []runtime.ServeMuxOption{
|
||||
runtime.WithMarshalerOption(DefaultJSONMarshaler.ContentType(nil), DefaultJSONMarshaler),
|
||||
runtime.WithMarshalerOption(mimeWildcard, DefaultJSONMarshaler),
|
||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, DefaultJSONMarshaler),
|
||||
runtime.WithIncomingHeaderMatcher(DefaultHeaderMatcher(customHeaders...)),
|
||||
runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher),
|
||||
}
|
||||
serveMuxOptions = []runtime.ServeMuxOption{
|
||||
runtime.WithMarshalerOption(jsonMarshaler.ContentType(nil), jsonMarshaler),
|
||||
runtime.WithMarshalerOption(mimeWildcard, jsonMarshaler),
|
||||
runtime.WithMarshalerOption(runtime.MIMEWildcard, jsonMarshaler),
|
||||
runtime.WithIncomingHeaderMatcher(headerMatcher),
|
||||
runtime.WithOutgoingHeaderMatcher(runtime.DefaultHeaderMatcher),
|
||||
}
|
||||
|
||||
DefaultHeaderMatcher = func(customHeaders ...string) runtime.HeaderMatcherFunc {
|
||||
return func(header string) (string, bool) {
|
||||
headerMatcher = runtime.HeaderMatcherFunc(
|
||||
func(header string) (string, bool) {
|
||||
for _, customHeader := range customHeaders {
|
||||
if strings.HasPrefix(strings.ToLower(header), customHeader) {
|
||||
return header, true
|
||||
}
|
||||
}
|
||||
return runtime.DefaultHeaderMatcher(header)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
type Gateway interface {
|
||||
@@ -57,92 +56,21 @@ type Gateway interface {
|
||||
|
||||
type GatewayFunc func(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) error
|
||||
|
||||
//optional extending interfaces of Gateway below
|
||||
|
||||
type gatewayCustomServeMuxOptions interface {
|
||||
GatewayServeMuxOptions() []runtime.ServeMuxOption
|
||||
}
|
||||
|
||||
type grpcGatewayCustomInterceptor interface {
|
||||
GatewayHTTPInterceptor(http.Handler) http.Handler
|
||||
}
|
||||
|
||||
type gatewayCustomCallOptions interface {
|
||||
GatewayCallOptions() []grpc.DialOption
|
||||
}
|
||||
|
||||
type GatewayHandler struct {
|
||||
mux *http.ServeMux
|
||||
serverPort string
|
||||
gatewayPort string
|
||||
customHeaders []string
|
||||
}
|
||||
|
||||
func CreateGatewayHandler(config grpc_util.Config) *GatewayHandler {
|
||||
return &GatewayHandler{
|
||||
mux: http.NewServeMux(),
|
||||
serverPort: config.ServerPort,
|
||||
gatewayPort: config.GatewayPort,
|
||||
customHeaders: config.CustomHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
//RegisterGateway registers a handler (Gateway interface) on defined port
|
||||
//Gateway interface may be extended with optional implementation of interfaces (gatewayCustomServeMuxOptions, ...)
|
||||
func (g *GatewayHandler) RegisterGateway(ctx context.Context, gateway Gateway) {
|
||||
handler := createGateway(ctx, gateway, g.serverPort, g.customHeaders...)
|
||||
prefix := gateway.GatewayPathPrefix()
|
||||
g.RegisterHandler(prefix, handler)
|
||||
}
|
||||
|
||||
func (g *GatewayHandler) RegisterHandler(prefix string, handler http.Handler) {
|
||||
http_util.RegisterHandler(g.mux, prefix, handler)
|
||||
}
|
||||
|
||||
func (g *GatewayHandler) Serve(ctx context.Context) {
|
||||
http_util.Serve(ctx, g.mux, g.gatewayPort, "api")
|
||||
}
|
||||
|
||||
func createGateway(ctx context.Context, g Gateway, port string, customHeaders ...string) http.Handler {
|
||||
mux := createMux(g, customHeaders...)
|
||||
opts := createDialOptions(g)
|
||||
err := g.RegisterGateway()(ctx, mux, "localhost"+http_util.Endpoint(port), opts)
|
||||
logging.Log("SERVE-7B7G0E").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("failed to register grpc gateway")
|
||||
return addInterceptors(mux, g)
|
||||
}
|
||||
|
||||
func createMux(g Gateway, customHeaders ...string) *runtime.ServeMux {
|
||||
muxOptions := DefaultServeMuxOptions(customHeaders...)
|
||||
if customOpts, ok := g.(gatewayCustomServeMuxOptions); ok {
|
||||
muxOptions = customOpts.GatewayServeMuxOptions()
|
||||
}
|
||||
return runtime.NewServeMux(muxOptions...)
|
||||
}
|
||||
|
||||
func createDialOptions(g Gateway) []grpc.DialOption {
|
||||
func CreateGateway(ctx context.Context, g Gateway, port uint16) (http.Handler, string, error) {
|
||||
runtimeMux := runtime.NewServeMux(serveMuxOptions...)
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithInsecure(),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithUnaryInterceptor(client_middleware.DefaultTracingClient()),
|
||||
}
|
||||
|
||||
if customOpts, ok := g.(gatewayCustomCallOptions); ok {
|
||||
opts = append(opts, customOpts.GatewayCallOptions()...)
|
||||
err := g.RegisterGateway()(ctx, runtimeMux, fmt.Sprintf("localhost:%d", port), opts)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to register grpc gateway: %w", err)
|
||||
}
|
||||
return opts
|
||||
return addInterceptors(runtimeMux), g.GatewayPathPrefix(), nil
|
||||
}
|
||||
|
||||
func addInterceptors(handler http.Handler, g Gateway) http.Handler {
|
||||
func addInterceptors(handler http.Handler) http.Handler {
|
||||
handler = http_mw.DefaultMetricsHandler(handler)
|
||||
handler = http_mw.DefaultTelemetryHandler(handler)
|
||||
if interceptor, ok := g.(grpcGatewayCustomInterceptor); ok {
|
||||
handler = interceptor.GatewayHTTPInterceptor(handler)
|
||||
}
|
||||
return http_mw.CORSInterceptor(handler)
|
||||
}
|
||||
|
||||
func gatewayPort(port string) string {
|
||||
if port == "" {
|
||||
return defaultGatewayPort
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
@@ -1,23 +1,15 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
grpc_api "github.com/caos/zitadel/internal/api/grpc"
|
||||
"github.com/caos/zitadel/internal/telemetry/metrics"
|
||||
|
||||
"github.com/caos/logging"
|
||||
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/authz"
|
||||
"github.com/caos/zitadel/internal/api/grpc/server/middleware"
|
||||
"github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultGrpcPort = "80"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
@@ -46,24 +38,3 @@ func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, lang l
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func Serve(ctx context.Context, server *grpc.Server, port string) {
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
server.GracefulStop()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
listener := http.CreateListener(port)
|
||||
err := server.Serve(listener)
|
||||
logging.Log("SERVE-Ga3e94").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("grpc server serve failed")
|
||||
}()
|
||||
logging.LogWithFields("SERVE-bZ44QM", "port", port).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Info("grpc server is listening")
|
||||
}
|
||||
|
||||
func grpcPort(port string) string {
|
||||
if port == "" {
|
||||
return defaultGrpcPort
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
@@ -1,21 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/caos/logging"
|
||||
)
|
||||
|
||||
func CreateListener(endpoint string) net.Listener {
|
||||
l, err := net.Listen("tcp", Endpoint(endpoint))
|
||||
logging.Log("SERVE-6vasef").OnError(err).Fatal("creating listener failed")
|
||||
return l
|
||||
}
|
||||
|
||||
func Endpoint(endpoint string) string {
|
||||
if strings.Contains(endpoint, ":") {
|
||||
return endpoint
|
||||
}
|
||||
return ":" + endpoint
|
||||
}
|
@@ -8,7 +8,6 @@ import (
|
||||
"time"
|
||||
|
||||
http_utils "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
@@ -38,8 +37,8 @@ const (
|
||||
)
|
||||
|
||||
type CacheConfig struct {
|
||||
MaxAge types.Duration
|
||||
SharedMaxAge types.Duration
|
||||
MaxAge time.Duration
|
||||
SharedMaxAge time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
|
@@ -3,9 +3,9 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
http_utils "github.com/caos/zitadel/internal/api/http"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
@@ -35,12 +35,11 @@ type userAgentHandler struct {
|
||||
|
||||
type UserAgentCookieConfig struct {
|
||||
Name string
|
||||
Domain string
|
||||
Key *crypto.KeyConfig
|
||||
MaxAge types.Duration
|
||||
MaxAge time.Duration
|
||||
}
|
||||
|
||||
func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator, localDevMode bool) (func(http.Handler) http.Handler, error) {
|
||||
func NewUserAgentHandler(config *UserAgentCookieConfig, domain string, idGenerator id.Generator, externalSecure bool) (func(http.Handler) http.Handler, error) {
|
||||
key, err := crypto.LoadKey(config.Key, config.Key.EncryptionKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -48,10 +47,10 @@ func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator
|
||||
cookieKey := []byte(key)
|
||||
opts := []http_utils.CookieHandlerOpt{
|
||||
http_utils.WithEncryption(cookieKey, cookieKey),
|
||||
http_utils.WithDomain(config.Domain),
|
||||
http_utils.WithDomain(domain),
|
||||
http_utils.WithMaxAge(int(config.MaxAge.Seconds())),
|
||||
}
|
||||
if localDevMode {
|
||||
if !externalSecure {
|
||||
opts = append(opts, http_utils.WithUnsecure())
|
||||
}
|
||||
return func(handler http.Handler) http.Handler {
|
||||
|
@@ -30,3 +30,11 @@ func IsOrigin(rawOrigin string) bool {
|
||||
}
|
||||
return parsedUrl.Scheme != "" && parsedUrl.Host != "" && parsedUrl.Path == "" && len(parsedUrl.Query()) == 0 && parsedUrl.Fragment == ""
|
||||
}
|
||||
|
||||
func BuildHTTP(hostname string, externalPort uint16, secure bool) string {
|
||||
schema := "https"
|
||||
if !secure {
|
||||
schema = "http"
|
||||
}
|
||||
return fmt.Sprintf("%s://%s:%d", schema, hostname, externalPort)
|
||||
}
|
||||
|
@@ -1,33 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func Serve(ctx context.Context, handler http.Handler, port, servername string) {
|
||||
server := &http.Server{
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
listener := CreateListener(port)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
err := server.Shutdown(ctx)
|
||||
logging.LogWithFields("HTTP-m7kBlq", "name", servername).WithField("traceID", tracing.TraceIDFromCtx(ctx)).OnError(err).Warnf("error during graceful shutdown of http server (%s)", servername)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
logging.LogWithFields("HTTP-tBHR60", "name", servername).OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panicf("http serve (%s) failed", servername)
|
||||
}()
|
||||
logging.LogWithFields("HTTP-KHh0Cb", "name", servername, "port", port).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Infof("http server (%s) is listening", servername)
|
||||
}
|
||||
|
||||
func RegisterHandler(mux *http.ServeMux, prefix string, handler http.Handler) {
|
||||
mux.Handle(prefix+"/", http.StripPrefix(prefix, handler))
|
||||
}
|
@@ -2,44 +2,50 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/oidc/pkg/op"
|
||||
"github.com/rakyll/statik/fs"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/caos/zitadel/internal/api/assets"
|
||||
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"
|
||||
"github.com/caos/zitadel/internal/auth/repository"
|
||||
"github.com/caos/zitadel/internal/command"
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/handler/crdb"
|
||||
"github.com/caos/zitadel/internal/i18n"
|
||||
"github.com/caos/zitadel/internal/id"
|
||||
"github.com/caos/zitadel/internal/query"
|
||||
"github.com/caos/zitadel/internal/telemetry/metrics"
|
||||
"github.com/caos/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
type OPHandlerConfig struct {
|
||||
OPConfig *op.Config
|
||||
StorageConfig StorageConfig
|
||||
UserAgentCookieConfig *middleware.UserAgentCookieConfig
|
||||
Cache *middleware.CacheConfig
|
||||
Endpoints *EndpointConfig
|
||||
}
|
||||
const (
|
||||
HandlerPrefix = "/oauth/v2"
|
||||
AuthCallback = HandlerPrefix + "/authorize/callback?id="
|
||||
)
|
||||
|
||||
type StorageConfig struct {
|
||||
DefaultLoginURL string
|
||||
type Config struct {
|
||||
CodeMethodS256 bool
|
||||
AuthMethodPost bool
|
||||
AuthMethodPrivateKeyJWT bool
|
||||
GrantTypeRefreshToken bool
|
||||
RequestObjectSupported bool
|
||||
SigningKeyAlgorithm string
|
||||
DefaultAccessTokenLifetime types.Duration
|
||||
DefaultIdTokenLifetime types.Duration
|
||||
DefaultRefreshTokenIdleExpiration types.Duration
|
||||
DefaultRefreshTokenExpiration types.Duration
|
||||
DefaultAccessTokenLifetime time.Duration
|
||||
DefaultIdTokenLifetime time.Duration
|
||||
DefaultRefreshTokenIdleExpiration time.Duration
|
||||
DefaultRefreshTokenExpiration time.Duration
|
||||
UserAgentCookieConfig *middleware.UserAgentCookieConfig
|
||||
Cache *middleware.CacheConfig
|
||||
KeyConfig *crypto.KeyConfig
|
||||
CustomEndpoints *EndpointConfig
|
||||
}
|
||||
|
||||
type EndpointConfig struct {
|
||||
@@ -77,55 +83,116 @@ type OPStorage struct {
|
||||
assetAPIPrefix string
|
||||
}
|
||||
|
||||
func NewProvider(ctx context.Context, config OPHandlerConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, localDevMode bool, es *eventstore.Eventstore, projections types.SQL, keyChan <-chan interface{}, assetAPIPrefix string) op.OpenIDProvider {
|
||||
cookieHandler, err := middleware.NewUserAgentHandler(config.UserAgentCookieConfig, id.SonyFlakeGenerator, localDevMode)
|
||||
logging.Log("OIDC-sd4fd").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("cannot user agent handler")
|
||||
tokenKey, err := crypto.LoadKey(keyConfig.EncryptionConfig, keyConfig.EncryptionConfig.EncryptionKeyID)
|
||||
logging.Log("OIDC-ADvbv").OnError(err).Panic("cannot load OP crypto key")
|
||||
cryptoKey := []byte(tokenKey)
|
||||
if len(cryptoKey) != 32 {
|
||||
logging.Log("OIDC-Dsfds").Panic("OP crypto key must be exactly 32 bytes")
|
||||
func NewProvider(ctx context.Context, config Config, issuer, defaultLogoutRedirectURI string, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, es *eventstore.Eventstore, projections *sql.DB, keyChan <-chan interface{}, userAgentCookie func(http.Handler) http.Handler) (op.OpenIDProvider, error) {
|
||||
opConfig, err := createOPConfig(config, issuer, defaultLogoutRedirectURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create op config: %w", err)
|
||||
}
|
||||
storage, err := newStorage(config, command, query, repo, keyConfig, config.KeyConfig, es, projections, keyChan)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create storage: %w", err)
|
||||
}
|
||||
options, err := createOptions(config, userAgentCookie)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create options: %w", err)
|
||||
}
|
||||
copy(config.OPConfig.CryptoKey[:], cryptoKey)
|
||||
config.OPConfig.CodeMethodS256 = true
|
||||
config.OPConfig.AuthMethodPost = true
|
||||
config.OPConfig.AuthMethodPrivateKeyJWT = true
|
||||
config.OPConfig.GrantTypeRefreshToken = true
|
||||
supportedLanguages, err := getSupportedLanguages()
|
||||
logging.Log("OIDC-GBd3t").OnError(err).Panic("cannot get supported languages")
|
||||
config.OPConfig.SupportedUILocales = supportedLanguages
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||
storage, err := newStorage(config.StorageConfig, command, query, repo, keyConfig, es, projections, keyChan, assetAPIPrefix)
|
||||
logging.Log("OIDC-Jdg2k").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("cannot create storage")
|
||||
provider, err := op.NewOpenIDProvider(
|
||||
ctx,
|
||||
config.OPConfig,
|
||||
opConfig,
|
||||
storage,
|
||||
op.WithHttpInterceptors(
|
||||
middleware.MetricsHandler(metricTypes),
|
||||
middleware.TelemetryHandler(),
|
||||
middleware.NoCacheInterceptor,
|
||||
cookieHandler,
|
||||
http_utils.CopyHeadersToContext,
|
||||
),
|
||||
op.WithCustomAuthEndpoint(op.NewEndpointWithURL(config.Endpoints.Auth.Path, config.Endpoints.Auth.URL)),
|
||||
op.WithCustomTokenEndpoint(op.NewEndpointWithURL(config.Endpoints.Token.Path, config.Endpoints.Token.URL)),
|
||||
op.WithCustomIntrospectionEndpoint(op.NewEndpointWithURL(config.Endpoints.Introspection.Path, config.Endpoints.Introspection.URL)),
|
||||
op.WithCustomUserinfoEndpoint(op.NewEndpointWithURL(config.Endpoints.Userinfo.Path, config.Endpoints.Userinfo.URL)),
|
||||
op.WithCustomRevocationEndpoint(op.NewEndpointWithURL(config.Endpoints.Revocation.Path, config.Endpoints.Revocation.URL)),
|
||||
op.WithCustomEndSessionEndpoint(op.NewEndpointWithURL(config.Endpoints.EndSession.Path, config.Endpoints.EndSession.URL)),
|
||||
op.WithCustomKeysEndpoint(op.NewEndpointWithURL(config.Endpoints.Keys.Path, config.Endpoints.Keys.URL)),
|
||||
options...,
|
||||
)
|
||||
logging.Log("OIDC-asf13").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Panic("cannot create provider")
|
||||
return provider
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot create provider: %w", err)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func newStorage(config StorageConfig, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, es *eventstore.Eventstore, projections types.SQL, keyChan <-chan interface{}, assetAPIPrefix string) (*OPStorage, error) {
|
||||
encAlg, err := crypto.NewAESCrypto(keyConfig.EncryptionConfig)
|
||||
func Issuer(domain string, port uint16, externalSecure bool) string {
|
||||
return http_utils.BuildHTTP(domain, port, externalSecure) + HandlerPrefix
|
||||
}
|
||||
|
||||
func createOPConfig(config Config, issuer, defaultLogoutRedirectURI string) (*op.Config, error) {
|
||||
supportedLanguages, err := getSupportedLanguages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sqlClient, err := projections.Start()
|
||||
opConfig := &op.Config{
|
||||
Issuer: issuer,
|
||||
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
|
||||
CodeMethodS256: config.CodeMethodS256,
|
||||
AuthMethodPost: config.AuthMethodPost,
|
||||
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
|
||||
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
|
||||
RequestObjectSupported: config.RequestObjectSupported,
|
||||
SupportedUILocales: supportedLanguages,
|
||||
}
|
||||
if err := cryptoKey(opConfig, config.KeyConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return opConfig, nil
|
||||
}
|
||||
|
||||
func cryptoKey(config *op.Config, keyConfig *crypto.KeyConfig) error {
|
||||
tokenKey, err := crypto.LoadKey(keyConfig, keyConfig.EncryptionKeyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load OP crypto key: %w", err)
|
||||
}
|
||||
cryptoKey := []byte(tokenKey)
|
||||
if len(cryptoKey) != 32 {
|
||||
return fmt.Errorf("OP crypto key must be exactly 32 bytes")
|
||||
}
|
||||
copy(config.CryptoKey[:], cryptoKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func createOptions(config Config, userAgentCookie func(http.Handler) http.Handler) ([]op.Option, error) {
|
||||
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
||||
interceptor := op.WithHttpInterceptors(
|
||||
middleware.MetricsHandler(metricTypes),
|
||||
middleware.TelemetryHandler(),
|
||||
middleware.NoCacheInterceptor,
|
||||
userAgentCookie,
|
||||
http_utils.CopyHeadersToContext,
|
||||
)
|
||||
endpoints := customEndpoints(config.CustomEndpoints)
|
||||
if len(endpoints) == 0 {
|
||||
return []op.Option{interceptor}, nil
|
||||
}
|
||||
return append(endpoints, interceptor), nil
|
||||
}
|
||||
|
||||
func customEndpoints(endpointConfig *EndpointConfig) []op.Option {
|
||||
if endpointConfig == nil {
|
||||
return nil
|
||||
}
|
||||
options := []op.Option{}
|
||||
if endpointConfig.Auth != nil {
|
||||
options = append(options, op.WithCustomAuthEndpoint(op.NewEndpointWithURL(endpointConfig.Auth.Path, endpointConfig.Auth.URL)))
|
||||
}
|
||||
if endpointConfig.Token != nil {
|
||||
options = append(options, op.WithCustomTokenEndpoint(op.NewEndpointWithURL(endpointConfig.Token.Path, endpointConfig.Token.URL)))
|
||||
}
|
||||
if endpointConfig.Introspection != nil {
|
||||
options = append(options, op.WithCustomIntrospectionEndpoint(op.NewEndpointWithURL(endpointConfig.Introspection.Path, endpointConfig.Introspection.URL)))
|
||||
}
|
||||
if endpointConfig.Userinfo != nil {
|
||||
options = append(options, op.WithCustomUserinfoEndpoint(op.NewEndpointWithURL(endpointConfig.Userinfo.Path, endpointConfig.Userinfo.URL)))
|
||||
}
|
||||
if endpointConfig.Revocation != nil {
|
||||
options = append(options, op.WithCustomRevocationEndpoint(op.NewEndpointWithURL(endpointConfig.Revocation.Path, endpointConfig.Revocation.URL)))
|
||||
}
|
||||
if endpointConfig.EndSession != nil {
|
||||
options = append(options, op.WithCustomEndSessionEndpoint(op.NewEndpointWithURL(endpointConfig.EndSession.Path, endpointConfig.EndSession.URL)))
|
||||
}
|
||||
if endpointConfig.Keys != nil {
|
||||
options = append(options, op.WithCustomKeysEndpoint(op.NewEndpointWithURL(endpointConfig.Keys.Path, endpointConfig.Keys.URL)))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func newStorage(config Config, command *command.Commands, query *query.Queries, repo repository.Repository, keyConfig systemdefaults.KeyConfig, c *crypto.KeyConfig, es *eventstore.Eventstore, projections *sql.DB, keyChan <-chan interface{}) (*OPStorage, error) {
|
||||
encAlg, err := crypto.NewAESCrypto(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -134,18 +201,18 @@ func newStorage(config StorageConfig, command *command.Commands, query *query.Qu
|
||||
command: command,
|
||||
query: query,
|
||||
eventstore: es,
|
||||
defaultLoginURL: config.DefaultLoginURL,
|
||||
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
||||
signingKeyAlgorithm: config.SigningKeyAlgorithm,
|
||||
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime.Duration,
|
||||
defaultIdTokenLifetime: config.DefaultIdTokenLifetime.Duration,
|
||||
defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration.Duration,
|
||||
defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration.Duration,
|
||||
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime,
|
||||
defaultIdTokenLifetime: config.DefaultIdTokenLifetime,
|
||||
defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration,
|
||||
defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration,
|
||||
encAlg: encAlg,
|
||||
signingKeyGracefulPeriod: keyConfig.SigningKeyGracefulPeriod.Duration,
|
||||
signingKeyRotationCheck: keyConfig.SigningKeyRotationCheck.Duration,
|
||||
locker: crdb.NewLocker(sqlClient, locksTable, signingKey),
|
||||
signingKeyGracefulPeriod: keyConfig.SigningKeyGracefulPeriod,
|
||||
signingKeyRotationCheck: keyConfig.SigningKeyRotationCheck,
|
||||
locker: crdb.NewLocker(projections, locksTable, signingKey),
|
||||
keyChan: keyChan,
|
||||
assetAPIPrefix: assetAPIPrefix,
|
||||
assetAPIPrefix: assets.HandlerPrefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
129
internal/api/ui/console/console.go
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Black.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Black.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-BlackItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-BlackItalic.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Bold.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Bold.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-BoldItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-BoldItalic.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Italic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Italic.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Light.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Light.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-LightItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-LightItalic.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Regular.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Regular.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Thin.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-Thin.ttf
Executable file
Binary file not shown.
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-ThinItalic.ttf
Executable file
BIN
internal/api/ui/login/static/resources/fonts/lato/Lato-ThinItalic.ttf
Executable file
Binary file not shown.
93
internal/api/ui/login/static/resources/fonts/lato/OFL.txt
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";
|
||||
}
|
Binary file not shown.
@@ -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 |
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user