mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-13 09:25:12 +00:00
Merge branch 'rc' into next-rc
This commit is contained in:
+23
-27
@@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/health"
|
||||
healthpb "google.golang.org/grpc/health/grpc_health_v1"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
internal_authz "github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server"
|
||||
@@ -19,24 +20,22 @@ import (
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/logstore"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/metrics"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
port uint16
|
||||
grpcServer *grpc.Server
|
||||
verifier *internal_authz.TokenVerifier
|
||||
health healthCheck
|
||||
router *mux.Router
|
||||
http1HostName string
|
||||
grpcGateway *server.Gateway
|
||||
healthServer *health.Server
|
||||
cookieHandler *http_util.CookieHandler
|
||||
cookieConfig *http_mw.AccessConfig
|
||||
queries *query.Queries
|
||||
port uint16
|
||||
grpcServer *grpc.Server
|
||||
verifier *internal_authz.TokenVerifier
|
||||
health healthCheck
|
||||
router *mux.Router
|
||||
http1HostName string
|
||||
grpcGateway *server.Gateway
|
||||
healthServer *health.Server
|
||||
accessInterceptor *http_mw.AccessInterceptor
|
||||
queries *query.Queries
|
||||
}
|
||||
|
||||
type healthCheck interface {
|
||||
@@ -51,23 +50,20 @@ func New(
|
||||
verifier *internal_authz.TokenVerifier,
|
||||
authZ internal_authz.Config,
|
||||
tlsConfig *tls.Config, http2HostName, http1HostName string,
|
||||
accessSvc *logstore.Service,
|
||||
cookieHandler *http_util.CookieHandler,
|
||||
cookieConfig *http_mw.AccessConfig,
|
||||
accessInterceptor *http_mw.AccessInterceptor,
|
||||
) (_ *API, err error) {
|
||||
api := &API{
|
||||
port: port,
|
||||
verifier: verifier,
|
||||
health: queries,
|
||||
router: router,
|
||||
http1HostName: http1HostName,
|
||||
cookieConfig: cookieConfig,
|
||||
cookieHandler: cookieHandler,
|
||||
queries: queries,
|
||||
port: port,
|
||||
verifier: verifier,
|
||||
health: queries,
|
||||
router: router,
|
||||
http1HostName: http1HostName,
|
||||
queries: queries,
|
||||
accessInterceptor: accessInterceptor,
|
||||
}
|
||||
|
||||
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, tlsConfig, accessSvc)
|
||||
api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName, cookieHandler, cookieConfig)
|
||||
api.grpcServer = server.CreateServer(api.verifier, authZ, queries, http2HostName, tlsConfig, accessInterceptor.AccessService())
|
||||
api.grpcGateway, err = server.CreateGateway(ctx, port, http1HostName, accessInterceptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -76,6 +72,7 @@ func New(
|
||||
api.RegisterHandlerOnPrefix("/debug", api.healthHandler())
|
||||
api.router.Handle("/", http.RedirectHandler(login.HandlerPrefix, http.StatusFound))
|
||||
|
||||
reflection.Register(api.grpcServer)
|
||||
return api, nil
|
||||
}
|
||||
|
||||
@@ -90,8 +87,7 @@ func (a *API) RegisterServer(ctx context.Context, grpcServer server.WithGatewayP
|
||||
grpcServer,
|
||||
a.port,
|
||||
a.http1HostName,
|
||||
a.cookieHandler,
|
||||
a.cookieConfig,
|
||||
a.accessInterceptor,
|
||||
a.queries,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
// UserIDInCTX checks if the userID
|
||||
// equals the authenticated user in the context.
|
||||
func UserIDInCTX(ctx context.Context, userID string) error {
|
||||
if GetCtxData(ctx).UserID != userID {
|
||||
return errors.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -629,7 +629,7 @@ func (s *Server) importData(ctx context.Context, orgs []*admin_pb.DataOrg) (*adm
|
||||
ExternalUserID: userLinks.ProvidedUserId,
|
||||
DisplayName: userLinks.ProvidedUserName,
|
||||
}
|
||||
if err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil {
|
||||
if _, err := s.command.AddUserIDPLink(ctx, userLinks.UserId, org.GetOrgId(), externalIDP); err != nil {
|
||||
errors = append(errors, &admin_pb.ImportDataError{Type: "user_link", Id: userLinks.UserId + "_" + userLinks.IdpId, Message: err.Error()})
|
||||
if isCtxTimeout(ctx) {
|
||||
return &admin_pb.ImportDataResponse{Errors: errors, Success: success}, count, err
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// AllFieldsSet recusively checks if all values in a message
|
||||
// have a non-zero value.
|
||||
func AllFieldsSet(t testing.TB, msg protoreflect.Message, ignoreTypes ...protoreflect.FullName) {
|
||||
ignore := make(map[protoreflect.FullName]bool, len(ignoreTypes))
|
||||
for _, name := range ignoreTypes {
|
||||
ignore[name] = true
|
||||
}
|
||||
|
||||
md := msg.Descriptor()
|
||||
name := md.FullName()
|
||||
if ignore[name] {
|
||||
return
|
||||
}
|
||||
|
||||
fields := md.Fields()
|
||||
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
fd := fields.Get(i)
|
||||
if !msg.Has(fd) {
|
||||
t.Errorf("not all fields set in %q, missing %q", name, fd.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if fd.Kind() == protoreflect.MessageKind {
|
||||
AllFieldsSet(t, msg.Get(fd).Message(), ignoreTypes...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +241,6 @@ func AddHumanUserRequestToAddHuman(req *mgmt_pb.AddHumanUserRequest) *command.Ad
|
||||
PasswordChangeRequired: true,
|
||||
Passwordless: false,
|
||||
Register: false,
|
||||
ExternalIDP: false,
|
||||
}
|
||||
if req.Phone != nil {
|
||||
human.Phone = command.Phone{
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||
)
|
||||
|
||||
const (
|
||||
Healthz = "/Healthz"
|
||||
Readiness = "/Ready"
|
||||
@@ -9,3 +16,18 @@ const (
|
||||
var (
|
||||
Probes = []string{Healthz, Readiness, Validation}
|
||||
)
|
||||
|
||||
func init() {
|
||||
Probes = append(Probes, AllPaths(grpc_reflection_v1alpha.ServerReflection_ServiceDesc)...)
|
||||
}
|
||||
|
||||
func AllPaths(sd grpc.ServiceDesc) []string {
|
||||
paths := make([]string, 0, len(sd.Methods)+len(sd.Streams))
|
||||
for _, method := range sd.Methods {
|
||||
paths = append(paths, path.Join("/", sd.ServiceName, method.MethodName))
|
||||
}
|
||||
for _, stream := range sd.Streams {
|
||||
paths = append(paths, path.Join("/", sd.ServiceName, stream.StreamName))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||
)
|
||||
|
||||
func TestAllPaths(t *testing.T) {
|
||||
type args struct {
|
||||
sd grpc.ServiceDesc
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "server reflection",
|
||||
args: args{grpc_reflection_v1alpha.ServerReflection_ServiceDesc},
|
||||
want: []string{"/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := AllPaths(tt.args.sd)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
client_middleware "github.com/zitadel/zitadel/internal/api/grpc/client/middleware"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/server/middleware"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
@@ -66,16 +65,15 @@ var (
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
mux *runtime.ServeMux
|
||||
http1HostName string
|
||||
connection *grpc.ClientConn
|
||||
cookieHandler *http_utils.CookieHandler
|
||||
cookieConfig *http_mw.AccessConfig
|
||||
queries *query.Queries
|
||||
mux *runtime.ServeMux
|
||||
http1HostName string
|
||||
connection *grpc.ClientConn
|
||||
accessInterceptor *http_mw.AccessInterceptor
|
||||
queries *query.Queries
|
||||
}
|
||||
|
||||
func (g *Gateway) Handler() http.Handler {
|
||||
return addInterceptors(g.mux, g.http1HostName, g.cookieHandler, g.cookieConfig, g.queries)
|
||||
return addInterceptors(g.mux, g.http1HostName, g.accessInterceptor, g.queries)
|
||||
}
|
||||
|
||||
type CustomHTTPResponse interface {
|
||||
@@ -89,8 +87,7 @@ func CreateGatewayWithPrefix(
|
||||
g WithGatewayPrefix,
|
||||
port uint16,
|
||||
http1HostName string,
|
||||
cookieHandler *http_utils.CookieHandler,
|
||||
cookieConfig *http_mw.AccessConfig,
|
||||
accessInterceptor *http_mw.AccessInterceptor,
|
||||
queries *query.Queries,
|
||||
) (http.Handler, string, error) {
|
||||
runtimeMux := runtime.NewServeMux(serveMuxOptions...)
|
||||
@@ -106,10 +103,10 @@ func CreateGatewayWithPrefix(
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to register grpc gateway: %w", err)
|
||||
}
|
||||
return addInterceptors(runtimeMux, http1HostName, cookieHandler, cookieConfig, queries), g.GatewayPathPrefix(), nil
|
||||
return addInterceptors(runtimeMux, http1HostName, accessInterceptor, queries), g.GatewayPathPrefix(), nil
|
||||
}
|
||||
|
||||
func CreateGateway(ctx context.Context, port uint16, http1HostName string, cookieHandler *http_utils.CookieHandler, cookieConfig *http_mw.AccessConfig) (*Gateway, error) {
|
||||
func CreateGateway(ctx context.Context, port uint16, http1HostName string, accessInterceptor *http_mw.AccessInterceptor) (*Gateway, error) {
|
||||
connection, err := dial(ctx,
|
||||
port,
|
||||
[]grpc.DialOption{
|
||||
@@ -121,11 +118,10 @@ func CreateGateway(ctx context.Context, port uint16, http1HostName string, cooki
|
||||
}
|
||||
runtimeMux := runtime.NewServeMux(append(serveMuxOptions, runtime.WithHealthzEndpoint(healthpb.NewHealthClient(connection)))...)
|
||||
return &Gateway{
|
||||
mux: runtimeMux,
|
||||
http1HostName: http1HostName,
|
||||
connection: connection,
|
||||
cookieHandler: cookieHandler,
|
||||
cookieConfig: cookieConfig,
|
||||
mux: runtimeMux,
|
||||
http1HostName: http1HostName,
|
||||
connection: connection,
|
||||
accessInterceptor: accessInterceptor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -163,8 +159,7 @@ func dial(ctx context.Context, port uint16, opts []grpc.DialOption) (*grpc.Clien
|
||||
func addInterceptors(
|
||||
handler http.Handler,
|
||||
http1HostName string,
|
||||
cookieHandler *http_utils.CookieHandler,
|
||||
cookieConfig *http_mw.AccessConfig,
|
||||
accessInterceptor *http_mw.AccessInterceptor,
|
||||
queries *query.Queries,
|
||||
) http.Handler {
|
||||
handler = http_mw.CallDurationHandler(handler)
|
||||
@@ -174,7 +169,7 @@ func addInterceptors(
|
||||
handler = http_mw.DefaultTelemetryHandler(handler)
|
||||
// For some non-obvious reason, the exhaustedCookieInterceptor sends the SetCookie header
|
||||
// only if it follows the http_mw.DefaultTelemetryHandler
|
||||
handler = exhaustedCookieInterceptor(handler, cookieHandler, cookieConfig, queries)
|
||||
handler = exhaustedCookieInterceptor(handler, accessInterceptor, queries)
|
||||
handler = http_mw.DefaultMetricsHandler(handler)
|
||||
return handler
|
||||
}
|
||||
@@ -193,35 +188,32 @@ func http1Host(next http.Handler, http1HostName string) http.Handler {
|
||||
|
||||
func exhaustedCookieInterceptor(
|
||||
next http.Handler,
|
||||
cookieHandler *http_utils.CookieHandler,
|
||||
cookieConfig *http_mw.AccessConfig,
|
||||
accessInterceptor *http_mw.AccessInterceptor,
|
||||
queries *query.Queries,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
next.ServeHTTP(&cookieResponseWriter{
|
||||
ResponseWriter: writer,
|
||||
cookieHandler: cookieHandler,
|
||||
cookieConfig: cookieConfig,
|
||||
request: request,
|
||||
queries: queries,
|
||||
ResponseWriter: writer,
|
||||
accessInterceptor: accessInterceptor,
|
||||
request: request,
|
||||
queries: queries,
|
||||
}, request)
|
||||
})
|
||||
}
|
||||
|
||||
type cookieResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
cookieHandler *http_utils.CookieHandler
|
||||
cookieConfig *http_mw.AccessConfig
|
||||
request *http.Request
|
||||
queries *query.Queries
|
||||
accessInterceptor *http_mw.AccessInterceptor
|
||||
request *http.Request
|
||||
queries *query.Queries
|
||||
}
|
||||
|
||||
func (r *cookieResponseWriter) WriteHeader(status int) {
|
||||
if status >= 200 && status < 300 {
|
||||
http_mw.DeleteExhaustedCookie(r.cookieHandler, r.ResponseWriter, r.request, r.cookieConfig)
|
||||
r.accessInterceptor.DeleteExhaustedCookie(r.ResponseWriter)
|
||||
}
|
||||
if status == http.StatusTooManyRequests {
|
||||
http_mw.SetExhaustedCookie(r.cookieHandler, r.ResponseWriter, r.cookieConfig, r.request)
|
||||
r.accessInterceptor.SetExhaustedCookie(r.ResponseWriter, r.request)
|
||||
}
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
@@ -11,38 +11,13 @@ import (
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
|
||||
)
|
||||
|
||||
var ignoreMessageTypes = map[protoreflect.FullName]bool{
|
||||
"google.protobuf.Duration": true,
|
||||
}
|
||||
|
||||
// allFieldsSet recusively checks if all values in a message
|
||||
// have a non-zero value.
|
||||
func allFieldsSet(t testing.TB, msg protoreflect.Message) {
|
||||
md := msg.Descriptor()
|
||||
name := md.FullName()
|
||||
if ignoreMessageTypes[name] {
|
||||
return
|
||||
}
|
||||
|
||||
fields := md.Fields()
|
||||
|
||||
for i := 0; i < fields.Len(); i++ {
|
||||
fd := fields.Get(i)
|
||||
if !msg.Has(fd) {
|
||||
t.Errorf("not all fields set in %q, missing %q", name, fd.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
if fd.Kind() == protoreflect.MessageKind {
|
||||
allFieldsSet(t, msg.Get(fd).Message())
|
||||
}
|
||||
}
|
||||
}
|
||||
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}
|
||||
|
||||
func Test_loginSettingsToPb(t *testing.T) {
|
||||
arg := &query.LoginPolicy{
|
||||
@@ -100,7 +75,7 @@ func Test_loginSettingsToPb(t *testing.T) {
|
||||
}
|
||||
|
||||
got := loginSettingsToPb(arg)
|
||||
allFieldsSet(t, got.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
@@ -241,7 +216,7 @@ func Test_passwordSettingsToPb(t *testing.T) {
|
||||
}
|
||||
|
||||
got := passwordSettingsToPb(arg)
|
||||
allFieldsSet(t, got.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("passwordSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
@@ -295,7 +270,7 @@ func Test_brandingSettingsToPb(t *testing.T) {
|
||||
}
|
||||
|
||||
got := brandingSettingsToPb(arg, "http://example.com")
|
||||
allFieldsSet(t, got.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
@@ -315,7 +290,7 @@ func Test_domainSettingsToPb(t *testing.T) {
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
got := domainSettingsToPb(arg)
|
||||
allFieldsSet(t, got.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
@@ -337,7 +312,7 @@ func Test_legalSettingsToPb(t *testing.T) {
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
got := legalAndSupportSettingsToPb(arg)
|
||||
allFieldsSet(t, got.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
@@ -353,7 +328,7 @@ func Test_lockoutSettingsToPb(t *testing.T) {
|
||||
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
|
||||
}
|
||||
got := lockoutSettingsToPb(arg)
|
||||
allFieldsSet(t, got.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(got, want) {
|
||||
t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
@@ -387,7 +362,7 @@ func Test_identityProvidersToPb(t *testing.T) {
|
||||
got := identityProvidersToPb(arg)
|
||||
require.Len(t, got, len(got))
|
||||
for i, v := range got {
|
||||
allFieldsSet(t, v.ProtoReflect())
|
||||
grpc.AllFieldsSet(t, v.ProtoReflect(), ignoreTypes...)
|
||||
if !proto.Equal(v, want[i]) {
|
||||
t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
|
||||
var (
|
||||
resourceOwner = authz.GetCtxData(ctx).ResourceOwner
|
||||
authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator())
|
||||
)
|
||||
if code := req.GetCode(); code != nil {
|
||||
return passkeyRegistrationDetailsToPb(
|
||||
s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), resourceOwner, authenticator, code.Id, code.Code, s.userCodeAlg),
|
||||
)
|
||||
}
|
||||
return passkeyRegistrationDetailsToPb(
|
||||
s.command.RegisterUserPasskey(ctx, req.GetUserId(), resourceOwner, authenticator),
|
||||
)
|
||||
}
|
||||
|
||||
func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.AuthenticatorAttachment {
|
||||
switch pa {
|
||||
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED:
|
||||
return domain.AuthenticatorAttachmentUnspecified
|
||||
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM:
|
||||
return domain.AuthenticatorAttachmentPlattform
|
||||
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM:
|
||||
return domain.AuthenticatorAttachmentCrossPlattform
|
||||
default:
|
||||
return domain.AuthenticatorAttachmentUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func passkeyRegistrationDetailsToPb(details *domain.PasskeyRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.RegisterPasskeyResponse{
|
||||
Details: object.DomainToDetailsPb(details.ObjectDetails),
|
||||
PasskeyId: details.PasskeyID,
|
||||
PublicKeyCredentialCreationOptions: details.PublicKeyCredentialCreationOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
|
||||
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
||||
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), resourceOwner, req.GetPasskeyName(), "", req.GetPublicKeyCredential())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.VerifyPasskeyRegistrationResponse{
|
||||
Details: object.DomainToDetailsPb(objectDetails),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) {
|
||||
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
||||
|
||||
switch medium := req.Medium.(type) {
|
||||
case nil:
|
||||
return passkeyDetailsToPb(
|
||||
s.command.AddUserPasskeyCode(ctx, req.GetUserId(), resourceOwner, s.userCodeAlg),
|
||||
)
|
||||
case *user.CreatePasskeyRegistrationLinkRequest_SendLink:
|
||||
return passkeyDetailsToPb(
|
||||
s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), resourceOwner, s.userCodeAlg, medium.SendLink.GetUrlTemplate()),
|
||||
)
|
||||
case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode:
|
||||
return passkeyCodeDetailsToPb(
|
||||
s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), resourceOwner, s.userCodeAlg),
|
||||
)
|
||||
default:
|
||||
return nil, caos_errs.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium)
|
||||
}
|
||||
}
|
||||
|
||||
func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details.ObjectDetails),
|
||||
Code: &user.PasskeyRegistrationCode{
|
||||
Id: details.CodeID,
|
||||
Code: details.Code,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
//go:build integration
|
||||
|
||||
package user_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/webauthn"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func TestServer_RegisterPasskey(t *testing.T) {
|
||||
userID := createHumanUser(t).GetUserId()
|
||||
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
client := webauthn.NewClient(Tester.Config.WebAuthNName, Tester.Config.ExternalDomain, "https://"+Tester.Host())
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RegisterPasskeyRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RegisterPasskeyResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "register code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
|
||||
},
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reuse code (not allowed)",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: &user.PasskeyRegistrationCode{
|
||||
Id: reg.GetCode().GetId(),
|
||||
Code: "foobar",
|
||||
},
|
||||
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "user mismatch",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
/* TODO after we are able to obtain a Bearer token for a human user
|
||||
{
|
||||
name: "human user",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.RegisterPasskeyRequest{
|
||||
UserId: humanUserID,
|
||||
},
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RegisterPasskey(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.want != nil {
|
||||
assert.NotEmpty(t, got.GetPasskeyId())
|
||||
assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions())
|
||||
_, err := client.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
|
||||
userID := createHumanUser(t).GetUserId()
|
||||
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
|
||||
UserId: userID,
|
||||
Code: reg.GetCode(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, pkr.GetPasskeyId())
|
||||
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||
|
||||
client := webauthn.NewClient(Tester.Config.WebAuthNName, Tester.Config.ExternalDomain, "https://"+Tester.Host())
|
||||
attestationResponse, err := client.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.VerifyPasskeyRegistrationRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.VerifyPasskeyRegistrationResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.VerifyPasskeyRegistrationRequest{
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: []byte(attestationResponse),
|
||||
PasskeyName: "nice name",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.VerifyPasskeyRegistrationRequest{
|
||||
UserId: userID,
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: attestationResponse,
|
||||
PasskeyName: "nice name",
|
||||
},
|
||||
},
|
||||
want: &user.VerifyPasskeyRegistrationResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong credential",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.VerifyPasskeyRegistrationRequest{
|
||||
UserId: userID,
|
||||
PasskeyId: pkr.GetPasskeyId(),
|
||||
PublicKeyCredential: []byte("attestationResponseattestationResponseattestationResponse"),
|
||||
PasskeyName: "nice name",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.VerifyPasskeyRegistration(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
|
||||
userID := createHumanUser(t).GetUserId()
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.CreatePasskeyRegistrationLinkRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.CreatePasskeyRegistrationLinkResponse
|
||||
wantCode bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing user id",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "send default mail",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
},
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send custom url",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_SendLink{
|
||||
SendLink: &user.SendPasskeyRegistrationLink{
|
||||
UrlTemplate: gu.Ptr("https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "return code",
|
||||
args: args{
|
||||
ctx: CTX,
|
||||
req: &user.CreatePasskeyRegistrationLinkRequest{
|
||||
UserId: userID,
|
||||
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
|
||||
},
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
wantCode: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.CreatePasskeyRegistrationLink(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
if tt.wantCode {
|
||||
assert.NotEmpty(t, got.GetCode().GetId())
|
||||
assert.NotEmpty(t, got.GetCode().GetId())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
pa user.PasskeyAuthenticator
|
||||
want domain.AuthenticatorAttachment
|
||||
}{
|
||||
{
|
||||
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED,
|
||||
want: domain.AuthenticatorAttachmentUnspecified,
|
||||
},
|
||||
{
|
||||
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
|
||||
want: domain.AuthenticatorAttachmentPlattform,
|
||||
},
|
||||
{
|
||||
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
|
||||
want: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
},
|
||||
{
|
||||
pa: 999,
|
||||
want: domain.AuthenticatorAttachmentUnspecified,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.pa.String(), func(t *testing.T) {
|
||||
got := passkeyAuthenticatorToDomain(tt.pa)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
type args struct {
|
||||
details *domain.PasskeyRegistrationDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RegisterPasskeyResponse
|
||||
}{
|
||||
{
|
||||
name: "an error",
|
||||
args: args{
|
||||
details: nil,
|
||||
err: io.ErrClosedPipe,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
details: &domain.PasskeyRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte{1, 2, 3},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
want: &user.RegisterPasskeyResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyId: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte{1, 2, 3},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err)
|
||||
require.ErrorIs(t, err, tt.args.err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
if tt.want != nil {
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passkeyDetailsToPb(t *testing.T) {
|
||||
type args struct {
|
||||
details *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.CreatePasskeyRegistrationLinkResponse
|
||||
}{
|
||||
{
|
||||
name: "an error",
|
||||
args: args{
|
||||
details: nil,
|
||||
err: io.ErrClosedPipe,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
details: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := passkeyDetailsToPb(tt.args.details, tt.args.err)
|
||||
require.ErrorIs(t, err, tt.args.err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_passkeyCodeDetailsToPb(t *testing.T) {
|
||||
type args struct {
|
||||
details *domain.PasskeyCodeDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.CreatePasskeyRegistrationLinkResponse
|
||||
}{
|
||||
{
|
||||
name: "an error",
|
||||
args: args{
|
||||
details: nil,
|
||||
err: io.ErrClosedPipe,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
details: &domain.PasskeyCodeDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
CodeID: "123",
|
||||
Code: "456",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
want: &user.CreatePasskeyRegistrationLinkResponse{
|
||||
Details: &object.Details{
|
||||
Sequence: 22,
|
||||
ChangeDate: ×tamppb.Timestamp{
|
||||
Seconds: 3000,
|
||||
Nanos: 22,
|
||||
},
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
Code: &user.PasskeyRegistrationCode{
|
||||
Id: "123",
|
||||
Code: "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err)
|
||||
require.ErrorIs(t, err, tt.args.err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
if tt.want != nil {
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
@@ -18,15 +20,25 @@ type Server struct {
|
||||
command *command.Commands
|
||||
query *query.Queries
|
||||
userCodeAlg crypto.EncryptionAlgorithm
|
||||
idpAlg crypto.EncryptionAlgorithm
|
||||
idpCallback func(ctx context.Context) string
|
||||
}
|
||||
|
||||
type Config struct{}
|
||||
|
||||
func CreateServer(command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm) *Server {
|
||||
func CreateServer(
|
||||
command *command.Commands,
|
||||
query *query.Queries,
|
||||
userCodeAlg crypto.EncryptionAlgorithm,
|
||||
idpAlg crypto.EncryptionAlgorithm,
|
||||
idpCallback func(ctx context.Context) string,
|
||||
) *Server {
|
||||
return &Server{
|
||||
command: command,
|
||||
query: query,
|
||||
userCodeAlg: userCodeAlg,
|
||||
idpAlg: idpAlg,
|
||||
idpCallback: idpCallback,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,19 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
@@ -56,6 +60,14 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
|
||||
Value: metadataEntry.GetValue(),
|
||||
}
|
||||
}
|
||||
links := make([]*command.AddLink, len(req.GetIdpLinks()))
|
||||
for i, link := range req.GetIdpLinks() {
|
||||
links[i] = &command.AddLink{
|
||||
IDPID: link.GetIdpId(),
|
||||
IDPExternalID: link.GetIdpExternalId(),
|
||||
DisplayName: link.GetDisplayName(),
|
||||
}
|
||||
}
|
||||
return &command.AddHuman{
|
||||
ID: req.GetUserId(),
|
||||
Username: username,
|
||||
@@ -76,9 +88,9 @@ func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman,
|
||||
BcryptedPassword: bcryptedPassword,
|
||||
PasswordChangeRequired: passwordChangeRequired,
|
||||
Passwordless: false,
|
||||
ExternalIDP: false,
|
||||
Register: false,
|
||||
Metadata: metadata,
|
||||
Links: links,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -107,3 +119,95 @@ func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
|
||||
}
|
||||
return hashed.GetHash(), nil
|
||||
}
|
||||
|
||||
func (s *Server) AddIDPLink(ctx context.Context, req *user.AddIDPLinkRequest) (_ *user.AddIDPLinkResponse, err error) {
|
||||
orgID := authz.GetCtxData(ctx).OrgID
|
||||
details, err := s.command.AddUserIDPLink(ctx, req.UserId, orgID, &domain.UserIDPLink{
|
||||
IDPConfigID: req.GetIdpLink().GetIdpId(),
|
||||
ExternalUserID: req.GetIdpLink().GetIdpExternalId(),
|
||||
DisplayName: req.GetIdpLink().GetDisplayName(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.AddIDPLinkResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) StartIdentityProviderFlow(ctx context.Context, req *user.StartIdentityProviderFlowRequest) (_ *user.StartIdentityProviderFlowResponse, err error) {
|
||||
id, details, err := s.command.CreateIntent(ctx, req.GetIdpId(), req.GetSuccessUrl(), req.GetFailureUrl(), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authURL, err := s.command.AuthURLFromProvider(ctx, req.GetIdpId(), id, s.idpCallback(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user.StartIdentityProviderFlowResponse{
|
||||
Details: object.DomainToDetailsPb(details),
|
||||
NextStep: &user.StartIdentityProviderFlowResponse_AuthUrl{AuthUrl: authURL},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) RetrieveIdentityProviderInformation(ctx context.Context, req *user.RetrieveIdentityProviderInformationRequest) (_ *user.RetrieveIdentityProviderInformationResponse, err error) {
|
||||
intent, err := s.command.GetIntentWriteModel(ctx, req.GetIntentId(), authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.checkIntentToken(req.GetToken(), intent.AggregateID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if intent.State != domain.IDPIntentStateSucceeded {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "IDP-Hk38e", "Errors.Intent.NotSucceeded")
|
||||
}
|
||||
return intentToIDPInformationPb(intent, s.idpAlg)
|
||||
}
|
||||
|
||||
func intentToIDPInformationPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderInformationResponse, err error) {
|
||||
var idToken *string
|
||||
if intent.IDPIDToken != "" {
|
||||
idToken = &intent.IDPIDToken
|
||||
}
|
||||
var accessToken string
|
||||
if intent.IDPAccessToken != nil {
|
||||
accessToken, err = crypto.DecryptString(intent.IDPAccessToken, alg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &user.RetrieveIdentityProviderInformationResponse{
|
||||
Details: &object_pb.Details{
|
||||
Sequence: intent.ProcessedSequence,
|
||||
ChangeDate: timestamppb.New(intent.ChangeDate),
|
||||
ResourceOwner: intent.ResourceOwner,
|
||||
},
|
||||
IdpInformation: &user.IDPInformation{
|
||||
Access: &user.IDPInformation_Oauth{
|
||||
Oauth: &user.IDPOAuthAccessInformation{
|
||||
AccessToken: accessToken,
|
||||
IdToken: idToken,
|
||||
},
|
||||
},
|
||||
IdpInformation: intent.IDPUser,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) checkIntentToken(token string, intentID string) error {
|
||||
if token == "" {
|
||||
return errors.ThrowPermissionDenied(nil, "IDP-Sfefs", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
data, err := base64.RawURLEncoding.DecodeString(token)
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "IDP-Swg31", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
decryptedToken, err := s.idpAlg.Decrypt(data, s.idpAlg.EncryptionKeyID())
|
||||
if err != nil {
|
||||
return errors.ThrowPermissionDenied(err, "IDP-Sf4gt", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
if string(decryptedToken) != intentID {
|
||||
return errors.ThrowPermissionDenied(nil, "IDP-dkje3", "Errors.Intent.InvalidToken")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,16 +6,24 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
"github.com/zitadel/zitadel/internal/integration"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,7 +47,60 @@ func TestMain(m *testing.M) {
|
||||
}())
|
||||
}
|
||||
|
||||
func createProvider(t *testing.T) string {
|
||||
ctx := authz.WithInstance(context.Background(), Tester.Instance)
|
||||
id, _, err := Tester.Commands.AddOrgGenericOAuthProvider(ctx, Tester.Organisation.ID, command.GenericOAuthProvider{
|
||||
"idp",
|
||||
"clientID",
|
||||
"clientSecret",
|
||||
"https://example.com/oauth/v2/authorize",
|
||||
"https://example.com/oauth/v2/token",
|
||||
"https://api.example.com/user",
|
||||
[]string{"openid", "profile", "email"},
|
||||
"id",
|
||||
idp.Options{
|
||||
IsLinkingAllowed: true,
|
||||
IsCreationAllowed: true,
|
||||
IsAutoCreation: true,
|
||||
IsAutoUpdate: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func createIntent(t *testing.T, idpID string) string {
|
||||
ctx := authz.WithInstance(context.Background(), Tester.Instance)
|
||||
id, _, err := Tester.Commands.CreateIntent(ctx, idpID, "https://example.com/success", "https://example.com/failure", Tester.Organisation.ID)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
|
||||
func createSuccessfulIntent(t *testing.T, idpID string) (string, string, time.Time, uint64) {
|
||||
ctx := authz.WithInstance(context.Background(), Tester.Instance)
|
||||
intentID := createIntent(t, idpID)
|
||||
writeModel, err := Tester.Commands.GetIntentWriteModel(ctx, intentID, Tester.Organisation.ID)
|
||||
require.NoError(t, err)
|
||||
idpUser := &oauth.UserMapper{
|
||||
RawInfo: map[string]interface{}{
|
||||
"id": "id",
|
||||
},
|
||||
}
|
||||
idpSession := &oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
}
|
||||
token, err := Tester.Commands.SucceedIDPIntent(ctx, writeModel, idpUser, idpSession, "")
|
||||
require.NoError(t, err)
|
||||
return intentID, token, writeModel.ChangeDate, writeModel.ProcessedSequence
|
||||
}
|
||||
|
||||
func TestServer_AddHumanUser(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddHumanUserRequest
|
||||
@@ -287,6 +348,105 @@ func TestServer_AddHumanUser(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing idp",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddHumanUserRequest{
|
||||
Organisation: &object.Organisation{
|
||||
Org: &object.Organisation_OrgId{
|
||||
OrgId: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
Profile: &user.SetHumanProfile{
|
||||
FirstName: "Donald",
|
||||
LastName: "Duck",
|
||||
NickName: gu.Ptr("Dukkie"),
|
||||
DisplayName: gu.Ptr("Donald Duck"),
|
||||
PreferredLanguage: gu.Ptr("en"),
|
||||
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
||||
},
|
||||
Email: &user.SetHumanEmail{
|
||||
Email: "[email protected]",
|
||||
Verification: &user.SetHumanEmail_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
Metadata: []*user.SetMetadataEntry{
|
||||
{
|
||||
Key: "somekey",
|
||||
Value: []byte("somevalue"),
|
||||
},
|
||||
},
|
||||
PasswordType: &user.AddHumanUserRequest_Password{
|
||||
Password: &user.Password{
|
||||
Password: "DifficultPW666!",
|
||||
ChangeRequired: false,
|
||||
},
|
||||
},
|
||||
IdpLinks: []*user.IDPLink{
|
||||
{
|
||||
IdpId: "idpID",
|
||||
IdpExternalId: "externalID",
|
||||
DisplayName: "displayName",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "with idp",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddHumanUserRequest{
|
||||
Organisation: &object.Organisation{
|
||||
Org: &object.Organisation_OrgId{
|
||||
OrgId: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
Profile: &user.SetHumanProfile{
|
||||
FirstName: "Donald",
|
||||
LastName: "Duck",
|
||||
NickName: gu.Ptr("Dukkie"),
|
||||
DisplayName: gu.Ptr("Donald Duck"),
|
||||
PreferredLanguage: gu.Ptr("en"),
|
||||
Gender: user.Gender_GENDER_DIVERSE.Enum(),
|
||||
},
|
||||
Email: &user.SetHumanEmail{
|
||||
Email: "[email protected]",
|
||||
Verification: &user.SetHumanEmail_IsVerified{
|
||||
IsVerified: true,
|
||||
},
|
||||
},
|
||||
Metadata: []*user.SetMetadataEntry{
|
||||
{
|
||||
Key: "somekey",
|
||||
Value: []byte("somevalue"),
|
||||
},
|
||||
},
|
||||
PasswordType: &user.AddHumanUserRequest_Password{
|
||||
Password: &user.Password{
|
||||
Password: "DifficultPW666!",
|
||||
ChangeRequired: false,
|
||||
},
|
||||
},
|
||||
IdpLinks: []*user.IDPLink{
|
||||
{
|
||||
IdpId: idpID,
|
||||
IdpExternalId: "externalID",
|
||||
DisplayName: "displayName",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.AddHumanUserResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -315,3 +475,226 @@ func TestServer_AddHumanUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_AddIDPLink(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.AddIDPLinkRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.AddIDPLinkResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "user does not exist",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: "userID",
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
IdpExternalId: "externalID",
|
||||
DisplayName: "displayName",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "idp does not exist",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: Tester.Users[integration.OrgOwner].ID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: "idpID",
|
||||
IdpExternalId: "externalID",
|
||||
DisplayName: "displayName",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "add link",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.AddIDPLinkRequest{
|
||||
UserId: Tester.Users[integration.OrgOwner].ID,
|
||||
IdpLink: &user.IDPLink{
|
||||
IdpId: idpID,
|
||||
IdpExternalId: "externalID",
|
||||
DisplayName: "displayName",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: &user.AddIDPLinkResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.AddIDPLink(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_StartIdentityProviderFlow(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.StartIdentityProviderFlowRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.StartIdentityProviderFlowResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "missing urls",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.StartIdentityProviderFlowRequest{
|
||||
IdpId: idpID,
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "next step auth url",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.StartIdentityProviderFlowRequest{
|
||||
IdpId: idpID,
|
||||
SuccessUrl: "https://example.com/success",
|
||||
FailureUrl: "https://example.com/failure",
|
||||
},
|
||||
},
|
||||
want: &user.StartIdentityProviderFlowResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.Now(),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
},
|
||||
NextStep: &user.StartIdentityProviderFlowResponse_AuthUrl{
|
||||
AuthUrl: "https://example.com/oauth/v2/authorize?client_id=clientID&prompt=select_account&redirect_uri=https%3A%2F%2Flocalhost%3A8080%2Fidps%2Fcallback&response_type=code&scope=openid+profile+email&state=",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.StartIdentityProviderFlow(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
if nextStep := tt.want.GetNextStep(); nextStep != nil {
|
||||
if !strings.HasPrefix(got.GetAuthUrl(), tt.want.GetAuthUrl()) {
|
||||
assert.Failf(t, "auth url does not match", "expected: %s, but got: %s", tt.want.GetAuthUrl(), got.GetAuthUrl())
|
||||
}
|
||||
}
|
||||
integration.AssertDetails(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServer_RetrieveIdentityProviderInformation(t *testing.T) {
|
||||
idpID := createProvider(t)
|
||||
intentID := createIntent(t, idpID)
|
||||
successfulID, token, changeDate, sequence := createSuccessfulIntent(t, idpID)
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
req *user.RetrieveIdentityProviderInformationRequest
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *user.RetrieveIdentityProviderInformationResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "failed intent",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.RetrieveIdentityProviderInformationRequest{
|
||||
IntentId: intentID,
|
||||
Token: "",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong token",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.RetrieveIdentityProviderInformationRequest{
|
||||
IntentId: successfulID,
|
||||
Token: "wrong token",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "retrieve successful intent",
|
||||
args: args{
|
||||
CTX,
|
||||
&user.RetrieveIdentityProviderInformationRequest{
|
||||
IntentId: successfulID,
|
||||
Token: token,
|
||||
},
|
||||
},
|
||||
want: &user.RetrieveIdentityProviderInformationResponse{
|
||||
Details: &object.Details{
|
||||
ChangeDate: timestamppb.New(changeDate),
|
||||
ResourceOwner: Tester.Organisation.ID,
|
||||
Sequence: sequence,
|
||||
},
|
||||
IdpInformation: &user.IDPInformation{
|
||||
Access: &user.IDPInformation_Oauth{
|
||||
Oauth: &user.IDPOAuthAccessInformation{
|
||||
AccessToken: "accessToken",
|
||||
IdToken: gu.Ptr("idToken"),
|
||||
},
|
||||
},
|
||||
IdpInformation: []byte(`{"RawInfo":{"id":"id"}}`),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Client.RetrieveIdentityProviderInformation(tt.args.ctx, tt.args.req)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
require.Equal(t, tt.want.GetDetails(), got.GetDetails())
|
||||
require.Equal(t, tt.want.GetIdpInformation(), got.GetIdpInformation())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,21 @@ package user
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/muhlemmer/gu"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||
)
|
||||
|
||||
@@ -78,3 +88,118 @@ func Test_hashedPasswordToCommand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_intentToIDPInformationPb(t *testing.T) {
|
||||
decryption := func(err error) crypto.EncryptionAlgorithm {
|
||||
mCrypto := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
||||
mCrypto.EXPECT().Algorithm().Return("enc")
|
||||
mCrypto.EXPECT().DecryptionKeyIDs().Return([]string{"id"})
|
||||
mCrypto.EXPECT().DecryptString(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(code []byte, keyID string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(code), nil
|
||||
})
|
||||
return mCrypto
|
||||
}
|
||||
|
||||
type args struct {
|
||||
intent *command.IDPIntentWriteModel
|
||||
alg crypto.EncryptionAlgorithm
|
||||
}
|
||||
type res struct {
|
||||
resp *user.RetrieveIdentityProviderInformationResponse
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"decryption invalid key id error",
|
||||
args{
|
||||
intent: &command.IDPIntentWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: "intentID",
|
||||
ProcessedSequence: 123,
|
||||
ResourceOwner: "ro",
|
||||
InstanceID: "instanceID",
|
||||
ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local),
|
||||
},
|
||||
IDPID: "idpID",
|
||||
IDPUser: []byte(`{"id": "id"}`),
|
||||
IDPAccessToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
IDPIDToken: "idToken",
|
||||
UserID: "userID",
|
||||
State: domain.IDPIntentStateSucceeded,
|
||||
},
|
||||
alg: decryption(caos_errs.ThrowInternal(nil, "id", "invalid key id")),
|
||||
},
|
||||
res{
|
||||
resp: nil,
|
||||
err: caos_errs.ThrowInternal(nil, "id", "invalid key id"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"successful",
|
||||
args{
|
||||
intent: &command.IDPIntentWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: "intentID",
|
||||
ProcessedSequence: 123,
|
||||
ResourceOwner: "ro",
|
||||
InstanceID: "instanceID",
|
||||
ChangeDate: time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local),
|
||||
},
|
||||
IDPID: "idpID",
|
||||
IDPUser: []byte(`{"id": "id"}`),
|
||||
IDPAccessToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
IDPIDToken: "idToken",
|
||||
UserID: "userID",
|
||||
State: domain.IDPIntentStateSucceeded,
|
||||
},
|
||||
alg: decryption(nil),
|
||||
},
|
||||
res{
|
||||
resp: &user.RetrieveIdentityProviderInformationResponse{
|
||||
Details: &object_pb.Details{
|
||||
Sequence: 123,
|
||||
ChangeDate: timestamppb.New(time.Date(2019, 4, 1, 1, 1, 1, 1, time.Local)),
|
||||
ResourceOwner: "ro",
|
||||
},
|
||||
IdpInformation: &user.IDPInformation{
|
||||
Access: &user.IDPInformation_Oauth{
|
||||
Oauth: &user.IDPOAuthAccessInformation{
|
||||
AccessToken: "accessToken",
|
||||
IdToken: gu.Ptr("idToken"),
|
||||
}},
|
||||
IdpInformation: []byte(`{"id": "id"}`),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := intentToIDPInformationPb(tt.args.intent, tt.args.alg)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.resp, got)
|
||||
if tt.res.resp != nil {
|
||||
grpc.AllFieldsSet(t, got.ProtoReflect())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,8 +123,8 @@ func (c *CookieHandler) SetEncryptedCookie(w http.ResponseWriter, name, domain s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, r *http.Request, name string) {
|
||||
c.httpSet(w, name, r.Host, "", -1)
|
||||
func (c *CookieHandler) DeleteCookie(w http.ResponseWriter, name string) {
|
||||
c.httpSet(w, name, "", "", -1)
|
||||
}
|
||||
|
||||
func (c *CookieHandler) httpSet(w http.ResponseWriter, name, domain, value string, maxage int) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -32,15 +33,54 @@ type AccessConfig struct {
|
||||
// NewAccessInterceptor intercepts all requests and stores them to the logstore.
|
||||
// If storeOnly is false, it also checks if requests are exhausted.
|
||||
// If requests are exhausted, it also returns http.StatusTooManyRequests and sets a cookie
|
||||
func NewAccessInterceptor(svc *logstore.Service, cookieHandler *http_utils.CookieHandler, cookieConfig *AccessConfig, storeOnly bool) *AccessInterceptor {
|
||||
func NewAccessInterceptor(svc *logstore.Service, cookieHandler *http_utils.CookieHandler, cookieConfig *AccessConfig) *AccessInterceptor {
|
||||
return &AccessInterceptor{
|
||||
svc: svc,
|
||||
cookieHandler: cookieHandler,
|
||||
limitConfig: cookieConfig,
|
||||
storeOnly: storeOnly,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) WithoutLimiting() *AccessInterceptor {
|
||||
return &AccessInterceptor{
|
||||
svc: a.svc,
|
||||
cookieHandler: a.cookieHandler,
|
||||
limitConfig: a.limitConfig,
|
||||
storeOnly: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) AccessService() *logstore.Service {
|
||||
return a.svc
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) Limit(ctx context.Context) bool {
|
||||
if !a.svc.Enabled() || a.storeOnly {
|
||||
return false
|
||||
}
|
||||
instance := authz.GetInstance(ctx)
|
||||
remaining := a.svc.Limit(ctx, instance.InstanceID())
|
||||
return remaining != nil && *remaining <= 0
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) SetExhaustedCookie(writer http.ResponseWriter, request *http.Request) {
|
||||
cookieValue := "true"
|
||||
host := request.Header.Get(middleware.HTTP1Host)
|
||||
domain := host
|
||||
if strings.ContainsAny(host, ":") {
|
||||
var err error
|
||||
domain, _, err = net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
logging.WithError(err).WithField("host", host).Warning("failed to extract cookie domain from request host")
|
||||
}
|
||||
}
|
||||
a.cookieHandler.SetCookie(writer, a.limitConfig.ExhaustedCookieKey, domain, cookieValue)
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) DeleteExhaustedCookie(writer http.ResponseWriter) {
|
||||
a.cookieHandler.DeleteCookie(writer, a.limitConfig.ExhaustedCookieKey)
|
||||
}
|
||||
|
||||
func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
|
||||
if !a.svc.Enabled() {
|
||||
return next
|
||||
@@ -49,23 +89,16 @@ func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
|
||||
ctx := request.Context()
|
||||
tracingCtx, checkSpan := tracing.NewNamedSpan(ctx, "checkAccess")
|
||||
wrappedWriter := &statusRecorder{ResponseWriter: writer, status: 0}
|
||||
instance := authz.GetInstance(ctx)
|
||||
limit := false
|
||||
if !a.storeOnly {
|
||||
remaining := a.svc.Limit(tracingCtx, instance.InstanceID())
|
||||
limit = remaining != nil && *remaining == 0
|
||||
}
|
||||
limited := a.Limit(tracingCtx)
|
||||
checkSpan.End()
|
||||
if limit {
|
||||
// Limit can only be true when storeOnly is false, so set the cookie and the response code
|
||||
SetExhaustedCookie(a.cookieHandler, wrappedWriter, a.limitConfig, request)
|
||||
if limited {
|
||||
a.SetExhaustedCookie(wrappedWriter, request)
|
||||
http.Error(wrappedWriter, "quota for authenticated requests is exhausted", http.StatusTooManyRequests)
|
||||
} else {
|
||||
if !a.storeOnly {
|
||||
// If not limited and not storeOnly, ensure the cookie is deleted
|
||||
DeleteExhaustedCookie(a.cookieHandler, wrappedWriter, request, a.limitConfig)
|
||||
}
|
||||
// Always serve if not limited
|
||||
}
|
||||
if !limited && !a.storeOnly {
|
||||
a.DeleteExhaustedCookie(wrappedWriter)
|
||||
}
|
||||
if !limited {
|
||||
next.ServeHTTP(wrappedWriter, request)
|
||||
}
|
||||
tracingCtx, writeSpan := tracing.NewNamedSpan(tracingCtx, "writeAccess")
|
||||
@@ -75,6 +108,7 @@ func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
|
||||
if err != nil {
|
||||
logging.WithError(err).WithField("url", requestURL).Warning("failed to unescape request url")
|
||||
}
|
||||
instance := authz.GetInstance(tracingCtx)
|
||||
a.svc.Handle(tracingCtx, &access.Record{
|
||||
LogDate: time.Now(),
|
||||
Protocol: access.HTTP,
|
||||
@@ -90,24 +124,6 @@ func (a *AccessInterceptor) Handle(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func SetExhaustedCookie(cookieHandler *http_utils.CookieHandler, writer http.ResponseWriter, cookieConfig *AccessConfig, request *http.Request) {
|
||||
cookieValue := "true"
|
||||
host := request.Header.Get(middleware.HTTP1Host)
|
||||
domain := host
|
||||
if strings.ContainsAny(host, ":") {
|
||||
var err error
|
||||
domain, _, err = net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
logging.WithError(err).WithField("host", host).Warning("failed to extract cookie domain from request host")
|
||||
}
|
||||
}
|
||||
cookieHandler.SetCookie(writer, cookieConfig.ExhaustedCookieKey, domain, cookieValue)
|
||||
}
|
||||
|
||||
func DeleteExhaustedCookie(cookieHandler *http_utils.CookieHandler, writer http.ResponseWriter, request *http.Request, cookieConfig *AccessConfig) {
|
||||
cookieHandler.DeleteCookie(writer, request, cookieConfig.ExhaustedCookieKey)
|
||||
}
|
||||
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
z_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/form"
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/github"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/google"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
const (
|
||||
HandlerPrefix = "/idps"
|
||||
callbackPath = "/callback"
|
||||
|
||||
paramIntentID = "id"
|
||||
paramToken = "token"
|
||||
paramUserID = "user"
|
||||
paramError = "error"
|
||||
paramErrorDescription = "error_description"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
commands *command.Commands
|
||||
queries *query.Queries
|
||||
parser *form.Parser
|
||||
encryptionAlgorithm crypto.EncryptionAlgorithm
|
||||
callbackURL func(ctx context.Context) string
|
||||
}
|
||||
|
||||
type externalIDPCallbackData struct {
|
||||
State string `schema:"state"`
|
||||
Code string `schema:"code"`
|
||||
Error string `schema:"error"`
|
||||
ErrorDescription string `schema:"error_description"`
|
||||
}
|
||||
|
||||
// CallbackURL generates the instance specific URL to the IDP callback handler
|
||||
func CallbackURL(externalSecure bool) func(ctx context.Context) string {
|
||||
return func(ctx context.Context) string {
|
||||
return http_utils.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix + callbackPath
|
||||
}
|
||||
}
|
||||
|
||||
func NewHandler(
|
||||
commands *command.Commands,
|
||||
queries *query.Queries,
|
||||
encryptionAlgorithm crypto.EncryptionAlgorithm,
|
||||
externalSecure bool,
|
||||
instanceInterceptor func(next http.Handler) http.Handler,
|
||||
) http.Handler {
|
||||
h := &Handler{
|
||||
commands: commands,
|
||||
queries: queries,
|
||||
parser: form.NewParser(),
|
||||
encryptionAlgorithm: encryptionAlgorithm,
|
||||
callbackURL: CallbackURL(externalSecure),
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
router.Use(instanceInterceptor)
|
||||
router.HandleFunc(callbackPath, h.handleCallback)
|
||||
return router
|
||||
}
|
||||
|
||||
func (h *Handler) handleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := h.parseCallbackRequest(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
intent := h.getActiveIntent(w, r, data.State)
|
||||
if intent == nil {
|
||||
// if we didn't get an active intent the error was already handled (either redirected or display directly)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
// the provider might have returned an error
|
||||
if data.Error != "" {
|
||||
cmdErr := h.commands.FailIDPIntent(ctx, intent, reason(data.Error, data.ErrorDescription))
|
||||
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent")
|
||||
redirectToFailureURL(w, r, intent, data.Error, data.ErrorDescription)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := h.commands.GetProvider(ctx, intent.IDPID, h.callbackURL(ctx))
|
||||
if err != nil {
|
||||
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
|
||||
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent")
|
||||
redirectToFailureURLErr(w, r, intent, err)
|
||||
return
|
||||
}
|
||||
|
||||
idpUser, idpSession, err := h.fetchIDPUser(ctx, provider, data.Code)
|
||||
if err != nil {
|
||||
cmdErr := h.commands.FailIDPIntent(ctx, intent, err.Error())
|
||||
logging.WithFields("intent", intent.AggregateID).OnError(cmdErr).Error("failed to push failed event on idp intent")
|
||||
redirectToFailureURLErr(w, r, intent, err)
|
||||
return
|
||||
}
|
||||
userID, err := h.checkExternalUser(ctx, intent.IDPID, idpUser.GetID())
|
||||
logging.WithFields("intent", intent.AggregateID).OnError(err).Error("could not check if idp user already exists")
|
||||
|
||||
token, err := h.commands.SucceedIDPIntent(ctx, intent, idpUser, idpSession, userID)
|
||||
if err != nil {
|
||||
redirectToFailureURLErr(w, r, intent, z_errs.ThrowInternal(err, "IDP-JdD3g", "Errors.Intent.TokenCreationFailed"))
|
||||
return
|
||||
}
|
||||
redirectToSuccessURL(w, r, intent, token, userID)
|
||||
}
|
||||
|
||||
func (h *Handler) parseCallbackRequest(r *http.Request) (*externalIDPCallbackData, error) {
|
||||
data := new(externalIDPCallbackData)
|
||||
err := h.parser.Parse(r, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data.State == "" {
|
||||
return nil, z_errs.ThrowInvalidArgument(nil, "IDP-Hk38e", "Errors.Intent.StateMissing")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (h *Handler) getActiveIntent(w http.ResponseWriter, r *http.Request, state string) *command.IDPIntentWriteModel {
|
||||
intent, err := h.commands.GetIntentWriteModel(r.Context(), state, "")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
if intent.State == domain.IDPIntentStateUnspecified {
|
||||
http.Error(w, reason("IDP-Hk38e", "Errors.Intent.NotStarted"), http.StatusBadRequest)
|
||||
return nil
|
||||
}
|
||||
if intent.State != domain.IDPIntentStateStarted {
|
||||
redirectToFailureURL(w, r, intent, "IDP-Sfrgs", "Errors.Intent.NotStarted")
|
||||
return nil
|
||||
}
|
||||
return intent
|
||||
}
|
||||
|
||||
func redirectToSuccessURL(w http.ResponseWriter, r *http.Request, intent *command.IDPIntentWriteModel, token, userID string) {
|
||||
queries := intent.SuccessURL.Query()
|
||||
queries.Set(paramIntentID, intent.AggregateID)
|
||||
queries.Set(paramToken, token)
|
||||
if userID != "" {
|
||||
queries.Set(paramUserID, userID)
|
||||
}
|
||||
intent.SuccessURL.RawQuery = queries.Encode()
|
||||
http.Redirect(w, r, intent.SuccessURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
func redirectToFailureURLErr(w http.ResponseWriter, r *http.Request, i *command.IDPIntentWriteModel, err error) {
|
||||
msg := err.Error()
|
||||
var description string
|
||||
zErr := new(z_errs.CaosError)
|
||||
if errors.As(err, &zErr) {
|
||||
msg = zErr.GetID()
|
||||
description = zErr.GetMessage() // TODO: i18n?
|
||||
}
|
||||
redirectToFailureURL(w, r, i, msg, description)
|
||||
}
|
||||
|
||||
func redirectToFailureURL(w http.ResponseWriter, r *http.Request, i *command.IDPIntentWriteModel, err, description string) {
|
||||
queries := i.FailureURL.Query()
|
||||
queries.Set(paramIntentID, i.AggregateID)
|
||||
queries.Set(paramError, err)
|
||||
queries.Set(paramErrorDescription, description)
|
||||
i.FailureURL.RawQuery = queries.Encode()
|
||||
http.Redirect(w, r, i.FailureURL.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *Handler) fetchIDPUser(ctx context.Context, identityProvider idp.Provider, code string) (user idp.User, idpTokens idp.Session, err error) {
|
||||
var session idp.Session
|
||||
switch provider := identityProvider.(type) {
|
||||
case *oauth.Provider:
|
||||
session = &oauth.Session{Provider: provider, Code: code}
|
||||
case *openid.Provider:
|
||||
session = &openid.Session{Provider: provider, Code: code}
|
||||
case *azuread.Provider:
|
||||
session = &oauth.Session{Provider: provider.Provider, Code: code}
|
||||
case *github.Provider:
|
||||
session = &oauth.Session{Provider: provider.Provider, Code: code}
|
||||
case *gitlab.Provider:
|
||||
session = &openid.Session{Provider: provider.Provider, Code: code}
|
||||
case *google.Provider:
|
||||
session = &openid.Session{Provider: provider.Provider, Code: code}
|
||||
case *jwt.Provider, *ldap.Provider:
|
||||
return nil, nil, z_errs.ThrowInvalidArgument(nil, "IDP-52jmn", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
default:
|
||||
return nil, nil, z_errs.ThrowUnimplemented(nil, "IDP-SSDg", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
}
|
||||
|
||||
user, err = session.FetchUser(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return user, session, nil
|
||||
}
|
||||
|
||||
func (h *Handler) checkExternalUser(ctx context.Context, idpID, externalUserID string) (userID string, err error) {
|
||||
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
queries := []query.SearchQuery{
|
||||
idQuery, externalIDQuery,
|
||||
}
|
||||
links, err := h.queries.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(links.Links) != 1 {
|
||||
return "", nil
|
||||
}
|
||||
return links.Links[0].UserID, nil
|
||||
}
|
||||
|
||||
func reason(err, description string) string {
|
||||
if description == "" {
|
||||
return err
|
||||
}
|
||||
return err + ": " + description
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package idp
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
z_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/form"
|
||||
)
|
||||
|
||||
func Test_redirectToSuccessURL(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
userID string
|
||||
token string
|
||||
failureURL string
|
||||
successURL string
|
||||
}
|
||||
type res struct {
|
||||
want string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"redirect",
|
||||
args{
|
||||
id: "id",
|
||||
token: "token",
|
||||
failureURL: "https://example.com/failure",
|
||||
successURL: "https://example.com/success",
|
||||
},
|
||||
res{
|
||||
"https://example.com/success?id=id&token=token",
|
||||
},
|
||||
},
|
||||
{
|
||||
"redirect with userID",
|
||||
args{
|
||||
id: "id",
|
||||
userID: "user",
|
||||
token: "token",
|
||||
failureURL: "https://example.com/failure",
|
||||
successURL: "https://example.com/success",
|
||||
},
|
||||
res{
|
||||
"https://example.com/success?id=id&token=token&user=user",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
|
||||
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
|
||||
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
|
||||
|
||||
redirectToSuccessURL(resp, req, wm, tt.args.token, tt.args.userID)
|
||||
assert.Equal(t, tt.res.want, resp.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_redirectToFailureURL(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
failureURL string
|
||||
successURL string
|
||||
err string
|
||||
desc string
|
||||
}
|
||||
type res struct {
|
||||
want string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"redirect",
|
||||
args{
|
||||
id: "id",
|
||||
failureURL: "https://example.com/failure",
|
||||
successURL: "https://example.com/success",
|
||||
},
|
||||
res{
|
||||
"https://example.com/failure?error=&error_description=&id=id",
|
||||
},
|
||||
},
|
||||
{
|
||||
"redirect with error",
|
||||
args{
|
||||
id: "id",
|
||||
failureURL: "https://example.com/failure",
|
||||
successURL: "https://example.com/success",
|
||||
err: "test",
|
||||
desc: "testdesc",
|
||||
},
|
||||
res{
|
||||
"https://example.com/failure?error=test&error_description=testdesc&id=id",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
|
||||
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
|
||||
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
|
||||
|
||||
redirectToFailureURL(resp, req, wm, tt.args.err, tt.args.desc)
|
||||
assert.Equal(t, tt.res.want, resp.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_redirectToFailureURLErr(t *testing.T) {
|
||||
type args struct {
|
||||
id string
|
||||
failureURL string
|
||||
successURL string
|
||||
err error
|
||||
}
|
||||
type res struct {
|
||||
want string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"redirect with error",
|
||||
args{
|
||||
id: "id",
|
||||
failureURL: "https://example.com/failure",
|
||||
successURL: "https://example.com/success",
|
||||
err: z_errors.ThrowError(nil, "test", "testdesc"),
|
||||
},
|
||||
res{
|
||||
"https://example.com/failure?error=test&error_description=testdesc&id=id",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "http://example.com", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
wm := command.NewIDPIntentWriteModel(tt.args.id, tt.args.id)
|
||||
wm.FailureURL, _ = url.Parse(tt.args.failureURL)
|
||||
wm.SuccessURL, _ = url.Parse(tt.args.successURL)
|
||||
|
||||
redirectToFailureURLErr(resp, req, wm, tt.args.err)
|
||||
assert.Equal(t, tt.res.want, resp.Header().Get("Location"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseCallbackRequest(t *testing.T) {
|
||||
type args struct {
|
||||
url string
|
||||
}
|
||||
type res struct {
|
||||
want *externalIDPCallbackData
|
||||
err bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"no state",
|
||||
args{
|
||||
url: "https://example.com?state=&code=code&error=error&error_description=desc",
|
||||
},
|
||||
res{
|
||||
err: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"parse",
|
||||
args{
|
||||
url: "https://example.com?state=state&code=code&error=error&error_description=desc",
|
||||
},
|
||||
res{
|
||||
want: &externalIDPCallbackData{
|
||||
State: "state",
|
||||
Code: "code",
|
||||
Error: "error",
|
||||
ErrorDescription: "desc",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", tt.args.url, nil)
|
||||
handler := Handler{parser: form.NewParser()}
|
||||
|
||||
data, err := handler.parseCallbackRequest(req)
|
||||
if tt.res.err {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
assert.Equal(t, tt.res.want, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func (f *file) Stat() (_ fs.FileInfo, err error) {
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, callDurationInterceptor, instanceHandler, accessInterceptor func(http.Handler) http.Handler, customerPortal string) (http.Handler, error) {
|
||||
func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, callDurationInterceptor, instanceHandler func(http.Handler) http.Handler, limitingAccessInterceptor *middleware.AccessInterceptor, customerPortal string) (http.Handler, error) {
|
||||
fSys, err := fs.Sub(static, "static")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -106,20 +106,27 @@ func Start(config Config, externalSecure bool, issuer op.IssuerFromRequest, call
|
||||
|
||||
handler := mux.NewRouter()
|
||||
|
||||
handler.Use(callDurationInterceptor, instanceHandler, security, accessInterceptor)
|
||||
handler.Use(callDurationInterceptor, instanceHandler, security, limitingAccessInterceptor.WithoutLimiting().Handle)
|
||||
handler.Handle(envRequestPath, middleware.TelemetryHandler()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
url := http_util.BuildOrigin(r.Host, externalSecure)
|
||||
instance := authz.GetInstance(r.Context())
|
||||
ctx := r.Context()
|
||||
instance := authz.GetInstance(ctx)
|
||||
instanceMgmtURL, err := templateInstanceManagementURL(config.InstanceManagementURL, instance)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to template instance management url for console: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
environmentJSON, err := createEnvironmentJSON(url, issuer(r), instance.ConsoleClientID(), customerPortal, instanceMgmtURL)
|
||||
exhausted := limitingAccessInterceptor.Limit(ctx)
|
||||
environmentJSON, err := createEnvironmentJSON(url, issuer(r), instance.ConsoleClientID(), customerPortal, instanceMgmtURL, exhausted)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("unable to marshal env for console: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if exhausted {
|
||||
limitingAccessInterceptor.SetExhaustedCookie(w, r)
|
||||
} else {
|
||||
limitingAccessInterceptor.DeleteExhaustedCookie(w)
|
||||
}
|
||||
_, err = w.Write(environmentJSON)
|
||||
logging.OnError(err).Error("error serving environment.json")
|
||||
})))
|
||||
@@ -148,19 +155,21 @@ func csp() *middleware.CSP {
|
||||
return &csp
|
||||
}
|
||||
|
||||
func createEnvironmentJSON(api, issuer, clientID, customerPortal, instanceMgmtUrl string) ([]byte, error) {
|
||||
func createEnvironmentJSON(api, issuer, clientID, customerPortal, instanceMgmtUrl string, exhausted bool) ([]byte, error) {
|
||||
environment := struct {
|
||||
API string `json:"api,omitempty"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
ClientID string `json:"clientid,omitempty"`
|
||||
CustomerPortal string `json:"customer_portal,omitempty"`
|
||||
InstanceManagementURL string `json:"instance_management_url,omitempty"`
|
||||
Exhausted bool `json:"exhausted,omitempty"`
|
||||
}{
|
||||
API: api,
|
||||
Issuer: issuer,
|
||||
ClientID: clientID,
|
||||
CustomerPortal: customerPortal,
|
||||
InstanceManagementURL: instanceMgmtUrl,
|
||||
Exhausted: exhausted,
|
||||
}
|
||||
return json.Marshal(environment)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
instance_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
@@ -31,7 +32,7 @@ type Commands struct {
|
||||
httpClient *http.Client
|
||||
|
||||
checkPermission domain.PermissionCheck
|
||||
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||
newCode cryptoCodeFunc
|
||||
|
||||
eventstore *eventstore.Eventstore
|
||||
static static.Storage
|
||||
@@ -108,7 +109,7 @@ func StartCommands(
|
||||
webauthnConfig: webAuthN,
|
||||
httpClient: httpClient,
|
||||
checkPermission: permissionCheck,
|
||||
newEmailCode: newEmailCode,
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
sessionTokenCreator: sessionTokenCreator(idGenerator, sessionAlg),
|
||||
sessionTokenVerifier: sessionTokenVerifier,
|
||||
}
|
||||
@@ -122,6 +123,7 @@ func StartCommands(
|
||||
action.RegisterEventMappers(repo.eventstore)
|
||||
quota.RegisterEventMappers(repo.eventstore)
|
||||
session.RegisterEventMappers(repo.eventstore)
|
||||
idpintent.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.userPasswordAlg = crypto.NewBCrypt(defaults.SecretGenerators.PasswordSaltCost)
|
||||
repo.machineKeySize = int(defaults.SecretGenerators.MachineKeySize)
|
||||
@@ -139,11 +141,21 @@ func StartCommands(
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func AppendAndReduce(object interface {
|
||||
type AppendReducer interface {
|
||||
AppendEvents(...eventstore.Event)
|
||||
// TODO: Why is it allowed to return an error here?
|
||||
Reduce() error
|
||||
}, events ...eventstore.Event) error {
|
||||
}
|
||||
|
||||
func (c *Commands) pushAppendAndReduce(ctx context.Context, object AppendReducer, cmds ...eventstore.Command) error {
|
||||
events, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return AppendAndReduce(object, events...)
|
||||
}
|
||||
|
||||
func AppendAndReduce(object AppendReducer, events ...eventstore.Event) error {
|
||||
object.AppendEvents(events...)
|
||||
return object.Reduce()
|
||||
}
|
||||
|
||||
+28
-18
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
type cryptoCodeFunc func(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error)
|
||||
|
||||
type CryptoCodeWithExpiry struct {
|
||||
Crypted *crypto.CryptoValue
|
||||
Plain string
|
||||
@@ -17,42 +19,50 @@ type CryptoCodeWithExpiry struct {
|
||||
}
|
||||
|
||||
func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
|
||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||
gen, config, err := secretGenerator(ctx, filter, typ, alg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := new(CryptoCodeWithExpiry)
|
||||
switch a := alg.(type) {
|
||||
case crypto.HashAlgorithm:
|
||||
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
||||
case crypto.EncryptionAlgorithm:
|
||||
code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
||||
default:
|
||||
return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal")
|
||||
}
|
||||
crypted, plain, err := crypto.NewCode(gen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CryptoCodeWithExpiry{
|
||||
Crypted: crypted,
|
||||
Plain: plain,
|
||||
Expiry: config.Expiry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
code.Expiry = config.Expiry
|
||||
return code, nil
|
||||
func verifyCryptoCode(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto, creation time.Time, expiry time.Duration, crypted *crypto.CryptoValue, plain string) error {
|
||||
gen, _, err := secretGenerator(ctx, filter, typ, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return crypto.VerifyCode(creation, expiry, crypted, plain, gen)
|
||||
}
|
||||
|
||||
func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) {
|
||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||
gen, _, err := secretGenerator(ctx, filter, typ, alg)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return crypto.NewCode(gen)
|
||||
}
|
||||
|
||||
func secretGenerator(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (crypto.Generator, *crypto.GeneratorConfig, error) {
|
||||
config, err := secretGeneratorConfig(ctx, filter, typ)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
switch a := alg.(type) {
|
||||
case crypto.HashAlgorithm:
|
||||
return crypto.NewCode(crypto.NewHashGenerator(*config, a))
|
||||
return crypto.NewHashGenerator(*config, a), config, nil
|
||||
case crypto.EncryptionAlgorithm:
|
||||
return crypto.NewCode(crypto.NewEncryptionGenerator(*config, a))
|
||||
return crypto.NewEncryptionGenerator(*config, a), config, nil
|
||||
default:
|
||||
return nil, nil, errors.ThrowInternalf(nil, "COMMA-RreV6", "Errors.Internal unsupported crypto algorithm type %T", a)
|
||||
}
|
||||
|
||||
return nil, "", errors.ThrowInvalidArgument(nil, "V2-NGESt", "Errors.Internal")
|
||||
}
|
||||
|
||||
func secretGeneratorConfig(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType) (*crypto.GeneratorConfig, error) {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
)
|
||||
|
||||
func mockCode(code string, exp time.Duration) cryptoCodeFunc {
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer, _ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) {
|
||||
return &CryptoCodeWithExpiry{
|
||||
Crypted: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte(code),
|
||||
},
|
||||
Plain: code,
|
||||
Expiry: exp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
testGeneratorConfig = crypto.GeneratorConfig{
|
||||
Length: 12,
|
||||
Expiry: 60000000000,
|
||||
IncludeLowerLetters: true,
|
||||
IncludeUpperLetters: true,
|
||||
IncludeDigits: true,
|
||||
IncludeSymbols: true,
|
||||
}
|
||||
)
|
||||
|
||||
func testSecretGeneratorAddedEvent(typ domain.SecretGeneratorType) *instance.SecretGeneratorAddedEvent {
|
||||
return instance.NewSecretGeneratorAddedEvent(context.Background(),
|
||||
&instance.NewAggregate("inst1").Aggregate, typ,
|
||||
testGeneratorConfig.Length,
|
||||
testGeneratorConfig.Expiry,
|
||||
testGeneratorConfig.IncludeLowerLetters,
|
||||
testGeneratorConfig.IncludeUpperLetters,
|
||||
testGeneratorConfig.IncludeDigits,
|
||||
testGeneratorConfig.IncludeSymbols,
|
||||
)
|
||||
}
|
||||
|
||||
func Test_newCryptoCode(t *testing.T) {
|
||||
type args struct {
|
||||
typ domain.SecretGeneratorType
|
||||
alg crypto.Crypto
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore *eventstore.Eventstore
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter config error",
|
||||
eventstore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventstore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := newCryptoCodeWithExpiry(context.Background(), tt.eventstore.Filter, tt.args.typ, tt.args.alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.wantErr == nil {
|
||||
require.NotNil(t, got)
|
||||
assert.NotNil(t, got.Crypted)
|
||||
assert.NotEmpty(t, got)
|
||||
assert.Equal(t, testGeneratorConfig.Expiry, got.Expiry)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_verifyCryptoCode(t *testing.T) {
|
||||
es := eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
))
|
||||
code, err := newCryptoCodeWithExpiry(context.Background(), es.Filter, domain.SecretGeneratorTypeVerifyEmailCode, crypto.CreateMockHashAlg(gomock.NewController(t)))
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
typ domain.SecretGeneratorType
|
||||
alg crypto.Crypto
|
||||
expiry time.Duration
|
||||
crypted *crypto.CryptoValue
|
||||
plain string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventsore *eventstore.Eventstore
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "filter config error",
|
||||
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
expiry: code.Expiry,
|
||||
crypted: code.Crypted,
|
||||
plain: code.Plain,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
expiry: code.Expiry,
|
||||
crypted: code.Crypted,
|
||||
plain: code.Plain,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wrong plain",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
expiry: code.Expiry,
|
||||
crypted: code.Crypted,
|
||||
plain: "wrong",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := verifyCryptoCode(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg, time.Now(), tt.args.expiry, tt.args.crypted, tt.args.plain)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_secretGenerator(t *testing.T) {
|
||||
type args struct {
|
||||
typ domain.SecretGeneratorType
|
||||
alg crypto.Crypto
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
eventsore *eventstore.Eventstore
|
||||
args args
|
||||
want crypto.Generator
|
||||
wantConf *crypto.GeneratorConfig
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter config error",
|
||||
eventsore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "hash generator",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
},
|
||||
want: crypto.NewHashGenerator(testGeneratorConfig, crypto.CreateMockHashAlg(gomock.NewController(t))),
|
||||
wantConf: &testGeneratorConfig,
|
||||
},
|
||||
{
|
||||
name: "encryption generator",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
want: crypto.NewEncryptionGenerator(testGeneratorConfig, crypto.CreateMockEncryptionAlg(gomock.NewController(t))),
|
||||
wantConf: &testGeneratorConfig,
|
||||
},
|
||||
{
|
||||
name: "unsupported type",
|
||||
eventsore: eventstoreExpect(t, expectFilter(
|
||||
eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypeVerifyEmailCode)),
|
||||
)),
|
||||
args: args{
|
||||
typ: domain.SecretGeneratorTypeVerifyEmailCode,
|
||||
alg: nil,
|
||||
},
|
||||
wantErr: errors.ThrowInternalf(nil, "COMMA-RreV6", "Errors.Internal unsupported crypto algorithm type %T", nil),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, gotConf, err := secretGenerator(context.Background(), tt.eventsore.Filter, tt.args.typ, tt.args.alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.IsType(t, tt.want, got)
|
||||
assert.Equal(t, tt.wantConf, gotConf)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,6 @@ func (e *Email) Validate() error {
|
||||
return e.Address.Validate()
|
||||
}
|
||||
|
||||
func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
||||
func (c *Commands) newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return c.newCode(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
)
|
||||
|
||||
@@ -139,3 +140,37 @@ func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id,
|
||||
}
|
||||
return instanceWriteModel.State.Exists(), nil
|
||||
}
|
||||
|
||||
func IDPProviderWriteModel(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (_ *AllIDPWriteModel, err error) {
|
||||
writeModel := NewIDPTypeWriteModel(id)
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-as02jin", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err := writeModel.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allWriteModel, err := NewAllIDPWriteModel(
|
||||
writeModel.ResourceOwner,
|
||||
writeModel.ResourceOwner == writeModel.InstanceID,
|
||||
writeModel.ID,
|
||||
writeModel.Type,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err = filter(ctx, allWriteModel.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allWriteModel.AppendEvents(events...)
|
||||
if err := allWriteModel.Reduce(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return allWriteModel, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
)
|
||||
|
||||
func (c *Commands) prepareCreateIntent(writeModel *IDPIntentWriteModel, idpID string, successURL, failureURL string) preparation.Validation {
|
||||
return func() (_ preparation.CreateCommands, err error) {
|
||||
if idpID == "" {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing")
|
||||
}
|
||||
successURL, err := url.Parse(successURL)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-x8j3bk", "Errors.Intent.SuccessURLMissing")
|
||||
}
|
||||
failureURL, err := url.Parse(failureURL)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInvalidArgument(nil, "COMMAND-x8j4bk", "Errors.Intent.FailureURLMissing")
|
||||
}
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
|
||||
err = getIDPIntentWriteModel(ctx, writeModel, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exists, err := ExistsIDP(ctx, filter, idpID, writeModel.ResourceOwner)
|
||||
if !exists || err != nil {
|
||||
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
return []eventstore.Command{
|
||||
idpintent.NewStartedEvent(ctx, writeModel.aggregate, successURL, failureURL, idpID),
|
||||
}, nil
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Commands) CreateIntent(ctx context.Context, idpID, successURL, failureURL, resourceOwner string) (string, *domain.ObjectDetails, error) {
|
||||
id, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
writeModel := NewIDPIntentWriteModel(id, resourceOwner)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, c.prepareCreateIntent(writeModel, idpID, successURL, failureURL))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
pushedEvents, err := c.eventstore.Push(ctx, cmds...)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
err = AppendAndReduce(writeModel, pushedEvents...)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return id, writeModelToObjectDetails(&writeModel.WriteModel), nil
|
||||
}
|
||||
|
||||
func (c *Commands) GetProvider(ctx context.Context, idpID, callbackURL string) (idp.Provider, error) {
|
||||
writeModel, err := IDPProviderWriteModel(ctx, c.eventstore.Filter, idpID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel.ToProvider(callbackURL, c.idpConfigEncryption)
|
||||
}
|
||||
|
||||
func (c *Commands) AuthURLFromProvider(ctx context.Context, idpID, state, callbackURL string) (string, error) {
|
||||
provider, err := c.GetProvider(ctx, idpID, callbackURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
session, err := provider.BeginAuth(ctx, state)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return session.GetAuthURL(), nil
|
||||
}
|
||||
|
||||
func getIDPIntentWriteModel(ctx context.Context, writeModel *IDPIntentWriteModel, filter preparation.FilterToQueryReducer) error {
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
return writeModel.Reduce()
|
||||
}
|
||||
|
||||
func (c *Commands) SucceedIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, idpUser idp.User, idpSession idp.Session, userID string) (string, error) {
|
||||
token, err := c.idpConfigEncryption.Encrypt([]byte(writeModel.AggregateID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
accessToken, idToken, err := tokensForSucceededIDPIntent(idpSession, c.idpConfigEncryption)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
idpInfo, err := json.Marshal(idpUser)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd, err := idpintent.NewSucceededEvent(
|
||||
ctx,
|
||||
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
|
||||
idpInfo,
|
||||
userID,
|
||||
accessToken,
|
||||
idToken,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = c.pushAppendAndReduce(ctx, writeModel, cmd)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(token), nil
|
||||
}
|
||||
|
||||
func (c *Commands) FailIDPIntent(ctx context.Context, writeModel *IDPIntentWriteModel, reason string) error {
|
||||
cmd := idpintent.NewFailedEvent(
|
||||
ctx,
|
||||
&idpintent.NewAggregate(writeModel.AggregateID, writeModel.ResourceOwner).Aggregate,
|
||||
reason,
|
||||
)
|
||||
_, err := c.eventstore.Push(ctx, cmd)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Commands) GetIntentWriteModel(ctx context.Context, id, resourceOwner string) (*IDPIntentWriteModel, error) {
|
||||
writeModel := NewIDPIntentWriteModel(id, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, writeModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writeModel, err
|
||||
}
|
||||
|
||||
// tokensForSucceededIDPIntent extracts the oidc.Tokens if available (and encrypts the access_token) for the succeeded event payload
|
||||
func tokensForSucceededIDPIntent(session idp.Session, encryptionAlg crypto.EncryptionAlgorithm) (*crypto.CryptoValue, string, error) {
|
||||
var tokens *oidc.Tokens[*oidc.IDTokenClaims]
|
||||
switch s := session.(type) {
|
||||
case *oauth.Session:
|
||||
tokens = s.Tokens
|
||||
case *openid.Session:
|
||||
tokens = s.Tokens
|
||||
case *jwt.Session:
|
||||
tokens = s.Tokens
|
||||
default:
|
||||
return nil, "", nil
|
||||
}
|
||||
if tokens.Token == nil || tokens.AccessToken == "" {
|
||||
return nil, tokens.IDToken, nil
|
||||
}
|
||||
accessToken, err := crypto.Encrypt([]byte(tokens.AccessToken), encryptionAlg)
|
||||
return accessToken, tokens.IDToken, err
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
)
|
||||
|
||||
type IDPIntentWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
SuccessURL *url.URL
|
||||
FailureURL *url.URL
|
||||
IDPID string
|
||||
IDPUser []byte
|
||||
IDPAccessToken *crypto.CryptoValue
|
||||
IDPIDToken string
|
||||
UserID string
|
||||
|
||||
State domain.IDPIntentState
|
||||
aggregate *eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewIDPIntentWriteModel(id, resourceOwner string) *IDPIntentWriteModel {
|
||||
return &IDPIntentWriteModel{
|
||||
WriteModel: eventstore.WriteModel{
|
||||
AggregateID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
aggregate: &idpintent.NewAggregate(id, resourceOwner).Aggregate,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *IDPIntentWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *idpintent.StartedEvent:
|
||||
wm.reduceStartedEvent(e)
|
||||
case *idpintent.SucceededEvent:
|
||||
wm.reduceSucceededEvent(e)
|
||||
case *idpintent.FailedEvent:
|
||||
wm.reduceFailedEvent(e)
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *IDPIntentWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(wm.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(idpintent.AggregateType).
|
||||
AggregateIDs(wm.AggregateID).
|
||||
EventTypes(
|
||||
idpintent.StartedEventType,
|
||||
idpintent.SucceededEventType,
|
||||
idpintent.FailedEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
||||
|
||||
func (wm *IDPIntentWriteModel) reduceStartedEvent(e *idpintent.StartedEvent) {
|
||||
wm.SuccessURL = e.SuccessURL
|
||||
wm.FailureURL = e.FailureURL
|
||||
wm.IDPID = e.IDPID
|
||||
wm.State = domain.IDPIntentStateStarted
|
||||
}
|
||||
|
||||
func (wm *IDPIntentWriteModel) reduceSucceededEvent(e *idpintent.SucceededEvent) {
|
||||
wm.UserID = e.UserID
|
||||
wm.IDPUser = e.IDPUser
|
||||
wm.IDPAccessToken = e.IDPAccessToken
|
||||
wm.IDPIDToken = e.IDPIDToken
|
||||
wm.State = domain.IDPIntentStateSucceeded
|
||||
}
|
||||
|
||||
func (wm *IDPIntentWriteModel) reduceFailedEvent(e *idpintent.FailedEvent) {
|
||||
wm.State = domain.IDPIntentStateFailed
|
||||
}
|
||||
@@ -0,0 +1,667 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
z_errors "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
"github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
rep_idp "github.com/zitadel/zitadel/internal/repository/idp"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
)
|
||||
|
||||
func TestCommands_CreateIntent(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
idpID string
|
||||
successURL string
|
||||
failureURL string
|
||||
resourceOwner string
|
||||
}
|
||||
type res struct {
|
||||
intentID string
|
||||
details *domain.ObjectDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"error no id generator",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: mock.NewIDGeneratorExpectError(t, z_errors.ThrowInternal(nil, "", "error id")),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInternal(nil, "", "error id"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"error no idpID",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"error no successURL",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
successURL: ":",
|
||||
failureURL: "https://failure.url",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j3bk", "Errors.Intent.SuccessURLMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"error no failureURL",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
successURL: "https://success.url",
|
||||
failureURL: ":",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j4bk", "Errors.Intent.FailureURLMissing"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"error idp not existing",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowPreconditionFailed(nil, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"push",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(),
|
||||
expectFilter(),
|
||||
expectFilter(
|
||||
eventFromEventPusher(
|
||||
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("ro").Aggregate,
|
||||
"idp",
|
||||
"name",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
"auth",
|
||||
"token",
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
func() eventstore.Command {
|
||||
success, _ := url.Parse("https://success.url")
|
||||
failure, _ := url.Parse("https://failure.url")
|
||||
return idpintent.NewStartedEvent(
|
||||
context.Background(),
|
||||
&idpintent.NewAggregate("id", "ro").Aggregate,
|
||||
success,
|
||||
failure,
|
||||
"idp",
|
||||
)
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
idGenerator: mock.ExpectID(t, "id"),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
resourceOwner: "ro",
|
||||
idpID: "idp",
|
||||
successURL: "https://success.url",
|
||||
failureURL: "https://failure.url",
|
||||
},
|
||||
res{
|
||||
intentID: "id",
|
||||
details: &domain.ObjectDetails{ResourceOwner: "ro"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
intentID, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.resourceOwner)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.intentID, intentID)
|
||||
assert.Equal(t, tt.res.details, details)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_AuthURLFromProvider(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
secretCrypto crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
idpID string
|
||||
state string
|
||||
callbackURL string
|
||||
}
|
||||
type res struct {
|
||||
authURL string
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"idp not existing",
|
||||
fields{
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowPreconditionFailed(nil, "", ""),
|
||||
},
|
||||
},
|
||||
{
|
||||
"idp removed",
|
||||
fields{
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance",
|
||||
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
||||
"idp",
|
||||
"name",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
"auth",
|
||||
"token",
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance",
|
||||
instance.NewIDPRemovedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
||||
"idp",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"push",
|
||||
fields{
|
||||
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance",
|
||||
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
||||
"idp",
|
||||
"name",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
"auth",
|
||||
"token",
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
expectFilter(
|
||||
eventFromEventPusherWithInstanceID(
|
||||
"instance",
|
||||
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
||||
"idp",
|
||||
"name",
|
||||
"clientID",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("clientSecret"),
|
||||
},
|
||||
"auth",
|
||||
"token",
|
||||
"user",
|
||||
"idAttribute",
|
||||
nil,
|
||||
rep_idp.Options{},
|
||||
)),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
||||
idpID: "idp",
|
||||
state: "state",
|
||||
callbackURL: "url",
|
||||
},
|
||||
res{
|
||||
authURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idpConfigEncryption: tt.fields.secretCrypto,
|
||||
}
|
||||
authURL, err := c.AuthURLFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.authURL, authURL)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_SucceedIDPIntent(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idpConfigEncryption crypto.EncryptionAlgorithm
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
writeModel *IDPIntentWriteModel
|
||||
idpUser idp.User
|
||||
idpSession idp.Session
|
||||
userID string
|
||||
}
|
||||
type res struct {
|
||||
token string
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"encryption fails",
|
||||
fields{
|
||||
idpConfigEncryption: func() crypto.EncryptionAlgorithm {
|
||||
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
||||
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
|
||||
return m
|
||||
}(),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"token encryption fails",
|
||||
fields{
|
||||
idpConfigEncryption: func() crypto.EncryptionAlgorithm {
|
||||
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
||||
m.EXPECT().Encrypt(gomock.Any()).DoAndReturn(func(value []byte) ([]byte, error) {
|
||||
return value, nil
|
||||
})
|
||||
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
|
||||
return m
|
||||
}(),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
||||
idpSession: &oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res{
|
||||
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"push",
|
||||
fields{
|
||||
idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
func() eventstore.Command {
|
||||
event, _ := idpintent.NewSucceededEvent(
|
||||
context.Background(),
|
||||
&idpintent.NewAggregate("id", "ro").Aggregate,
|
||||
[]byte(`{"RawInfo":{"id":"id"}}`),
|
||||
"",
|
||||
&crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
"",
|
||||
)
|
||||
return event
|
||||
}(),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
||||
idpSession: &oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
},
|
||||
},
|
||||
idpUser: &oauth.UserMapper{
|
||||
RawInfo: map[string]interface{}{
|
||||
"id": "id",
|
||||
},
|
||||
},
|
||||
},
|
||||
res{
|
||||
token: "aWQ",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idpConfigEncryption: tt.fields.idpConfigEncryption,
|
||||
}
|
||||
got, err := c.SucceedIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.idpSession, tt.args.userID)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.token, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_FailIDPIntent(t *testing.T) {
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
writeModel *IDPIntentWriteModel
|
||||
reason string
|
||||
}
|
||||
type res struct {
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"push",
|
||||
fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectPush(
|
||||
eventPusherToEvents(
|
||||
idpintent.NewFailedEvent(
|
||||
context.Background(),
|
||||
&idpintent.NewAggregate("id", "ro").Aggregate,
|
||||
"reason",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
args{
|
||||
ctx: context.Background(),
|
||||
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
||||
reason: "reason",
|
||||
},
|
||||
res{
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
err := c.FailIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.reason)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
||||
type args struct {
|
||||
session idp.Session
|
||||
encryptionAlg crypto.EncryptionAlgorithm
|
||||
}
|
||||
type res struct {
|
||||
accessToken *crypto.CryptoValue
|
||||
idToken string
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
"no tokens",
|
||||
args{
|
||||
&ldap.Session{},
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: nil,
|
||||
idToken: "",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"token encryption fails",
|
||||
args{
|
||||
&oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
},
|
||||
},
|
||||
func() crypto.EncryptionAlgorithm {
|
||||
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
||||
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
|
||||
return m
|
||||
}(),
|
||||
},
|
||||
res{
|
||||
accessToken: nil,
|
||||
idToken: "",
|
||||
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"oauth tokens",
|
||||
args{
|
||||
&oauth.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
},
|
||||
},
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
idToken: "",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"oidc tokens",
|
||||
args{
|
||||
&openid.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
Token: &oauth2.Token{
|
||||
AccessToken: "accessToken",
|
||||
},
|
||||
IDToken: "idToken",
|
||||
},
|
||||
},
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("accessToken"),
|
||||
},
|
||||
idToken: "idToken",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
"jwt tokens",
|
||||
args{
|
||||
&jwt.Session{
|
||||
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
||||
IDToken: "idToken",
|
||||
},
|
||||
},
|
||||
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
},
|
||||
res{
|
||||
accessToken: nil,
|
||||
idToken: "idToken",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotAccessToken, gotIDToken, err := tokensForSucceededIDPIntent(tt.args.session, tt.args.encryptionAlg)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
assert.Equal(t, tt.res.accessToken, gotAccessToken)
|
||||
assert.Equal(t, tt.res.idToken, gotIDToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,31 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
providers "github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/github"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/google"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpconfig"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
type OAuthIDPWriteModel struct {
|
||||
@@ -133,6 +150,45 @@ func (wm *OAuthIDPWriteModel) NewChanges(
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (wm *OAuthIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config := &oauth2.Config{
|
||||
ClientID: wm.ClientID,
|
||||
ClientSecret: secret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: wm.AuthorizationEndpoint,
|
||||
TokenURL: wm.TokenEndpoint,
|
||||
},
|
||||
RedirectURL: callbackURL,
|
||||
Scopes: wm.Scopes,
|
||||
}
|
||||
opts := make([]oauth.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, oauth.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, oauth.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, oauth.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, oauth.WithAutoUpdate())
|
||||
}
|
||||
return oauth.New(
|
||||
config,
|
||||
wm.Name,
|
||||
wm.UserEndpoint,
|
||||
func() providers.User {
|
||||
return oauth.NewUserMapper(wm.IDAttribute)
|
||||
},
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type OIDCIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -286,6 +342,40 @@ func (wm *OIDCIDPWriteModel) reduceOIDCConfigChangedEvent(e *idpconfig.OIDCConfi
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *OIDCIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]oidc.ProviderOpts, 1, 6)
|
||||
opts[0] = oidc.WithSelectAccount()
|
||||
if wm.IsIDTokenMapping {
|
||||
opts = append(opts, oidc.WithIDTokenMapping())
|
||||
}
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, oidc.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, oidc.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, oidc.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, oidc.WithAutoUpdate())
|
||||
}
|
||||
return oidc.New(
|
||||
wm.Name,
|
||||
wm.Issuer,
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.Scopes,
|
||||
oidc.DefaultMapper,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type JWTIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -423,6 +513,31 @@ func (wm *JWTIDPWriteModel) reduceJWTConfigChangedEvent(e *idpconfig.JWTConfigCh
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *JWTIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
opts := make([]jwt.ProviderOpts, 0)
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, jwt.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, jwt.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, jwt.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, jwt.WithAutoUpdate())
|
||||
}
|
||||
return jwt.New(
|
||||
wm.Name,
|
||||
wm.Issuer,
|
||||
wm.JWTEndpoint,
|
||||
wm.KeysEndpoint,
|
||||
wm.HeaderName,
|
||||
idpAlg,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type AzureADIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -527,6 +642,43 @@ func (wm *AzureADIDPWriteModel) NewChanges(
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
func (wm *AzureADIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]azuread.ProviderOptions, 0, 3)
|
||||
if wm.IsEmailVerified {
|
||||
opts = append(opts, azuread.WithEmailVerified())
|
||||
}
|
||||
if wm.Tenant != "" {
|
||||
opts = append(opts, azuread.WithTenant(azuread.TenantType(wm.Tenant)))
|
||||
}
|
||||
oauthOpts := make([]oauth.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
|
||||
}
|
||||
if len(oauthOpts) > 0 {
|
||||
opts = append(opts, azuread.WithOAuthOptions(oauthOpts...))
|
||||
}
|
||||
return azuread.New(
|
||||
wm.Name,
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.Scopes,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type GitHubIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
@@ -614,6 +766,32 @@ func (wm *GitHubIDPWriteModel) NewChanges(
|
||||
}
|
||||
return changes, nil
|
||||
}
|
||||
func (wm *GitHubIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oauthOpts := make([]oauth.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
|
||||
}
|
||||
return github.New(
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.Scopes,
|
||||
oauthOpts...,
|
||||
)
|
||||
}
|
||||
|
||||
type GitHubEnterpriseIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
@@ -728,6 +906,37 @@ func (wm *GitHubEnterpriseIDPWriteModel) NewChanges(
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (wm *GitHubEnterpriseIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oauthOpts := make([]oauth.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
oauthOpts = append(oauthOpts, oauth.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
oauthOpts = append(oauthOpts, oauth.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
oauthOpts = append(oauthOpts, oauth.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
oauthOpts = append(oauthOpts, oauth.WithAutoUpdate())
|
||||
}
|
||||
return github.NewCustomURL(
|
||||
wm.Name,
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.AuthorizationEndpoint,
|
||||
wm.TokenEndpoint,
|
||||
wm.UserEndpoint,
|
||||
wm.Scopes,
|
||||
oauthOpts...,
|
||||
)
|
||||
}
|
||||
|
||||
type GitLabIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -815,6 +1024,33 @@ func (wm *GitLabIDPWriteModel) NewChanges(
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (wm *GitLabIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]oidc.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, oidc.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, oidc.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, oidc.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, oidc.WithAutoUpdate())
|
||||
}
|
||||
return gitlab.New(
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.Scopes,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type GitLabSelfHostedIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -910,6 +1146,35 @@ func (wm *GitLabSelfHostedIDPWriteModel) NewChanges(
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (wm *GitLabSelfHostedIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]oidc.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, oidc.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, oidc.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, oidc.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, oidc.WithAutoUpdate())
|
||||
}
|
||||
return gitlab.NewCustomIssuer(
|
||||
wm.Name,
|
||||
wm.Issuer,
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.Scopes,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type GoogleIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -997,6 +1262,38 @@ func (wm *GoogleIDPWriteModel) NewChanges(
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (wm *GoogleIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
||||
logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state)
|
||||
rp.DefaultErrorHandler(w, r, errorType, errorDesc, state)
|
||||
}
|
||||
oidc.WithRelyingPartyOption(rp.WithErrorHandler(errorHandler))
|
||||
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts := make([]oidc.ProviderOpts, 0, 4)
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, oidc.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, oidc.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, oidc.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, oidc.WithAutoUpdate())
|
||||
}
|
||||
return google.New(
|
||||
wm.ClientID,
|
||||
secret,
|
||||
callbackURL,
|
||||
wm.Scopes,
|
||||
opts...,
|
||||
)
|
||||
}
|
||||
|
||||
type LDAPIDPWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -1157,6 +1454,81 @@ func (wm *LDAPIDPWriteModel) NewChanges(
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (wm *LDAPIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
password, err := crypto.DecryptString(wm.BindPassword, idpAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var opts []ldap.ProviderOpts
|
||||
if !wm.StartTLS {
|
||||
opts = append(opts, ldap.WithoutStartTLS())
|
||||
}
|
||||
if wm.LDAPAttributes.IDAttribute != "" {
|
||||
opts = append(opts, ldap.WithCustomIDAttribute(wm.LDAPAttributes.IDAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.FirstNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithFirstNameAttribute(wm.LDAPAttributes.FirstNameAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.LastNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithLastNameAttribute(wm.LDAPAttributes.LastNameAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.DisplayNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithDisplayNameAttribute(wm.LDAPAttributes.DisplayNameAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.NickNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithNickNameAttribute(wm.LDAPAttributes.NickNameAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.PreferredUsernameAttribute != "" {
|
||||
opts = append(opts, ldap.WithPreferredUsernameAttribute(wm.LDAPAttributes.PreferredUsernameAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.EmailAttribute != "" {
|
||||
opts = append(opts, ldap.WithEmailAttribute(wm.LDAPAttributes.EmailAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.EmailVerifiedAttribute != "" {
|
||||
opts = append(opts, ldap.WithEmailVerifiedAttribute(wm.LDAPAttributes.EmailVerifiedAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.PhoneAttribute != "" {
|
||||
opts = append(opts, ldap.WithPhoneAttribute(wm.LDAPAttributes.PhoneAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.PhoneVerifiedAttribute != "" {
|
||||
opts = append(opts, ldap.WithPhoneVerifiedAttribute(wm.LDAPAttributes.PhoneVerifiedAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.PreferredLanguageAttribute != "" {
|
||||
opts = append(opts, ldap.WithPreferredLanguageAttribute(wm.LDAPAttributes.PreferredLanguageAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.AvatarURLAttribute != "" {
|
||||
opts = append(opts, ldap.WithAvatarURLAttribute(wm.LDAPAttributes.AvatarURLAttribute))
|
||||
}
|
||||
if wm.LDAPAttributes.ProfileAttribute != "" {
|
||||
opts = append(opts, ldap.WithProfileAttribute(wm.LDAPAttributes.ProfileAttribute))
|
||||
}
|
||||
if wm.IsCreationAllowed {
|
||||
opts = append(opts, ldap.WithCreationAllowed())
|
||||
}
|
||||
if wm.IsLinkingAllowed {
|
||||
opts = append(opts, ldap.WithLinkingAllowed())
|
||||
}
|
||||
if wm.IsAutoCreation {
|
||||
opts = append(opts, ldap.WithAutoCreation())
|
||||
}
|
||||
if wm.IsAutoUpdate {
|
||||
opts = append(opts, ldap.WithAutoUpdate())
|
||||
}
|
||||
return ldap.New(
|
||||
wm.Name,
|
||||
wm.Servers,
|
||||
wm.BaseDN,
|
||||
wm.BindDN,
|
||||
password,
|
||||
wm.UserBase,
|
||||
wm.UserObjectClasses,
|
||||
wm.UserFilters,
|
||||
wm.Timeout,
|
||||
callbackURL,
|
||||
opts...,
|
||||
), nil
|
||||
}
|
||||
|
||||
type IDPRemoveWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
@@ -1211,3 +1583,252 @@ func (wm *IDPRemoveWriteModel) reduceRemoved(id string) {
|
||||
}
|
||||
wm.State = domain.IDPStateRemoved
|
||||
}
|
||||
|
||||
type IDPTypeWriteModel struct {
|
||||
eventstore.WriteModel
|
||||
|
||||
ID string
|
||||
Type domain.IDPType
|
||||
State domain.IDPState
|
||||
}
|
||||
|
||||
func NewIDPTypeWriteModel(id string) *IDPTypeWriteModel {
|
||||
return &IDPTypeWriteModel{
|
||||
ID: id,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *IDPTypeWriteModel) Reduce() error {
|
||||
for _, event := range wm.Events {
|
||||
switch e := event.(type) {
|
||||
case *instance.OAuthIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeOAuth, e.Aggregate())
|
||||
case *org.OAuthIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeOAuth, e.Aggregate())
|
||||
case *instance.OIDCIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeOIDC, e.Aggregate())
|
||||
case *org.OIDCIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeOIDC, e.Aggregate())
|
||||
case *instance.JWTIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeJWT, e.Aggregate())
|
||||
case *org.JWTIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeJWT, e.Aggregate())
|
||||
case *instance.AzureADIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeAzureAD, e.Aggregate())
|
||||
case *org.AzureADIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeAzureAD, e.Aggregate())
|
||||
case *instance.GitHubIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitHub, e.Aggregate())
|
||||
case *org.GitHubIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitHub, e.Aggregate())
|
||||
case *instance.GitHubEnterpriseIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitHubEnterprise, e.Aggregate())
|
||||
case *org.GitHubEnterpriseIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitHubEnterprise, e.Aggregate())
|
||||
case *instance.GitLabIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitLab, e.Aggregate())
|
||||
case *org.GitLabIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitLab, e.Aggregate())
|
||||
case *instance.GitLabSelfHostedIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitLabSelfHosted, e.Aggregate())
|
||||
case *org.GitLabSelfHostedIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGitLabSelfHosted, e.Aggregate())
|
||||
case *instance.GoogleIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGoogle, e.Aggregate())
|
||||
case *org.GoogleIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeGoogle, e.Aggregate())
|
||||
case *instance.LDAPIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
|
||||
case *org.LDAPIDPAddedEvent:
|
||||
wm.reduceAdded(e.ID, domain.IDPTypeLDAP, e.Aggregate())
|
||||
case *instance.IDPRemovedEvent:
|
||||
wm.reduceRemoved(e.ID)
|
||||
case *org.IDPRemovedEvent:
|
||||
wm.reduceRemoved(e.ID)
|
||||
case *instance.IDPConfigAddedEvent:
|
||||
if e.Typ == domain.IDPConfigTypeOIDC {
|
||||
wm.reduceAdded(e.ConfigID, domain.IDPTypeOIDC, e.Aggregate())
|
||||
} else if e.Typ == domain.IDPConfigTypeJWT {
|
||||
wm.reduceAdded(e.ConfigID, domain.IDPTypeJWT, e.Aggregate())
|
||||
}
|
||||
case *org.IDPConfigAddedEvent:
|
||||
if e.Typ == domain.IDPConfigTypeOIDC {
|
||||
wm.reduceAdded(e.ConfigID, domain.IDPTypeOIDC, e.Aggregate())
|
||||
} else if e.Typ == domain.IDPConfigTypeJWT {
|
||||
wm.reduceAdded(e.ConfigID, domain.IDPTypeJWT, e.Aggregate())
|
||||
}
|
||||
case *instance.IDPConfigRemovedEvent:
|
||||
wm.reduceRemoved(e.ConfigID)
|
||||
case *org.IDPConfigRemovedEvent:
|
||||
wm.reduceRemoved(e.ConfigID)
|
||||
}
|
||||
}
|
||||
return wm.WriteModel.Reduce()
|
||||
}
|
||||
|
||||
func (wm *IDPTypeWriteModel) reduceAdded(id string, t domain.IDPType, agg eventstore.Aggregate) {
|
||||
if wm.ID != id {
|
||||
return
|
||||
}
|
||||
wm.Type = t
|
||||
wm.State = domain.IDPStateActive
|
||||
wm.ResourceOwner = agg.ResourceOwner
|
||||
wm.InstanceID = agg.InstanceID
|
||||
}
|
||||
|
||||
func (wm *IDPTypeWriteModel) reduceRemoved(id string) {
|
||||
if wm.ID != id {
|
||||
return
|
||||
}
|
||||
wm.Type = domain.IDPTypeUnspecified
|
||||
wm.State = domain.IDPStateRemoved
|
||||
wm.ResourceOwner = ""
|
||||
wm.InstanceID = ""
|
||||
}
|
||||
|
||||
func (wm *IDPTypeWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
AddQuery().
|
||||
AggregateTypes(instance.AggregateType).
|
||||
EventTypes(
|
||||
instance.OAuthIDPAddedEventType,
|
||||
instance.OIDCIDPAddedEventType,
|
||||
instance.JWTIDPAddedEventType,
|
||||
instance.AzureADIDPAddedEventType,
|
||||
instance.GitHubIDPAddedEventType,
|
||||
instance.GitHubEnterpriseIDPAddedEventType,
|
||||
instance.GitLabIDPAddedEventType,
|
||||
instance.GitLabSelfHostedIDPAddedEventType,
|
||||
instance.GoogleIDPAddedEventType,
|
||||
instance.LDAPIDPAddedEventType,
|
||||
instance.IDPRemovedEventType,
|
||||
).
|
||||
EventData(map[string]interface{}{"id": wm.ID}).
|
||||
Or().
|
||||
AggregateTypes(org.AggregateType).
|
||||
EventTypes(
|
||||
org.OAuthIDPAddedEventType,
|
||||
org.OIDCIDPAddedEventType,
|
||||
org.JWTIDPAddedEventType,
|
||||
org.AzureADIDPAddedEventType,
|
||||
org.GitHubIDPAddedEventType,
|
||||
org.GitHubEnterpriseIDPAddedEventType,
|
||||
org.GitLabIDPAddedEventType,
|
||||
org.GitLabSelfHostedIDPAddedEventType,
|
||||
org.GoogleIDPAddedEventType,
|
||||
org.LDAPIDPAddedEventType,
|
||||
org.IDPRemovedEventType,
|
||||
).
|
||||
EventData(map[string]interface{}{"id": wm.ID}).
|
||||
Or(). // old events
|
||||
AggregateTypes(instance.AggregateType).
|
||||
EventTypes(
|
||||
instance.IDPConfigAddedEventType,
|
||||
instance.IDPConfigRemovedEventType,
|
||||
).
|
||||
EventData(map[string]interface{}{"idpConfigId": wm.ID}).
|
||||
Or().
|
||||
AggregateTypes(org.AggregateType).
|
||||
EventTypes(
|
||||
org.IDPConfigAddedEventType,
|
||||
org.IDPConfigRemovedEventType,
|
||||
).
|
||||
EventData(map[string]interface{}{"idpConfigId": wm.ID}).
|
||||
Builder()
|
||||
}
|
||||
|
||||
type IDP interface {
|
||||
eventstore.QueryReducer
|
||||
ToProvider(string, crypto.EncryptionAlgorithm) (providers.Provider, error)
|
||||
}
|
||||
|
||||
type AllIDPWriteModel struct {
|
||||
model IDP
|
||||
|
||||
ID string
|
||||
IDPType domain.IDPType
|
||||
ResourceOwner string
|
||||
Instance bool
|
||||
}
|
||||
|
||||
func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idpType domain.IDPType) (*AllIDPWriteModel, error) {
|
||||
writeModel := &AllIDPWriteModel{
|
||||
ID: id,
|
||||
IDPType: idpType,
|
||||
ResourceOwner: resourceOwner,
|
||||
Instance: instanceBool,
|
||||
}
|
||||
|
||||
if instanceBool {
|
||||
switch idpType {
|
||||
case domain.IDPTypeOIDC:
|
||||
writeModel.model = NewOIDCInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeJWT:
|
||||
writeModel.model = NewJWTInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeOAuth:
|
||||
writeModel.model = NewOAuthInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeLDAP:
|
||||
writeModel.model = NewLDAPInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeAzureAD:
|
||||
writeModel.model = NewAzureADInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitHub:
|
||||
writeModel.model = NewGitHubInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitHubEnterprise:
|
||||
writeModel.model = NewGitHubEnterpriseInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitLab:
|
||||
writeModel.model = NewGitLabInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitLabSelfHosted:
|
||||
writeModel.model = NewGitLabSelfHostedInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGoogle:
|
||||
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
return nil, errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
} else {
|
||||
switch idpType {
|
||||
case domain.IDPTypeOIDC:
|
||||
writeModel.model = NewOIDCOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeJWT:
|
||||
writeModel.model = NewJWTOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeOAuth:
|
||||
writeModel.model = NewOAuthOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeLDAP:
|
||||
writeModel.model = NewLDAPOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeAzureAD:
|
||||
writeModel.model = NewAzureADOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitHub:
|
||||
writeModel.model = NewGitHubOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitHubEnterprise:
|
||||
writeModel.model = NewGitHubEnterpriseOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitLab:
|
||||
writeModel.model = NewGitLabOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGitLabSelfHosted:
|
||||
writeModel.model = NewGitLabSelfHostedOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeGoogle:
|
||||
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
|
||||
case domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
return nil, errors.ThrowInternal(nil, "COMMAND-xw921111", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
}
|
||||
return writeModel, nil
|
||||
}
|
||||
|
||||
func (wm *AllIDPWriteModel) Reduce() error {
|
||||
return wm.model.Reduce()
|
||||
}
|
||||
|
||||
func (wm *AllIDPWriteModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return wm.model.Query()
|
||||
}
|
||||
|
||||
func (wm *AllIDPWriteModel) AppendEvents(events ...eventstore.Event) {
|
||||
wm.model.AppendEvents(events...)
|
||||
}
|
||||
|
||||
func (wm *AllIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm) (providers.Provider, error) {
|
||||
return wm.model.ToProvider(callbackURL, idpAlg)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func TestCommands_AllIDPWriteModel(t *testing.T) {
|
||||
type args struct {
|
||||
resourceOwner string
|
||||
instanceBool bool
|
||||
id string
|
||||
idpType domain.IDPType
|
||||
}
|
||||
type res struct {
|
||||
writeModelType interface{}
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "writemodel instance oidc",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeOIDC,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceOIDCIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance jwt",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeJWT,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceJWTIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance oauth",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeOAuth,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceOAuthIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance ldap",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeLDAP,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceLDAPIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance azureAD",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeAzureAD,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceAzureADIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance github",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitHub,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceGitHubIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance github enterprise",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitHubEnterprise,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceGitHubEnterpriseIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance gitlab",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitLab,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceGitLabIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance gitlab self hosted",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitLabSelfHosted,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceGitLabSelfHostedIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance google",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGoogle,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &InstanceGoogleIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel instance unspecified",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: true,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeUnspecified,
|
||||
},
|
||||
res: res{
|
||||
err: errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org oidc",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeOIDC,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgOIDCIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org jwt",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeJWT,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgJWTIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org oauth",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeOAuth,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgOAuthIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org ldap",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeLDAP,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgLDAPIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org azureAD",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeAzureAD,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgAzureADIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org github",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitHub,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgGitHubIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org github enterprise",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitHubEnterprise,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgGitHubEnterpriseIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org gitlab",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitLab,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgGitLabIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org gitlab self hosted",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGitLabSelfHosted,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgGitLabSelfHostedIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org google",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeGoogle,
|
||||
},
|
||||
res: res{
|
||||
writeModelType: &OrgGoogleIDPWriteModel{},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "writemodel org unspecified",
|
||||
args: args{
|
||||
resourceOwner: "owner",
|
||||
instanceBool: false,
|
||||
id: "id",
|
||||
idpType: domain.IDPTypeUnspecified,
|
||||
},
|
||||
res: res{
|
||||
err: errors.ThrowInternal(nil, "COMMAND-xw921111", "Errors.IDPConfig.NotExisting"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
wm, err := NewAllIDPWriteModel(tt.args.resourceOwner, tt.args.instanceBool, tt.args.id, tt.args.idpType)
|
||||
require.ErrorIs(t, err, tt.res.err)
|
||||
if wm != nil {
|
||||
assert.IsType(t, tt.res.writeModelType, wm.model)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
action_repo "github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
key_repo "github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
@@ -41,6 +42,7 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
|
||||
key_repo.RegisterEventMappers(es)
|
||||
action_repo.RegisterEventMappers(es)
|
||||
session.RegisterEventMappers(es)
|
||||
idpintent.RegisterEventMappers(es)
|
||||
return es
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ type AddHuman struct {
|
||||
Register bool
|
||||
Metadata []*AddMetadataEntry
|
||||
|
||||
// Links are optional
|
||||
Links []*AddLink
|
||||
|
||||
// Details are set after a successful execution of the command
|
||||
Details *domain.ObjectDetails
|
||||
|
||||
@@ -65,6 +68,12 @@ type AddHuman struct {
|
||||
EmailCode *string
|
||||
}
|
||||
|
||||
type AddLink struct {
|
||||
IDPID string
|
||||
DisplayName string
|
||||
IDPExternalID string
|
||||
}
|
||||
|
||||
func (h *AddHuman) Validate() (err error) {
|
||||
if err := h.Email.Validate(); err != nil {
|
||||
return err
|
||||
@@ -226,6 +235,13 @@ func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg cr
|
||||
metadataEntry.Value,
|
||||
))
|
||||
}
|
||||
for _, link := range human.Links {
|
||||
cmd, err := addLink(ctx, filter, a, link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return cmds, nil
|
||||
}, nil
|
||||
@@ -260,6 +276,15 @@ func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.
|
||||
}
|
||||
return cmds, nil
|
||||
}
|
||||
|
||||
func addLink(ctx context.Context, filter preparation.FilterToQueryReducer, a *user.Aggregate, link *AddLink) (eventstore.Command, error) {
|
||||
exists, err := ExistsIDP(ctx, filter, link.IDPID, a.ResourceOwner)
|
||||
if !exists || err != nil {
|
||||
return nil, errors.ThrowPreconditionFailed(err, "COMMAND-39nf2", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
return user.NewUserIDPLinkAddedEvent(ctx, &a.Aggregate, link.IDPID, link.DisplayName, link.IDPExternalID), nil
|
||||
}
|
||||
|
||||
func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) {
|
||||
if human.Phone.Number == "" {
|
||||
return cmds, nil
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
idGenerator id.Generator
|
||||
userPasswordAlg crypto.HashAlgorithm
|
||||
codeAlg crypto.EncryptionAlgorithm
|
||||
newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error)
|
||||
newCode cryptoCodeFunc
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@@ -446,7 +446,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||
newCode: mockCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -526,7 +526,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"),
|
||||
userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)),
|
||||
codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
||||
newEmailCode: mockEmailCode("emailCode", time.Hour),
|
||||
newCode: mockCode("emailCode", time.Hour),
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
@@ -1202,7 +1202,7 @@ func TestCommandSide_AddHuman(t *testing.T) {
|
||||
userPasswordAlg: tt.fields.userPasswordAlg,
|
||||
userEncryption: tt.fields.codeAlg,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
newEmailCode: tt.fields.newEmailCode,
|
||||
newCode: tt.fields.newCode,
|
||||
}
|
||||
err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail)
|
||||
if tt.res.err == nil {
|
||||
@@ -3992,18 +3992,3 @@ func TestAddHumanCommand(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockEmailCode(code string, exp time.Duration) func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) {
|
||||
return &CryptoCodeWithExpiry{
|
||||
Crypted: &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte(code),
|
||||
},
|
||||
Plain: code,
|
||||
Expiry: exp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,7 +539,7 @@ func (c *Commands) humanAddPasswordlessInitCode(ctx context.Context, userID, res
|
||||
}
|
||||
if !direct {
|
||||
codeEventCreator = func(ctx context.Context, agg *eventstore.Aggregate, id string, cryptoCode *crypto.CryptoValue, exp time.Duration) eventstore.Command {
|
||||
return usr_repo.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, id, cryptoCode, exp)
|
||||
return usr_repo.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, id, cryptoCode, exp, "", false)
|
||||
}
|
||||
}
|
||||
codeEvent := codeEventCreator(ctx, UserAggregateFromWriteModel(&initCode.WriteModel), codeID, cryptoCode, passwordlessCodeGenerator.Expiry())
|
||||
|
||||
@@ -512,6 +512,9 @@ func (wm *HumanPasswordlessInitCodeWriteModel) appendRequestedEvent(e *user.Huma
|
||||
wm.CryptoCode = e.Code
|
||||
wm.Expiration = e.Expiry
|
||||
wm.State = domain.PasswordlessInitCodeStateRequested
|
||||
if e.CodeReturned {
|
||||
wm.State = domain.PasswordlessInitCodeStateActive
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *HumanPasswordlessInitCodeWriteModel) appendCheckFailedEvent(e *user.HumanPasswordlessInitCodeCheckFailedEvent) {
|
||||
|
||||
@@ -3,20 +3,25 @@ package command
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
)
|
||||
|
||||
func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner string, link *domain.UserIDPLink) (err error) {
|
||||
func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner string, link *domain.UserIDPLink) (_ *domain.ObjectDetails, err error) {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing")
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-03j8f", "Errors.IDMissing")
|
||||
}
|
||||
if err := c.checkUserExists(ctx, userID, resourceOwner); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
if userID != authz.GetCtxData(ctx).UserID {
|
||||
if err := c.checkPermission(ctx, domain.PermissionUserWrite, resourceOwner, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
linkWriteModel := NewUserIDPLinkWriteModel(userID, link.IDPConfigID, link.ExternalUserID, resourceOwner)
|
||||
@@ -24,11 +29,18 @@ func (c *Commands) AddUserIDPLink(ctx context.Context, userID, resourceOwner str
|
||||
|
||||
event, err := c.addUserIDPLink(ctx, userAgg, link)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = c.eventstore.Push(ctx, event)
|
||||
return err
|
||||
events, err := c.eventstore.Push(ctx, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.ObjectDetails{
|
||||
Sequence: events[len(events)-1].Sequence(),
|
||||
EventDate: events[len(events)-1].CreationDate(),
|
||||
ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) BulkAddedUserIDPLinks(ctx context.Context, userID, resourceOwner string, links []*domain.UserIDPLink) (err error) {
|
||||
|
||||
@@ -199,7 +199,7 @@ func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) {
|
||||
email: "[email protected]",
|
||||
urlTmpl: "{{",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"),
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "permission missing",
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
)
|
||||
|
||||
// RegisterUserPasskey creates a passkey registration for the current authenticated user.
|
||||
// UserID, ussualy taken from the request is compaired against the user ID in the context.
|
||||
func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.PasskeyRegistrationDetails, error) {
|
||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.registerUserPasskey(ctx, userID, resourceOwner, authenticator)
|
||||
}
|
||||
|
||||
// RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id.
|
||||
// The resource is protected by the code, identified by the codeID.
|
||||
func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.PasskeyRegistrationDetails, error) {
|
||||
event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.registerUserPasskey(ctx, userID, resourceOwner, authenticator, event)
|
||||
}
|
||||
|
||||
type eventCallback func(context.Context, *eventstore.Aggregate) eventstore.Command
|
||||
|
||||
// verifyUserPasskeyCode verifies a passkey code, identified by codeID and userID.
|
||||
// A code can only be used once.
|
||||
// Upon success an event callback is returned, which must be called after
|
||||
// all other events for the current request are created.
|
||||
// This prevent consuming a code when another error occurred after verification.
|
||||
func (c *Commands) verifyUserPasskeyCode(ctx context.Context, userID, resourceOwner, codeID, code string, alg crypto.EncryptionAlgorithm) (eventCallback, error) {
|
||||
wm := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
|
||||
err := c.eventstore.FilterToQueryReducer(ctx, wm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = verifyCryptoCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg, wm.ChangeDate, wm.Expiration, wm.CryptoCode, code)
|
||||
if err != nil || wm.State != domain.PasswordlessInitCodeStateActive {
|
||||
c.verifyUserPasskeyCodeFailed(ctx, wm)
|
||||
return nil, caos_errs.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid")
|
||||
}
|
||||
return func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
|
||||
return user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, codeID)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Commands) verifyUserPasskeyCodeFailed(ctx context.Context, wm *HumanPasswordlessInitCodeWriteModel) {
|
||||
userAgg := UserAggregateFromWriteModel(&wm.WriteModel)
|
||||
_, err := c.eventstore.Push(ctx, user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, wm.CodeID))
|
||||
logging.WithFields("userID", userAgg.ID).OnError(err).Error("RegisterUserPasskeyWithCode push failed")
|
||||
}
|
||||
|
||||
func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.PasskeyRegistrationDetails, error) {
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.pushUserPasskey(ctx, wm, userAgg, webAuthN, events...)
|
||||
}
|
||||
|
||||
func (c *Commands) createUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||
passwordlessTokens, err := c.getHumanPasswordlessTokens(ctx, userID, resourceOwner)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired)
|
||||
}
|
||||
|
||||
func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.PasskeyRegistrationDetails, error) {
|
||||
cmds := make([]eventstore.Command, len(events)+1)
|
||||
cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
||||
for i, event := range events {
|
||||
cmds[i+1] = event(ctx, userAgg)
|
||||
}
|
||||
|
||||
err := c.pushAppendAndReduce(ctx, wm, cmds...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.PasskeyRegistrationDetails{
|
||||
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
|
||||
PasskeyID: wm.WebauthNTokenID,
|
||||
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddUserPasskeyCode generates a Passkey code and sends an email
|
||||
// with the default generated URL (pointing to zitadel).
|
||||
func (c *Commands) AddUserPasskeyCode(ctx context.Context, userID, resourceOwner string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) {
|
||||
details, err := c.addUserPasskeyCode(ctx, userID, resourceOwner, alg, "", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return details.ObjectDetails, err
|
||||
}
|
||||
|
||||
// AddUserPasskeyCodeURLTemplate generates a Passkey code and sends an email
|
||||
// with the URL created from passed template string.
|
||||
// The template is executed as a test, before pushing to the eventstore.
|
||||
func (c *Commands) AddUserPasskeyCodeURLTemplate(ctx context.Context, userID, resourceOwner string, alg crypto.EncryptionAlgorithm, urlTmpl string) (*domain.ObjectDetails, error) {
|
||||
if err := domain.RenderPasskeyURLTemplate(io.Discard, urlTmpl, userID, resourceOwner, "codeID", "code"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details, err := c.addUserPasskeyCode(ctx, userID, resourceOwner, alg, urlTmpl, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return details.ObjectDetails, err
|
||||
}
|
||||
|
||||
// AddUserPasskeyCodeReturn generates and returns a Passkey code.
|
||||
// No email will be send to the user.
|
||||
func (c *Commands) AddUserPasskeyCodeReturn(ctx context.Context, userID, resourceOwner string, alg crypto.EncryptionAlgorithm) (*domain.PasskeyCodeDetails, error) {
|
||||
return c.addUserPasskeyCode(ctx, userID, resourceOwner, alg, "", true)
|
||||
}
|
||||
|
||||
func (c *Commands) addUserPasskeyCode(ctx context.Context, userID, resourceOwner string, alg crypto.EncryptionAlgorithm, urlTmpl string, returnCode bool) (*domain.PasskeyCodeDetails, error) {
|
||||
codeID, err := c.idGenerator.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code, err := c.newCode(ctx, c.eventstore.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wm := NewHumanPasswordlessInitCodeWriteModel(userID, codeID, resourceOwner)
|
||||
err = c.eventstore.FilterToQueryReducer(ctx, wm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agg := UserAggregateFromWriteModel(&wm.WriteModel)
|
||||
|
||||
cmd := user.NewHumanPasswordlessInitCodeRequestedEvent(ctx, agg, codeID, code.Crypted, code.Expiry, urlTmpl, returnCode)
|
||||
err = c.pushAppendAndReduce(ctx, wm, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain.PasskeyCodeDetails{
|
||||
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
|
||||
CodeID: codeID,
|
||||
Code: code.Plain,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,915 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
id_mock "github.com/zitadel/zitadel/internal/id/mock"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
webauthn_helper "github.com/zitadel/zitadel/internal/webauthn"
|
||||
)
|
||||
|
||||
func TestCommands_RegisterUserPasskey(t *testing.T) {
|
||||
ctx := authz.NewMockContextWithPermissions("instance1", "org1", "user1", nil)
|
||||
ctx = authz.WithRequestedDomain(ctx, "example.com")
|
||||
|
||||
webauthnConfig := &webauthn_helper.Config{
|
||||
DisplayName: "test",
|
||||
ExternalSecure: true,
|
||||
}
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
authenticator domain.AuthenticatorAttachment
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.PasskeyRegistrationDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "wrong user",
|
||||
args: args{
|
||||
userID: "foo",
|
||||
resourceOwner: "org1",
|
||||
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
},
|
||||
wantErr: caos_errs.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong"),
|
||||
},
|
||||
{
|
||||
name: "get human passwordless error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "id generator error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(), // getHumanPasswordlessTokens
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(ctx,
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(ctx,
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(ctx,
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
false, false, false,
|
||||
),
|
||||
)),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webauthnConfig: webauthnConfig,
|
||||
}
|
||||
_, err := c.RegisterUserPasskey(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authenticator)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful case can't be tested due to random challenge.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_RegisterUserPasskeyWithCode(t *testing.T) {
|
||||
ctx := authz.WithRequestedDomain(context.Background(), "example.com")
|
||||
webauthnConfig := &webauthn_helper.Config{
|
||||
DisplayName: "test",
|
||||
ExternalSecure: true,
|
||||
}
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
es := eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
)
|
||||
code, err := newCryptoCodeWithExpiry(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
authenticator domain.AuthenticatorAttachment
|
||||
codeID string
|
||||
code string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "code verification error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg, "123", code.Crypted, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
codeID: "123",
|
||||
code: "wrong",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "code verification ok, get human passwordless error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg, "123", code.Crypted, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
authenticator: domain.AuthenticatorAttachmentCrossPlattform,
|
||||
codeID: "123",
|
||||
code: code.Plain,
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
webauthnConfig: webauthnConfig,
|
||||
}
|
||||
_, err := c.RegisterUserPasskeyWithCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.authenticator, tt.args.codeID, tt.args.code, alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
// successful case can't be tested due to random challenge.
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_verifyUserPasskeyCode(t *testing.T) {
|
||||
ctx := authz.WithRequestedDomain(context.Background(), "example.com")
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
es := eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
)
|
||||
code, err := newCryptoCodeWithExpiry(ctx, es.Filter, domain.SecretGeneratorTypePasswordlessInitCode, alg)
|
||||
require.NoError(t, err)
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
type fields struct {
|
||||
eventstore *eventstore.Eventstore
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
codeID string
|
||||
code string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *user.HumanPasswordlessInitCodeCheckSucceededEvent
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
codeID: "123",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "code verification error",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg, "123", code.Crypted, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeCheckFailedEvent(ctx, userAgg, "123"),
|
||||
)}),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
codeID: "123",
|
||||
code: "wrong",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(err, "COMMAND-Eeb2a", "Errors.User.Code.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg, "123", code.Crypted, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
eventFromEventPusherWithCreationDateNow(
|
||||
user.NewHumanPasswordlessInitCodeSentEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
),
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
codeID: "123",
|
||||
code: code.Plain,
|
||||
},
|
||||
want: user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, "123"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: tt.fields.eventstore,
|
||||
}
|
||||
got, err := c.verifyUserPasskeyCode(ctx, tt.args.userID, tt.args.resourceOwner, tt.args.codeID, tt.args.code, alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.wantErr == nil {
|
||||
assert.Equal(t, tt.want, got(ctx, userAgg))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_pushUserPasskey(t *testing.T) {
|
||||
ctx := authz.WithRequestedDomain(context.Background(), "example.com")
|
||||
webauthnConfig := &webauthn_helper.Config{
|
||||
DisplayName: "test",
|
||||
ExternalSecure: true,
|
||||
}
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
prep := []expect{
|
||||
expectFilter(), // getHumanPasswordlessTokens
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(ctx,
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusher(
|
||||
org.NewOrgAddedEvent(ctx,
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
"org1",
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusher(
|
||||
org.NewDomainPolicyAddedEvent(ctx,
|
||||
&org.NewAggregate("org1").Aggregate,
|
||||
false, false, false,
|
||||
),
|
||||
)),
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanWebAuthNAddedEvent(eventstore.NewBaseEventForPush(
|
||||
ctx, &org.NewAggregate("org1").Aggregate, user.HumanPasswordlessTokenAddedType,
|
||||
), "111", "challenge"),
|
||||
)),
|
||||
}
|
||||
|
||||
type args struct {
|
||||
events []eventCallback
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
expectPush func(challenge string) expect
|
||||
args args
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "push error",
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanPasswordlessAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
),
|
||||
)})
|
||||
},
|
||||
args: args{},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPush([]*repository.Event{eventFromEventPusher(
|
||||
user.NewHumanPasswordlessAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
),
|
||||
)})
|
||||
},
|
||||
args: args{},
|
||||
},
|
||||
{
|
||||
name: "initcode succeeded event",
|
||||
expectPush: func(challenge string) expect {
|
||||
return expectPush([]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessAddedEvent(ctx,
|
||||
userAgg, "123", challenge,
|
||||
),
|
||||
),
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, "123"),
|
||||
),
|
||||
})
|
||||
},
|
||||
args: args{
|
||||
events: []eventCallback{func(ctx context.Context, userAgg *eventstore.Aggregate) eventstore.Command {
|
||||
return user.NewHumanPasswordlessInitCodeCheckSucceededEvent(ctx, userAgg, "123")
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
eventstore: eventstoreExpect(t, prep...),
|
||||
webauthnConfig: webauthnConfig,
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
}
|
||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, "user1", "org1", domain.AuthenticatorAttachmentCrossPlattform)
|
||||
require.NoError(t, err)
|
||||
|
||||
c.eventstore = eventstoreExpect(t, tt.expectPush(webAuthN.Challenge))
|
||||
|
||||
got, err := c.pushUserPasskey(ctx, wm, userAgg, webAuthN, tt.args.events...)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.wantErr == nil {
|
||||
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
|
||||
assert.Equal(t, "123", got.PasskeyID)
|
||||
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_AddUserPasskeyCode(t *testing.T) {
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
type fields struct {
|
||||
newCode cryptoCodeFunc
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "id generator error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
newCode: mockCode("passkey1", time.Minute),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg,
|
||||
"123", &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("passkey1"),
|
||||
}, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
newCode: tt.fields.newCode,
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.AddUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_AddUserPasskeyCodeURLTemplate(t *testing.T) {
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
|
||||
type fields struct {
|
||||
newCode cryptoCodeFunc
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
urlTmpl string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.ObjectDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "template error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
urlTmpl: "{{",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "id generator error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
newCode: mockCode("passkey1", time.Minute),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg,
|
||||
"123", &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("passkey1"),
|
||||
},
|
||||
time.Minute,
|
||||
"https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
||||
false,
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
||||
},
|
||||
want: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
newCode: tt.fields.newCode,
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.AddUserPasskeyCodeURLTemplate(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, tt.args.urlTmpl)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_AddUserPasskeyCodeReturn(t *testing.T) {
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
type fields struct {
|
||||
newCode cryptoCodeFunc
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.PasskeyCodeDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "id generator error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
newCode: mockCode("passkey1", time.Minute),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg,
|
||||
"123", &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("passkey1"),
|
||||
}, time.Minute, "", true,
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
want: &domain.PasskeyCodeDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
CodeID: "123",
|
||||
Code: "passkey1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
newCode: tt.fields.newCode,
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.AddUserPasskeyCodeReturn(context.Background(), tt.args.userID, tt.args.resourceOwner, alg)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommands_addUserPasskeyCode(t *testing.T) {
|
||||
alg := crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
userAgg := &user.NewAggregate("user1", "org1").Aggregate
|
||||
type fields struct {
|
||||
newCode cryptoCodeFunc
|
||||
eventstore *eventstore.Eventstore
|
||||
idGenerator id.Generator
|
||||
}
|
||||
type args struct {
|
||||
userID string
|
||||
resourceOwner string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *domain.PasskeyCodeDetails
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "id generator error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectError(t, io.ErrClosedPipe),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "crypto error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t, expectFilterError(io.ErrClosedPipe)),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "filter query error",
|
||||
fields: fields{
|
||||
newCode: newCryptoCodeWithExpiry,
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(testSecretGeneratorAddedEvent(domain.SecretGeneratorTypePasswordlessInitCode))),
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "push error",
|
||||
fields: fields{
|
||||
newCode: mockCode("passkey1", time.Minute),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectPushFailed(io.ErrClosedPipe, []*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
&user.NewAggregate("user1", "org1").Aggregate,
|
||||
"123", &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("passkey1"),
|
||||
}, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
fields: fields{
|
||||
newCode: mockCode("passkey1", time.Minute),
|
||||
eventstore: eventstoreExpect(t,
|
||||
expectFilter(eventFromEventPusher(
|
||||
user.NewHumanAddedEvent(context.Background(),
|
||||
userAgg,
|
||||
"username",
|
||||
"firstname",
|
||||
"lastname",
|
||||
"nickname",
|
||||
"displayname",
|
||||
language.German,
|
||||
domain.GenderUnspecified,
|
||||
"[email protected]",
|
||||
true,
|
||||
),
|
||||
)),
|
||||
expectPush([]*repository.Event{
|
||||
eventFromEventPusher(
|
||||
user.NewHumanPasswordlessInitCodeRequestedEvent(context.Background(),
|
||||
userAgg,
|
||||
"123", &crypto.CryptoValue{
|
||||
CryptoType: crypto.TypeEncryption,
|
||||
Algorithm: "enc",
|
||||
KeyID: "id",
|
||||
Crypted: []byte("passkey1"),
|
||||
}, time.Minute, "", false,
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "123"),
|
||||
},
|
||||
args: args{
|
||||
userID: "user1",
|
||||
resourceOwner: "org1",
|
||||
},
|
||||
want: &domain.PasskeyCodeDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
CodeID: "123",
|
||||
Code: "passkey1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Commands{
|
||||
newCode: tt.fields.newCode,
|
||||
eventstore: tt.fields.eventstore,
|
||||
idGenerator: tt.fields.idGenerator,
|
||||
}
|
||||
got, err := c.addUserPasskeyCode(context.Background(), tt.args.userID, tt.args.resourceOwner, alg, "", false)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -113,14 +113,6 @@ type SSL struct {
|
||||
func (s *Config) checkSSL(user User) {
|
||||
if user.SSL.Mode == sslDisabledMode || user.SSL.Mode == "" {
|
||||
user.SSL = SSL{Mode: sslDisabledMode}
|
||||
return
|
||||
}
|
||||
if user.SSL.RootCert == "" {
|
||||
logging.WithFields(
|
||||
"cert set", user.SSL.Cert != "",
|
||||
"key set", user.SSL.Key != "",
|
||||
"rootCert set", user.SSL.RootCert != "",
|
||||
).Fatal("at least ssl root cert has to be set")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +141,9 @@ func (c Config) String(useAdmin bool) string {
|
||||
fields = append(fields, "dbname=postgres")
|
||||
}
|
||||
if user.SSL.Mode != sslDisabledMode {
|
||||
fields = append(fields, "sslrootcert="+user.SSL.RootCert)
|
||||
if user.SSL.RootCert != "" {
|
||||
fields = append(fields, "sslrootcert="+user.SSL.RootCert)
|
||||
}
|
||||
if user.SSL.Cert != "" {
|
||||
fields = append(fields, "sslcert="+user.SSL.Cert)
|
||||
}
|
||||
|
||||
@@ -4,12 +4,10 @@ import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
@@ -73,19 +71,8 @@ type ConfirmURLData struct {
|
||||
OrgID string
|
||||
}
|
||||
|
||||
// RenderConfirmURLTemplate parses and renders tmplStr.
|
||||
// RenderConfirmURLTemplate parses and renders tmpl.
|
||||
// userID, code and orgID are passed into the [ConfirmURLData].
|
||||
// "%s%s?userID=%s&code=%s&orgID=%s"
|
||||
func RenderConfirmURLTemplate(w io.Writer, tmplStr, userID, code, orgID string) error {
|
||||
tmpl, err := template.New("").Parse(tmplStr)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInvalidArgument(err, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate")
|
||||
}
|
||||
|
||||
data := &ConfirmURLData{userID, code, orgID}
|
||||
if err = tmpl.Execute(w, data); err != nil {
|
||||
return caos_errs.ThrowInvalidArgument(err, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate")
|
||||
}
|
||||
|
||||
return nil
|
||||
func RenderConfirmURLTemplate(w io.Writer, tmpl, userID, code, orgID string) error {
|
||||
return renderURLTemplate(w, tmpl, &ConfirmURLData{userID, code, orgID})
|
||||
}
|
||||
|
||||
@@ -81,10 +81,10 @@ func TestEmailValid(t *testing.T) {
|
||||
|
||||
func TestRenderConfirmURLTemplate(t *testing.T) {
|
||||
type args struct {
|
||||
tmplStr string
|
||||
userID string
|
||||
code string
|
||||
orgID string
|
||||
tmpl string
|
||||
userID string
|
||||
code string
|
||||
orgID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -95,30 +95,30 @@ func TestRenderConfirmURLTemplate(t *testing.T) {
|
||||
{
|
||||
name: "invalid template",
|
||||
args: args{
|
||||
tmplStr: "{{",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
tmpl: "{{",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"),
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "execution error",
|
||||
args: args{
|
||||
tmplStr: "{{.Foo}}",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
tmpl: "{{.Foo}}",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate"),
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-ieYa7", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
tmplStr: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
tmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
},
|
||||
want: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
|
||||
},
|
||||
@@ -126,7 +126,7 @@ func TestRenderConfirmURLTemplate(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var w strings.Builder
|
||||
err := RenderConfirmURLTemplate(&w, tt.args.tmplStr, tt.args.userID, tt.args.code, tt.args.orgID)
|
||||
err := RenderConfirmURLTemplate(&w, tt.args.tmpl, tt.args.userID, tt.args.code, tt.args.orgID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, w.String())
|
||||
})
|
||||
|
||||
@@ -91,3 +91,22 @@ func (t IDPType) DisplayName() string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type IDPIntentState int32
|
||||
|
||||
const (
|
||||
IDPIntentStateUnspecified IDPIntentState = iota
|
||||
IDPIntentStateStarted
|
||||
IDPIntentStateSucceeded
|
||||
IDPIntentStateFailed
|
||||
|
||||
idpIntentStateCount
|
||||
)
|
||||
|
||||
func (s IDPIntentState) Valid() bool {
|
||||
return s >= 0 && s < idpIntentStateCount
|
||||
}
|
||||
|
||||
func (s IDPIntentState) Exists() bool {
|
||||
return s != IDPIntentStateUnspecified && s != IDPIntentStateFailed //TODO: ?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"text/template"
|
||||
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func renderURLTemplate(w io.Writer, tmpl string, data any) error {
|
||||
parsed, err := template.New("").Parse(tmpl)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInvalidArgument(err, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate")
|
||||
}
|
||||
if err = parsed.Execute(w, data); err != nil {
|
||||
return caos_errs.ThrowInvalidArgument(err, "DOMAIN-ieYa7", "Errors.User.InvalidURLTemplate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func Test_renderURLTemplate(t *testing.T) {
|
||||
type args struct {
|
||||
tmpl string
|
||||
data any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantW string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "parse error",
|
||||
args: args{
|
||||
tmpl: "{{",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "execution error",
|
||||
args: args{
|
||||
tmpl: "{{.Some}}",
|
||||
data: struct{ Foo int }{Foo: 1},
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-ieYa7", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
tmpl: "{{.Foo}}",
|
||||
data: struct{ Foo int }{Foo: 1},
|
||||
},
|
||||
wantW: "1",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := &bytes.Buffer{}
|
||||
err := renderURLTemplate(w, tt.args.tmpl, tt.args.data)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantW, w.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package domain
|
||||
|
||||
import "io"
|
||||
|
||||
type PasskeyURLData struct {
|
||||
UserID string
|
||||
OrgID string
|
||||
CodeID string
|
||||
Code string
|
||||
}
|
||||
|
||||
// RenderPasskeyURLTemplate parses and renders tmpl.
|
||||
// userID, orgID, codeID and code are passed into the [PasskeyURLData].
|
||||
func RenderPasskeyURLTemplate(w io.Writer, tmpl, userID, orgID, codeID, code string) error {
|
||||
return renderURLTemplate(w, tmpl, &PasskeyURLData{userID, orgID, codeID, code})
|
||||
}
|
||||
|
||||
type PasskeyCodeDetails struct {
|
||||
*ObjectDetails
|
||||
CodeID string
|
||||
Code string
|
||||
}
|
||||
|
||||
type PasskeyRegistrationDetails struct {
|
||||
*ObjectDetails
|
||||
|
||||
PasskeyID string
|
||||
PublicKeyCredentialCreationOptions []byte
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func TestRenderPasskeyURLTemplate(t *testing.T) {
|
||||
type args struct {
|
||||
tmpl string
|
||||
userID string
|
||||
orgID string
|
||||
codeID string
|
||||
code string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantW string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "parse error",
|
||||
args: args{
|
||||
tmpl: "{{",
|
||||
},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
tmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
||||
userID: "user1",
|
||||
orgID: "org1",
|
||||
codeID: "99",
|
||||
code: "123",
|
||||
},
|
||||
wantW: "https://example.com/passkey/register?userID=user1&orgID=org1&codeID=99&code=123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := &bytes.Buffer{}
|
||||
err := RenderPasskeyURLTemplate(w, tt.args.tmpl, tt.args.userID, tt.args.orgID, tt.args.codeID, tt.args.code)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantW, w.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
)
|
||||
|
||||
@@ -394,6 +394,9 @@ func (u *userNotifier) reducePasswordlessCodeRequested(event eventstore.Event) (
|
||||
if !ok {
|
||||
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-EDtjd", "reduce.wrong.event.type %s", user.HumanPasswordlessInitCodeAddedType)
|
||||
}
|
||||
if e.CodeReturned {
|
||||
return crdb.NewNoOpStatement(e), nil
|
||||
}
|
||||
ctx := HandlerContext(event.Aggregate())
|
||||
alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, map[string]interface{}{"id": e.ID}, user.HumanPasswordlessInitCodeSentType)
|
||||
if err != nil {
|
||||
@@ -442,7 +445,7 @@ func (u *userNotifier) reducePasswordlessCodeRequested(event eventstore.Event) (
|
||||
e,
|
||||
u.metricSuccessfulDeliveriesEmail,
|
||||
u.metricFailedDeliveriesEmail,
|
||||
).SendPasswordlessRegistrationLink(notifyUser, origin, code, e.ID)
|
||||
).SendPasswordlessRegistrationLink(notifyUser, origin, code, e.ID, e.URLTemplate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,27 +12,6 @@ import (
|
||||
)
|
||||
|
||||
func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
type res struct {
|
||||
url string
|
||||
args map[string]interface{}
|
||||
messageType string
|
||||
allowUnverifiedNotificationChannel bool
|
||||
}
|
||||
notify := func(dst *res) Notify {
|
||||
return func(
|
||||
url string,
|
||||
args map[string]interface{},
|
||||
messageType string,
|
||||
allowUnverifiedNotificationChannel bool,
|
||||
) error {
|
||||
dst.url = url
|
||||
dst.args = args
|
||||
dst.messageType = messageType
|
||||
dst.allowUnverifiedNotificationChannel = allowUnverifiedNotificationChannel
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type args struct {
|
||||
user *query.NotifyUser
|
||||
origin string
|
||||
@@ -42,7 +21,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *res
|
||||
want *notifyResult
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
@@ -56,7 +35,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
code: "123",
|
||||
urlTmpl: "",
|
||||
},
|
||||
want: &res{
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/ui/login/mail/verification?userID=user1&code=123&orgID=org1",
|
||||
args: map[string]interface{}{"Code": "123"},
|
||||
messageType: domain.VerifyEmailMessageType,
|
||||
@@ -74,8 +53,8 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
code: "123",
|
||||
urlTmpl: "{{",
|
||||
},
|
||||
want: &res{},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"),
|
||||
want: ¬ifyResult{},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "template success",
|
||||
@@ -88,7 +67,7 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
code: "123",
|
||||
urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
},
|
||||
want: &res{
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
|
||||
args: map[string]interface{}{"Code": "123"},
|
||||
messageType: domain.VerifyEmailMessageType,
|
||||
@@ -98,8 +77,8 @@ func TestNotify_SendEmailVerificationCode(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := new(res)
|
||||
err := notify(got).SendEmailVerificationCode(tt.args.user, tt.args.origin, tt.args.code, tt.args.urlTmpl)
|
||||
got, notify := mockNotify()
|
||||
err := notify.SendEmailVerificationCode(tt.args.user, tt.args.origin, tt.args.code, tt.args.urlTmpl)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/ui/login"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func (notify Notify) SendPasswordlessRegistrationLink(user *query.NotifyUser, origin, code, codeID string) error {
|
||||
url := domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code)
|
||||
func (notify Notify) SendPasswordlessRegistrationLink(user *query.NotifyUser, origin, code, codeID, urlTmpl string) error {
|
||||
var url string
|
||||
if urlTmpl == "" {
|
||||
url = domain.PasswordlessInitCodeLink(origin+login.HandlerPrefix+login.EndpointPasswordlessRegistration, user.ID, user.ResourceOwner, codeID, code)
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
if err := domain.RenderPasskeyURLTemplate(&buf, urlTmpl, user.ID, user.ResourceOwner, codeID, code); err != nil {
|
||||
return err
|
||||
}
|
||||
url = buf.String()
|
||||
}
|
||||
|
||||
return notify(url, nil, domain.PasswordlessRegistrationMessageType, true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
func TestNotify_SendPasswordlessRegistrationLink(t *testing.T) {
|
||||
type args struct {
|
||||
user *query.NotifyUser
|
||||
origin string
|
||||
code string
|
||||
codeID string
|
||||
urlTmpl string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *notifyResult
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "default URL",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: "https://example.com",
|
||||
code: "123",
|
||||
codeID: "456",
|
||||
urlTmpl: "",
|
||||
},
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/ui/login/login/passwordless/init?userID=user1&orgID=org1&codeID=456&code=123",
|
||||
messageType: domain.PasswordlessRegistrationMessageType,
|
||||
allowUnverifiedNotificationChannel: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "template error",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: "https://example.com",
|
||||
code: "123",
|
||||
codeID: "456",
|
||||
urlTmpl: "{{",
|
||||
},
|
||||
want: ¬ifyResult{},
|
||||
wantErr: caos_errs.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "template success",
|
||||
args: args{
|
||||
user: &query.NotifyUser{
|
||||
ID: "user1",
|
||||
ResourceOwner: "org1",
|
||||
},
|
||||
origin: "https://example.com",
|
||||
code: "123",
|
||||
codeID: "456",
|
||||
urlTmpl: "https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}",
|
||||
},
|
||||
want: ¬ifyResult{
|
||||
url: "https://example.com/passkey/register?userID=user1&orgID=org1&codeID=456&code=123",
|
||||
messageType: domain.PasswordlessRegistrationMessageType,
|
||||
allowUnverifiedNotificationChannel: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, notify := mockNotify()
|
||||
err := notify.SendPasswordlessRegistrationLink(tt.args.user, tt.args.origin, tt.args.code, tt.args.codeID, tt.args.urlTmpl)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package types
|
||||
|
||||
type notifyResult struct {
|
||||
url string
|
||||
args map[string]interface{}
|
||||
messageType string
|
||||
allowUnverifiedNotificationChannel bool
|
||||
}
|
||||
|
||||
// mockNotify returns a notifyResult and Notify function for easy mocking.
|
||||
// The notifyResult will only be populated after Notify is called.
|
||||
func mockNotify() (*notifyResult, Notify) {
|
||||
dst := new(notifyResult)
|
||||
return dst, func(url string, args map[string]interface{}, messageType string, allowUnverifiedNotificationChannel bool) error {
|
||||
*dst = notifyResult{
|
||||
url: url,
|
||||
args: args,
|
||||
messageType: messageType,
|
||||
allowUnverifiedNotificationChannel: allowUnverifiedNotificationChannel,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
@@ -84,6 +85,7 @@ func StartQueries(
|
||||
keypair.RegisterEventMappers(repo.eventstore)
|
||||
usergrant.RegisterEventMappers(repo.eventstore)
|
||||
session.RegisterEventMappers(repo.eventstore)
|
||||
idpintent.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.idpConfigEncryption = idpConfigEncryption
|
||||
repo.multifactors = domain.MultifactorConfigs{
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package idpintent
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
const (
|
||||
instanceEventTypePrefix = eventstore.EventType("idpintent.")
|
||||
)
|
||||
|
||||
const (
|
||||
AggregateType = "idpintent"
|
||||
AggregateVersion = "v1"
|
||||
)
|
||||
|
||||
type Aggregate struct {
|
||||
eventstore.Aggregate
|
||||
}
|
||||
|
||||
func NewAggregate(id, resourceOwner string) *Aggregate {
|
||||
return &Aggregate{
|
||||
Aggregate: eventstore.Aggregate{
|
||||
Type: AggregateType,
|
||||
Version: AggregateVersion,
|
||||
ID: id,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package idpintent
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
func RegisterEventMappers(es *eventstore.Eventstore) {
|
||||
es.RegisterFilterEventMapper(AggregateType, StartedEventType, StartedEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, SucceededEventType, SucceededEventMapper).
|
||||
RegisterFilterEventMapper(AggregateType, FailedEventType, FailedEventMapper)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package idpintent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
StartedEventType = instanceEventTypePrefix + "started"
|
||||
SucceededEventType = instanceEventTypePrefix + "succeeded"
|
||||
FailedEventType = instanceEventTypePrefix + "failed"
|
||||
)
|
||||
|
||||
type StartedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
SuccessURL *url.URL `json:"successURL"`
|
||||
FailureURL *url.URL `json:"failureURL"`
|
||||
IDPID string `json:"idpId"`
|
||||
}
|
||||
|
||||
func NewStartedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
successURL,
|
||||
failureURL *url.URL,
|
||||
idpID string,
|
||||
) *StartedEvent {
|
||||
return &StartedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
StartedEventType,
|
||||
),
|
||||
SuccessURL: successURL,
|
||||
FailureURL: failureURL,
|
||||
IDPID: idpID,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *StartedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *StartedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
e := &StartedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(event.Data, e)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "IDP-Sf3f1", "unable to unmarshal event")
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
type SucceededEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
IDPUser []byte `json:"idpUser"`
|
||||
UserID string `json:"userId,omitempty"`
|
||||
IDPAccessToken *crypto.CryptoValue `json:"idpAccessToken,omitempty"`
|
||||
IDPIDToken string `json:"idpIdToken,omitempty"`
|
||||
}
|
||||
|
||||
func NewSucceededEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
idpUser []byte,
|
||||
userID string,
|
||||
idpAccessToken *crypto.CryptoValue,
|
||||
idpIDToken string,
|
||||
) (*SucceededEvent, error) {
|
||||
return &SucceededEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
SucceededEventType,
|
||||
),
|
||||
IDPUser: idpUser,
|
||||
UserID: userID,
|
||||
IDPAccessToken: idpAccessToken,
|
||||
IDPIDToken: idpIDToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *SucceededEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *SucceededEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SucceededEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
e := &SucceededEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(event.Data, e)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "IDP-HBreq", "unable to unmarshal event")
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
type FailedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
func NewFailedEvent(
|
||||
ctx context.Context,
|
||||
aggregate *eventstore.Aggregate,
|
||||
reason string,
|
||||
) *FailedEvent {
|
||||
return &FailedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
ctx,
|
||||
aggregate,
|
||||
FailedEventType,
|
||||
),
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *FailedEvent) Data() interface{} {
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *FailedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FailedEventMapper(event *repository.Event) (eventstore.Event, error) {
|
||||
e := &FailedEvent{
|
||||
BaseEvent: *eventstore.BaseEventFromRepo(event),
|
||||
}
|
||||
|
||||
err := json.Unmarshal(event.Data, e)
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "IDP-Sfer3", "unable to unmarshal event")
|
||||
}
|
||||
|
||||
return e, nil
|
||||
}
|
||||
@@ -315,9 +315,11 @@ func HumanPasswordlessInitCodeAddedEventMapper(event *repository.Event) (eventst
|
||||
type HumanPasswordlessInitCodeRequestedEvent struct {
|
||||
eventstore.BaseEvent `json:"-"`
|
||||
|
||||
ID string `json:"id"`
|
||||
Code *crypto.CryptoValue `json:"code"`
|
||||
Expiry time.Duration `json:"expiry"`
|
||||
ID string `json:"id"`
|
||||
Code *crypto.CryptoValue `json:"code"`
|
||||
Expiry time.Duration `json:"expiry"`
|
||||
URLTemplate string `json:"url_template,omitempty"`
|
||||
CodeReturned bool `json:"code_returned,omitempty"`
|
||||
}
|
||||
|
||||
func (e *HumanPasswordlessInitCodeRequestedEvent) Data() interface{} {
|
||||
@@ -334,6 +336,8 @@ func NewHumanPasswordlessInitCodeRequestedEvent(
|
||||
id string,
|
||||
code *crypto.CryptoValue,
|
||||
expiry time.Duration,
|
||||
urlTmpl string,
|
||||
codeReturned bool,
|
||||
) *HumanPasswordlessInitCodeRequestedEvent {
|
||||
return &HumanPasswordlessInitCodeRequestedEvent{
|
||||
BaseEvent: *eventstore.NewBaseEventForPush(
|
||||
@@ -341,9 +345,11 @@ func NewHumanPasswordlessInitCodeRequestedEvent(
|
||||
aggregate,
|
||||
HumanPasswordlessInitCodeRequestedType,
|
||||
),
|
||||
ID: id,
|
||||
Code: code,
|
||||
Expiry: expiry,
|
||||
ID: id,
|
||||
Code: code,
|
||||
Expiry: expiry,
|
||||
URLTemplate: urlTmpl,
|
||||
CodeReturned: codeReturned,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: Benutzer konnte in der gewünschten Organisation nicht gefunden werden
|
||||
NotAllowedOrg: Benutzer gehört nicht der benötigten Organisation an
|
||||
UserIDMissing: User ID fehlt
|
||||
UserIDWrong: "Der Anforderungsbenutzer ist nicht gleich dem authentifizierten Benutzer"
|
||||
DomainPolicyNil: Organisation Policy ist leer
|
||||
EmailAsUsernameNotAllowed: Benutzername darf keine E-Mail Adresse sein
|
||||
Invalid: Benutzerdaten sind ungültig
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: Keine Änderungen gefunden
|
||||
InitCodeNotFound: Kein Initialisierungs-Code gefunden
|
||||
UsernameNotChanged: Benutzername wurde nicht verändert
|
||||
InvalidURLTemplate: URL Template ist ungültig
|
||||
Profile:
|
||||
NotFound: Profil nicht gefunden
|
||||
NotChanged: Profil nicht verändert
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: Email wurde nicht geändert
|
||||
Empty: Email ist leer
|
||||
IDMissing: Email ID fehlt
|
||||
InvalidURLTemplate: URL Template ist ungültig
|
||||
Phone:
|
||||
NotFound: Telefonnummer nicht gefunden
|
||||
Invalid: Telefonnummer ist ungültig
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: Session bereits beendet
|
||||
Token:
|
||||
Invalid: Session Token ist ungültig
|
||||
Intent:
|
||||
IDPMissing: IDP ID fehlt im Request
|
||||
SuccessURLMissing: Success URL fehlt im Request
|
||||
FailureURLMissing: Failure URL fehlt im Request
|
||||
StateMissing: State parameter fehlt im Request
|
||||
NotStarted: Intent wurde nicht gestartet oder wurde bereits beendet
|
||||
NotSucceeded: Intent war nicht erfolgreich
|
||||
TokenCreationFailed: Tokenerstellung schlug fehl
|
||||
InvalidToken: Intent Token ist ungültig
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: User could not be found on chosen organization
|
||||
NotAllowedOrg: User is no member of the required organization
|
||||
UserIDMissing: User ID missing
|
||||
UserIDWrong: "Request user not equal to authenticated user"
|
||||
DomainPolicyNil: Organisation Policy is empty
|
||||
EmailAsUsernameNotAllowed: Email is not allowed as username
|
||||
Invalid: Userdata is invalid
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: No changes found
|
||||
InitCodeNotFound: Initialization Code not found
|
||||
UsernameNotChanged: Username not changed
|
||||
InvalidURLTemplate: URL Template is invalid
|
||||
Profile:
|
||||
NotFound: Profile not found
|
||||
NotChanged: Profile not changed
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: Email not changed
|
||||
Empty: Email is empty
|
||||
IDMissing: Email ID is missing
|
||||
InvalidURLTemplate: URL Template is invalid
|
||||
Phone:
|
||||
NotFound: Phone not found
|
||||
Invalid: Phone is invalid
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: Session already terminated
|
||||
Token:
|
||||
Invalid: Session Token is invalid
|
||||
Intent:
|
||||
IDPMissing: IDP ID is missing in the request
|
||||
SuccessURLMissing: Success URL is missing in the request
|
||||
FailureURLMissing: Failure URL is missing in the request
|
||||
StateMissing: State parameter is missing in the request
|
||||
NotStarted: Intent is not started or was already terminated
|
||||
NotSucceeded: Intent has not succeeded
|
||||
TokenCreationFailed: Token creation failed
|
||||
InvalidToken: Intent Token is invalid
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: El usuario no pudo encontrarse en la organización elegida
|
||||
NotAllowedOrg: El usuario no es miembro de la organización requerida
|
||||
UserIDMissing: Falta el ID de usuario
|
||||
UserIDWrong: "Solicitud de usuario no igual al usuario autenticado"
|
||||
DomainPolicyNil: Falta la política de la organización
|
||||
EmailAsUsernameNotAllowed: La dirección de Email no se permite como nombre de usuario
|
||||
Invalid: Los datos de usuario no son válidos
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: No se encontraron cambios
|
||||
InitCodeNotFound: Código de inicialización no encontrado
|
||||
UsernameNotChanged: El nombre de usuario no cambió
|
||||
InvalidURLTemplate: La plantilla URL no es válida
|
||||
Profile:
|
||||
NotFound: Perfil no encontrado
|
||||
NotChanged: El perfil no ha cambiado
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: El email no ha cambiado
|
||||
Empty: El email no está vacío
|
||||
IDMissing: Falta el ID del email
|
||||
InvalidURLTemplate: La plantilla URL no es válida
|
||||
Phone:
|
||||
NotFound: Teléfono no encontrado
|
||||
Invalid: El teléfono no es válido
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: Sesión ya terminada
|
||||
Token:
|
||||
Invalid: El identificador de sesión no es válido
|
||||
Intent:
|
||||
IDPMissing: Falta IDP en la solicitud
|
||||
SuccessURLMissing: Falta la URL de éxito en la solicitud
|
||||
FailureURLMissing: Falta la URL de error en la solicitud
|
||||
StateMissing: Falta un parámetro de estado en la solicitud
|
||||
NotStarted: La intención no se ha iniciado o ya ha finalizado
|
||||
NotSucceeded: Intento fallido
|
||||
TokenCreationFailed: Fallo en la creación del token
|
||||
InvalidToken: El token de la intención no es válido
|
||||
|
||||
AggregateTypes:
|
||||
action: Acción
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: L'utilisateur n'a pas été trouvé dans l'organisation choisie
|
||||
NotAllowedOrg: L'utilisateur n'est pas membre de l'organisation requise
|
||||
UserIDMissing: L'ID de l'utilisateur est manquant
|
||||
UserIDWrong: "L'utilisateur de la demande n'est pas égal à l'utilisateur authentifié"
|
||||
DomainPolicyNil: La politique de l'organisation est vide
|
||||
EmailAsUsernameNotAllowed: L'email n'est pas autorisé comme nom d'utilisateur
|
||||
Invalid: Les données de l'utilisateur ne sont pas valides
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: Aucun changement trouvé
|
||||
InitCodeNotFound: Code d'initialisation non trouvé
|
||||
UsernameNotChanged: Nom d'utilisateur non modifié
|
||||
InvalidURLTemplate: Le modèle d'URL n'est pas valide
|
||||
Profile:
|
||||
NotFound: Profil non trouvé
|
||||
NotChanged: Le profil n'a pas changé
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: L'adresse électronique n'a pas changé
|
||||
Empty: Email est vide
|
||||
IDMissing: Email ID manquant
|
||||
InvalidURLTemplate: Le modèle d'URL n'est pas valide
|
||||
Phone:
|
||||
Notfound: Téléphone non trouvé
|
||||
Invalid: Le téléphone n'est pas valide
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: La session est déjà terminée
|
||||
Token:
|
||||
Invalid: Le jeton de session n'est pas valide
|
||||
Intent:
|
||||
IDPMissing: IDP manquant dans la requête
|
||||
SuccessURLMissing: Success URL absent de la requête
|
||||
FailureURLMissing: Failure URL absent de la requête
|
||||
StateMissing: Paramètre d'état manquant dans la requête
|
||||
NotStarted: Intent n'a pas démarré ou s'est déjà terminé
|
||||
NotSucceeded: l'intention n'a pas abouti
|
||||
TokenCreationFailed: La création du token a échoué
|
||||
InvalidToken: Le jeton d'intention n'est pas valide
|
||||
|
||||
AggregateTypes:
|
||||
action: Action
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: L'utente non è stato trovato nell'organizzazione scelta
|
||||
NotAllowedOrg: L'utente non è membro dell'organizzazione richiesta
|
||||
UserIDMissing: ID utente mancante
|
||||
UserIDWrong: "Utente richiesta non uguale all'utente autenticato"
|
||||
DomainPolicyNil: Impostazione Org IAM mancante
|
||||
EmailAsUsernameNotAllowed: L'e-mail non è consentita come nome utente
|
||||
Invalid: I dati utente non sono validi
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: Nessun cambiamento trovato
|
||||
InitCodeNotFound: Codice di inizializzazione non trovato
|
||||
UsernameNotChanged: Nome utente non cambiato
|
||||
InvalidURLTemplate: Il modello di URL non è valido
|
||||
Profile:
|
||||
NotFound: Profilo non trovato
|
||||
NotChanged: Profilo non cambiato
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: Email non cambiata
|
||||
Empty: Email è vuota
|
||||
IDMissing: Email ID mancante
|
||||
InvalidURLTemplate: Il modello di URL non è valido
|
||||
Phone:
|
||||
NotFound: Telefono non trovato
|
||||
Invalid: Il telefono non è valido
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: Sessione già terminata
|
||||
Token:
|
||||
Invalid: Il token della sessione non è valido
|
||||
Intent:
|
||||
IDPMissing: IDP mancante nella richiesta
|
||||
SuccessURLMissing: URL di successo mancante nella richiesta
|
||||
FailureURLMissing: URL di errore mancante nella richiesta
|
||||
StateMissing: parametro di stato mancante nella richiesta
|
||||
NotStarted: l'intento non è stato avviato o è già stato terminato
|
||||
NotSucceeded: l'intento non è andato a buon fine
|
||||
TokenCreationFailed: creazione del token fallita
|
||||
InvalidToken: Il token dell'intento non è valido
|
||||
|
||||
AggregateTypes:
|
||||
action: Azione
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: ユーザーが選択した組織内で見つかりません
|
||||
NotAllowedOrg: ユーザーが必要な組織のメンバーでありません
|
||||
UserIDMissing: ユーザーIDがありません
|
||||
UserIDWrong: "リクエストユーザーが認証されたユーザーと等しくない"
|
||||
DomainPolicyNil: 組織ポリシーが空です
|
||||
EmailAsUsernameNotAllowed: メールアドレスはユーザー名として使用できません
|
||||
Invalid: 無効なユーザーデータです
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: 変更は見つかりません
|
||||
InitCodeNotFound: 初期化コードが見つかりません
|
||||
UsernameNotChanged: ユーザー名は変更されていません
|
||||
InvalidURLTemplate: URLテンプレートが無効です
|
||||
Profile:
|
||||
NotFound: プロファイルが見つかりません
|
||||
NotChanged: プロファイルが変更されていません
|
||||
@@ -76,7 +78,6 @@ Errors:
|
||||
Invalid: 無効なメールアドレスです
|
||||
AlreadyVerified: メールアドレスはすでに検証済みです
|
||||
NotChanged: メールアドレスが変更されていません
|
||||
InvalidURLTemplate: URLテンプレートが無効です
|
||||
Phone:
|
||||
NotFound: 電話番号が見つかりません
|
||||
Invalid: 無効な電話番号です
|
||||
@@ -464,6 +465,15 @@ Errors:
|
||||
Terminated: セッションはすでに終了しています
|
||||
Token:
|
||||
Invalid: セッショントークンが無効です
|
||||
Intent:
|
||||
IDPMissing: リクエストにIDP IDが含まれていません
|
||||
SuccessURLMissing: リクエストに成功時の URL がありません
|
||||
FailureURLMissing: リクエストに失敗の URL がありません
|
||||
StateMissing: リクエストに State パラメータがありません
|
||||
NotStarted: インテントが開始されなかったか、既に終了している
|
||||
NotSucceeded: インテントが成功しなかった
|
||||
TokenCreationFailed: トークンの作成に失敗しました
|
||||
InvalidToken: インテントのトークンが無効である
|
||||
|
||||
AggregateTypes:
|
||||
action: アクション
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: Użytkownik nie został znaleziony w wybranej organizacji
|
||||
NotAllowedOrg: Użytkownik nie jest członkiem wymaganej organizacji
|
||||
UserIDMissing: Brakuje ID użytkownika
|
||||
UserIDWrong: "Żądanie użytkownika nie jest równe uwierzytelnionemu użytkownikowi"
|
||||
DomainPolicyNil: Polityka organizacji jest pusta
|
||||
EmailAsUsernameNotAllowed: Adres e-mail nie jest dozwolony jako nazwa użytkownika
|
||||
Invalid: Dane użytkownika są nieprawidłowe
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: Nie znaleziono zmian
|
||||
InitCodeNotFound: Kod inicjalizacji nie znaleziony
|
||||
UsernameNotChanged: Nazwa użytkownika nie została zmieniona
|
||||
InvalidURLTemplate: Szablon URL jest nieprawidłowy
|
||||
Profile:
|
||||
NotFound: Profil nie znaleziony
|
||||
NotChanged: Profil nie zmieniony
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: Adres e-mail nie zmieniony
|
||||
Empty: Adres e-mail jest pusty
|
||||
IDMissing: Adres e-mail ID brakuje
|
||||
InvalidURLTemplate: Szablon URL jest nieprawidłowy
|
||||
Phone:
|
||||
NotFound: Numer telefonu nie znaleziony
|
||||
Invalid: Numer telefonu jest nieprawidłowy
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: Sesja już zakończona
|
||||
Token:
|
||||
Invalid: Token sesji jest nieprawidłowy
|
||||
Intent:
|
||||
IDPMissing: Brak identyfikatora IDP w żądaniu
|
||||
SuccessURLMissing: Brak adresu URL powodzenia w żądaniu
|
||||
FailureURLMissing: Brak adresu URL niepowodzenia w żądaniu
|
||||
StateMissing: Brak parametru stanu w żądaniu
|
||||
NotStarted: Intencja nie została rozpoczęta lub już się zakończyła
|
||||
NotSucceeded: intencja nie powiodła się
|
||||
TokenCreationFailed: Tworzenie tokena nie powiodło się
|
||||
InvalidToken: Token intencji jest nieprawidłowy
|
||||
|
||||
AggregateTypes:
|
||||
action: Działanie
|
||||
|
||||
@@ -53,6 +53,7 @@ Errors:
|
||||
NotFoundOnOrg: 在所选组织中找不到用户
|
||||
NotAllowedOrg: 用户不是所需组织的成员
|
||||
UserIDMissing: 缺少用户 ID
|
||||
UserIDWrong: "请求用户不等于经过身份验证的用户"
|
||||
DomainPolicyNil: 组织策略为空
|
||||
EmailAsUsernameNotAllowed: 电子邮件不允许作为用户名
|
||||
Invalid: 用户数据无效
|
||||
@@ -67,6 +68,7 @@ Errors:
|
||||
NoChanges: 未发现任何更改
|
||||
InitCodeNotFound: 未找到初始化验证码
|
||||
UsernameNotChanged: 用户名未更改
|
||||
InvalidURLTemplate: URL模板无效
|
||||
Profile:
|
||||
NotFound: 未找到个人资料
|
||||
NotChanged: 个人资料未更改
|
||||
@@ -81,7 +83,6 @@ Errors:
|
||||
NotChanged: 电子邮件未更改
|
||||
Empty: 电子邮件是空的
|
||||
IDMissing: 电子邮件ID丢失
|
||||
InvalidURLTemplate: URL模板无效
|
||||
Phone:
|
||||
NotFound: 手机号码未找到
|
||||
Invalid: 手机号码无效
|
||||
@@ -475,6 +476,15 @@ Errors:
|
||||
Terminated: 会话已经终止
|
||||
Token:
|
||||
Invalid: 会话令牌是无效的
|
||||
Intent:
|
||||
IDPMissing: 请求中缺少IDP ID
|
||||
SuccessURLMissing: 请求中缺少成功URL
|
||||
FailureURLMissing: 请求中缺少失败的URL
|
||||
StateMissing: 请求中缺少状态参数
|
||||
NotStarted: 意图没有开始或已经结束
|
||||
NotSucceeded: 意图不成功
|
||||
TokenCreationFailed: 令牌创建失败
|
||||
InvalidToken: 意图令牌是无效的
|
||||
|
||||
AggregateTypes:
|
||||
action: 动作
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/descope/virtualwebauthn"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
rp virtualwebauthn.RelyingParty
|
||||
auth virtualwebauthn.Authenticator
|
||||
credential virtualwebauthn.Credential
|
||||
}
|
||||
|
||||
func NewClient(name, domain, origin string) *Client {
|
||||
rp := virtualwebauthn.RelyingParty{
|
||||
Name: name,
|
||||
ID: domain,
|
||||
Origin: origin,
|
||||
}
|
||||
return &Client{
|
||||
rp: rp,
|
||||
auth: virtualwebauthn.NewAuthenticator(),
|
||||
credential: virtualwebauthn.NewCredential(virtualwebauthn.KeyTypeEC2),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) CreateAttestationResponse(options []byte) ([]byte, error) {
|
||||
parsedAttestationOptions, err := virtualwebauthn.ParseAttestationOptions(string(options))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err)
|
||||
}
|
||||
return []byte(virtualwebauthn.CreateAttestationResponse(
|
||||
c.rp, c.auth, c.credential, *parsedAttestationOptions,
|
||||
)), nil
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"github.com/duo-labs/webauthn/protocol"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/duo-labs/webauthn/protocol"
|
||||
"github.com/duo-labs/webauthn/webauthn"
|
||||
"github.com/go-webauthn/webauthn/protocol"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
@@ -177,9 +177,13 @@ func (w *Config) FinishLogin(ctx context.Context, user *domain.Human, webAuthN *
|
||||
|
||||
func (w *Config) serverFromContext(ctx context.Context) (*webauthn.WebAuthn, error) {
|
||||
instance := authz.GetInstance(ctx)
|
||||
return webauthn.New(&webauthn.Config{
|
||||
webAuthn, err := webauthn.New(&webauthn.Config{
|
||||
RPDisplayName: w.DisplayName,
|
||||
RPID: instance.RequestedDomain(),
|
||||
RPOrigin: http.BuildOrigin(instance.RequestedHost(), w.ExternalSecure),
|
||||
RPOrigins: []string{http.BuildOrigin(instance.RequestedHost(), w.ExternalSecure)},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "WEBAU-UX9ta", "Errors.User.WebAuthN.ServerConfig")
|
||||
}
|
||||
return webAuthn, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package webauthn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
)
|
||||
|
||||
func TestConfig_serverFromContext(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *webauthn.WebAuthn
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "webauthn error",
|
||||
args: args{context.Background()},
|
||||
wantErr: caos_errs.ThrowInternal(nil, "WEBAU-UX9ta", "Errors.User.WebAuthN.ServerConfig"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{authz.WithRequestedDomain(context.Background(), "example.com")},
|
||||
want: &webauthn.WebAuthn{
|
||||
Config: &webauthn.Config{
|
||||
RPDisplayName: "DisplayName",
|
||||
RPID: "example.com",
|
||||
RPOrigins: []string{"https://example.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := &Config{
|
||||
DisplayName: "DisplayName",
|
||||
ExternalSecure: true,
|
||||
}
|
||||
got, err := w.serverFromContext(tt.args.ctx)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
if tt.want != nil {
|
||||
require.NotNil(t, got)
|
||||
assert.Equal(t, tt.want.Config.RPDisplayName, got.Config.RPDisplayName)
|
||||
assert.Equal(t, tt.want.Config.RPID, got.Config.RPID)
|
||||
assert.Equal(t, tt.want.Config.RPOrigins, got.Config.RPOrigins)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user