mirror of
https://github.com/zitadel/zitadel.git
synced 2025-04-15 20:41:30 +00:00
feat(v2): register user u2f (#6020)
This commit is contained in:
parent
5b3f04e4d9
commit
2e323e8044
@ -27,7 +27,7 @@ var (
|
|||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
os.Exit(func() int {
|
os.Exit(func() int {
|
||||||
ctx, errCtx, cancel := integration.Contexts(time.Hour)
|
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
Tester = integration.NewTester(ctx)
|
Tester = integration.NewTester(ctx)
|
||||||
@ -55,7 +55,7 @@ retry:
|
|||||||
s = resp.GetSession()
|
s = resp.GetSession()
|
||||||
break retry
|
break retry
|
||||||
}
|
}
|
||||||
if status.Convert(err).Code() == codes.NotFound {
|
if code := status.Convert(err).Code(); code == codes.NotFound || code == codes.PermissionDenied {
|
||||||
select {
|
select {
|
||||||
case <-CTX.Done():
|
case <-CTX.Done():
|
||||||
t.Fatal(CTX.Err(), err)
|
t.Fatal(CTX.Err(), err)
|
||||||
|
@ -3,13 +3,13 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/internal/api/authz"
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
||||||
"github.com/zitadel/zitadel/internal/domain"
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
caos_errs "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"
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -41,24 +41,32 @@ func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.Authentic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func passkeyRegistrationDetailsToPb(details *domain.PasskeyRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
|
func webAuthNRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*object_pb.Details, *structpb.Struct, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
options := new(structpb.Struct)
|
||||||
|
if err := options.UnmarshalJSON(details.PublicKeyCredentialCreationOptions); err != nil {
|
||||||
|
return nil, nil, caos_errs.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal")
|
||||||
|
}
|
||||||
|
return object.DomainToDetailsPb(details.ObjectDetails), options, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func passkeyRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
|
||||||
|
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
options := new(structpb.Struct)
|
|
||||||
if err := protojson.Unmarshal(details.PublicKeyCredentialCreationOptions, options); err != nil {
|
|
||||||
return nil, caos_errs.ThrowInternal(err, "USERv2-Dohr6", "Errors.Internal")
|
|
||||||
}
|
|
||||||
return &user.RegisterPasskeyResponse{
|
return &user.RegisterPasskeyResponse{
|
||||||
Details: object.DomainToDetailsPb(details.ObjectDetails),
|
Details: objectDetails,
|
||||||
PasskeyId: details.PasskeyID,
|
PasskeyId: details.ID,
|
||||||
PublicKeyCredentialCreationOptions: options,
|
PublicKeyCredentialCreationOptions: options,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
|
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
|
||||||
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
||||||
pkc, err := protojson.Marshal(req.GetPublicKeyCredential())
|
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
|
return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,8 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
/* TODO after we are able to obtain a Bearer token for a human user
|
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||||
|
https://github.com/zitadel/zitadel/issues/6022
|
||||||
{
|
{
|
||||||
name: "human user",
|
name: "human user",
|
||||||
args: args{
|
args: args{
|
||||||
|
@ -50,7 +50,7 @@ func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
|||||||
|
|
||||||
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
details *domain.PasskeyRegistrationDetails
|
details *domain.WebAuthNRegistrationDetails
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -70,13 +70,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "unmarshall error",
|
name: "unmarshall error",
|
||||||
args: args{
|
args: args{
|
||||||
details: &domain.PasskeyRegistrationDetails{
|
details: &domain.WebAuthNRegistrationDetails{
|
||||||
ObjectDetails: &domain.ObjectDetails{
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
Sequence: 22,
|
Sequence: 22,
|
||||||
EventDate: time.Unix(3000, 22),
|
EventDate: time.Unix(3000, 22),
|
||||||
ResourceOwner: "me",
|
ResourceOwner: "me",
|
||||||
},
|
},
|
||||||
PasskeyID: "123",
|
ID: "123",
|
||||||
PublicKeyCredentialCreationOptions: []byte(`\\`),
|
PublicKeyCredentialCreationOptions: []byte(`\\`),
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
@ -86,13 +86,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "ok",
|
name: "ok",
|
||||||
args: args{
|
args: args{
|
||||||
details: &domain.PasskeyRegistrationDetails{
|
details: &domain.WebAuthNRegistrationDetails{
|
||||||
ObjectDetails: &domain.ObjectDetails{
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
Sequence: 22,
|
Sequence: 22,
|
||||||
EventDate: time.Unix(3000, 22),
|
EventDate: time.Unix(3000, 22),
|
||||||
ResourceOwner: "me",
|
ResourceOwner: "me",
|
||||||
},
|
},
|
||||||
PasskeyID: "123",
|
ID: "123",
|
||||||
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
|
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
|
||||||
},
|
},
|
||||||
err: nil,
|
err: nil,
|
||||||
|
44
internal/api/grpc/user/v2/u2f.go
Normal file
44
internal/api/grpc/user/v2/u2f.go
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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) RegisterU2F(ctx context.Context, req *user.RegisterU2FRequest) (*user.RegisterU2FResponse, error) {
|
||||||
|
return u2fRegistrationDetailsToPb(
|
||||||
|
s.command.RegisterUserU2F(ctx, req.GetUserId(), authz.GetCtxData(ctx).ResourceOwner),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func u2fRegistrationDetailsToPb(details *domain.WebAuthNRegistrationDetails, err error) (*user.RegisterU2FResponse, error) {
|
||||||
|
objectDetails, options, err := webAuthNRegistrationDetailsToPb(details, err)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.RegisterU2FResponse{
|
||||||
|
Details: objectDetails,
|
||||||
|
U2FId: details.ID,
|
||||||
|
PublicKeyCredentialCreationOptions: options,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) VerifyU2FRegistration(ctx context.Context, req *user.VerifyU2FRegistrationRequest) (*user.VerifyU2FRegistrationResponse, error) {
|
||||||
|
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
||||||
|
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, caos_errs.ThrowInternal(err, "USERv2-IeTh4", "Errors.Internal")
|
||||||
|
}
|
||||||
|
objectDetails, err := s.command.HumanVerifyU2FSetup(ctx, req.GetUserId(), resourceOwner, req.GetTokenName(), "", pkc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user.VerifyU2FRegistrationResponse{
|
||||||
|
Details: object.DomainToDetailsPb(objectDetails),
|
||||||
|
}, nil
|
||||||
|
}
|
167
internal/api/grpc/user/v2/u2f_integration_test.go
Normal file
167
internal/api/grpc/user/v2/u2f_integration_test.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/integration"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_RegisterU2F(t *testing.T) {
|
||||||
|
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *user.RegisterU2FRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.RegisterU2FResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing user id",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RegisterU2FRequest{},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "user mismatch",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RegisterU2FRequest{
|
||||||
|
UserId: userID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
/* TODO: after we are able to obtain a Bearer token for a human user
|
||||||
|
https://github.com/zitadel/zitadel/issues/6022
|
||||||
|
{
|
||||||
|
name: "human user",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.RegisterU2FRequest{
|
||||||
|
UserId: userID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.RegisterU2FResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.RegisterU2F(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.GetU2FId())
|
||||||
|
assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions())
|
||||||
|
_, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions())
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_VerifyU2FRegistration(t *testing.T) {
|
||||||
|
userID := Tester.CreateHumanUser(CTX).GetUserId()
|
||||||
|
/* TODO after we are able to obtain a Bearer token for a human user
|
||||||
|
pkr, err := Client.RegisterU2F(CTX, &user.RegisterU2FRequest{
|
||||||
|
UserId: userID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
|
||||||
|
|
||||||
|
attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
|
||||||
|
require.NoError(t, err)
|
||||||
|
*/
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
ctx context.Context
|
||||||
|
req *user.VerifyU2FRegistrationRequest
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.VerifyU2FRegistrationResponse
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing user id",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.VerifyU2FRegistrationRequest{
|
||||||
|
U2FId: "123",
|
||||||
|
TokenName: "nice name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
/* TODO after we are able to obtain a Bearer token for a human user
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.VerifyU2FRegistrationRequest{
|
||||||
|
UserId: userID,
|
||||||
|
U2FId: pkr.GetU2FId(),
|
||||||
|
PublicKeyCredential: attestationResponse,
|
||||||
|
TokenName: "nice name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &user.VerifyU2FRegistrationResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
ResourceOwner: Tester.Organisation.ID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
name: "wrong credential",
|
||||||
|
args: args{
|
||||||
|
ctx: CTX,
|
||||||
|
req: &user.VerifyU2FRegistrationRequest{
|
||||||
|
UserId: userID,
|
||||||
|
U2FId: "123",
|
||||||
|
PublicKeyCredential: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
|
||||||
|
},
|
||||||
|
TokenName: "nice name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := Client.VerifyU2FRegistration(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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
97
internal/api/grpc/user/v2/u2f_test.go
Normal file
97
internal/api/grpc/user/v2/u2f_test.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/grpc"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||||
|
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
|
||||||
|
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_u2fRegistrationDetailsToPb(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
details *domain.WebAuthNRegistrationDetails
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *user.RegisterU2FResponse
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "an error",
|
||||||
|
args: args{
|
||||||
|
details: nil,
|
||||||
|
err: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unmarshall error",
|
||||||
|
args: args{
|
||||||
|
details: &domain.WebAuthNRegistrationDetails{
|
||||||
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
|
Sequence: 22,
|
||||||
|
EventDate: time.Unix(3000, 22),
|
||||||
|
ResourceOwner: "me",
|
||||||
|
},
|
||||||
|
ID: "123",
|
||||||
|
PublicKeyCredentialCreationOptions: []byte(`\\`),
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
wantErr: caos_errs.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
args: args{
|
||||||
|
details: &domain.WebAuthNRegistrationDetails{
|
||||||
|
ObjectDetails: &domain.ObjectDetails{
|
||||||
|
Sequence: 22,
|
||||||
|
EventDate: time.Unix(3000, 22),
|
||||||
|
ResourceOwner: "me",
|
||||||
|
},
|
||||||
|
ID: "123",
|
||||||
|
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
want: &user.RegisterU2FResponse{
|
||||||
|
Details: &object.Details{
|
||||||
|
Sequence: 22,
|
||||||
|
ChangeDate: ×tamppb.Timestamp{
|
||||||
|
Seconds: 3000,
|
||||||
|
Nanos: 22,
|
||||||
|
},
|
||||||
|
ResourceOwner: "me",
|
||||||
|
},
|
||||||
|
U2FId: "123",
|
||||||
|
PublicKeyCredentialCreationOptions: &structpb.Struct{
|
||||||
|
Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := u2fRegistrationDetailsToPb(tt.args.details, tt.args.err)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if !proto.Equal(tt.want, got) {
|
||||||
|
t.Errorf("Not equal:\nExpected\n%s\nActual:%s", tt.want, got)
|
||||||
|
}
|
||||||
|
if tt.want != nil {
|
||||||
|
grpc.AllFieldsSet(t, got.ProtoReflect())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// RegisterUserPasskey creates a passkey registration for the current authenticated 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.
|
// 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) {
|
func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -25,7 +25,7 @@ func (c *Commands) RegisterUserPasskey(ctx context.Context, userID, resourceOwne
|
|||||||
|
|
||||||
// RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id.
|
// RegisterUserPasskeyWithCode registers a new passkey for a unauthenticated user id.
|
||||||
// The resource is protected by the code, identified by the codeID.
|
// 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) {
|
func (c *Commands) RegisterUserPasskeyWithCode(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, codeID, code string, alg crypto.EncryptionAlgorithm) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg)
|
event, err := c.verifyUserPasskeyCode(ctx, userID, resourceOwner, codeID, code, alg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -63,7 +63,7 @@ func (c *Commands) verifyUserPasskeyCodeFailed(ctx context.Context, wm *HumanPas
|
|||||||
logging.WithFields("userID", userAgg.ID).OnError(err).Error("RegisterUserPasskeyWithCode push failed")
|
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) {
|
func (c *Commands) registerUserPasskey(ctx context.Context, userID, resourceOwner string, authenticator domain.AuthenticatorAttachment, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator)
|
wm, userAgg, webAuthN, err := c.createUserPasskey(ctx, userID, resourceOwner, authenticator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -79,7 +79,7 @@ func (c *Commands) createUserPasskey(ctx context.Context, userID, resourceOwner
|
|||||||
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, passwordlessTokens, authenticator, domain.UserVerificationRequirementRequired)
|
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) {
|
func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken, events ...eventCallback) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
cmds := make([]eventstore.Command, len(events)+1)
|
cmds := make([]eventstore.Command, len(events)+1)
|
||||||
cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
cmds[0] = user.NewHumanPasswordlessAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
||||||
for i, event := range events {
|
for i, event := range events {
|
||||||
@ -90,9 +90,9 @@ func (c *Commands) pushUserPasskey(ctx context.Context, wm *HumanWebAuthNWriteMo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &domain.PasskeyRegistrationDetails{
|
return &domain.WebAuthNRegistrationDetails{
|
||||||
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
|
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
|
||||||
PasskeyID: wm.WebauthNTokenID,
|
ID: wm.WebauthNTokenID,
|
||||||
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
|
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ func TestCommands_RegisterUserPasskey(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
fields fields
|
fields fields
|
||||||
args args
|
args args
|
||||||
want *domain.PasskeyRegistrationDetails
|
want *domain.WebAuthNRegistrationDetails
|
||||||
wantErr error
|
wantErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -449,7 +449,7 @@ func TestCommands_pushUserPasskey(t *testing.T) {
|
|||||||
require.ErrorIs(t, err, tt.wantErr)
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
if tt.wantErr == nil {
|
if tt.wantErr == nil {
|
||||||
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
|
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
|
||||||
assert.Equal(t, "123", got.PasskeyID)
|
assert.Equal(t, "123", got.ID)
|
||||||
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
|
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
46
internal/command/user_v2_u2f.go
Normal file
46
internal/command/user_v2_u2f.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/internal/api/authz"
|
||||||
|
"github.com/zitadel/zitadel/internal/domain"
|
||||||
|
"github.com/zitadel/zitadel/internal/eventstore"
|
||||||
|
"github.com/zitadel/zitadel/internal/repository/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Commands) RegisterUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
|
if err := authz.UserIDInCTX(ctx, userID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.registerUserU2F(ctx, userID, resourceOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) registerUserU2F(ctx context.Context, userID, resourceOwner string) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
|
wm, userAgg, webAuthN, err := c.createUserU2F(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.pushUserU2F(ctx, wm, userAgg, webAuthN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) createUserU2F(ctx context.Context, userID, resourceOwner string) (*HumanWebAuthNWriteModel, *eventstore.Aggregate, *domain.WebAuthNToken, error) {
|
||||||
|
tokens, err := c.getHumanU2FTokens(ctx, userID, resourceOwner)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return c.addHumanWebAuthN(ctx, userID, resourceOwner, false, tokens, domain.AuthenticatorAttachmentUnspecified, domain.UserVerificationRequirementRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commands) pushUserU2F(ctx context.Context, wm *HumanWebAuthNWriteModel, userAgg *eventstore.Aggregate, webAuthN *domain.WebAuthNToken) (*domain.WebAuthNRegistrationDetails, error) {
|
||||||
|
cmd := user.NewHumanU2FAddedEvent(ctx, userAgg, wm.WebauthNTokenID, webAuthN.Challenge)
|
||||||
|
err := c.pushAppendAndReduce(ctx, wm, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &domain.WebAuthNRegistrationDetails{
|
||||||
|
ObjectDetails: writeModelToObjectDetails(&wm.WriteModel),
|
||||||
|
ID: wm.WebauthNTokenID,
|
||||||
|
PublicKeyCredentialCreationOptions: webAuthN.CredentialCreationData,
|
||||||
|
}, nil
|
||||||
|
}
|
215
internal/command/user_v2_u2f_test.go
Normal file
215
internal/command/user_v2_u2f_test.go
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/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_RegisterUserU2F(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
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
want *domain.WebAuthNRegistrationDetails
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "wrong user",
|
||||||
|
args: args{
|
||||||
|
userID: "foo",
|
||||||
|
resourceOwner: "org1",
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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@test.ch",
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
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.RegisterUserU2F(ctx, tt.args.userID, tt.args.resourceOwner)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
// successful case can't be tested due to random challenge.
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommands_pushUserU2F(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(), // getHumanU2FTokens
|
||||||
|
expectFilter(eventFromEventPusher(
|
||||||
|
user.NewHumanAddedEvent(ctx,
|
||||||
|
userAgg,
|
||||||
|
"username",
|
||||||
|
"firstname",
|
||||||
|
"lastname",
|
||||||
|
"nickname",
|
||||||
|
"displayname",
|
||||||
|
language.German,
|
||||||
|
domain.GenderUnspecified,
|
||||||
|
"email@test.ch",
|
||||||
|
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"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expectPush func(challenge string) expect
|
||||||
|
wantErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "push error",
|
||||||
|
expectPush: func(challenge string) expect {
|
||||||
|
return expectPushFailed(io.ErrClosedPipe, []*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanU2FAddedEvent(ctx,
|
||||||
|
userAgg, "123", challenge,
|
||||||
|
),
|
||||||
|
)})
|
||||||
|
},
|
||||||
|
wantErr: io.ErrClosedPipe,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
expectPush: func(challenge string) expect {
|
||||||
|
return expectPush([]*repository.Event{eventFromEventPusher(
|
||||||
|
user.NewHumanU2FAddedEvent(ctx,
|
||||||
|
userAgg, "123", challenge,
|
||||||
|
),
|
||||||
|
)})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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.pushUserU2F(ctx, wm, userAgg, webAuthN)
|
||||||
|
require.ErrorIs(t, err, tt.wantErr)
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
assert.NotEmpty(t, got.PublicKeyCredentialCreationOptions)
|
||||||
|
assert.Equal(t, "123", got.ID)
|
||||||
|
assert.Equal(t, "org1", got.ObjectDetails.ResourceOwner)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -21,9 +21,9 @@ type PasskeyCodeDetails struct {
|
|||||||
Code string
|
Code string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PasskeyRegistrationDetails struct {
|
type WebAuthNRegistrationDetails struct {
|
||||||
*ObjectDetails
|
*ObjectDetails
|
||||||
|
|
||||||
PasskeyID string
|
ID string
|
||||||
PublicKeyCredentialCreationOptions []byte
|
PublicKeyCredentialCreationOptions []byte
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user