mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 10:49:25 +00:00
feat(v2): register user u2f (#6020)
This commit is contained in:
@@ -27,7 +27,7 @@ var (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(func() int {
|
||||
ctx, errCtx, cancel := integration.Contexts(time.Hour)
|
||||
ctx, errCtx, cancel := integration.Contexts(5 * time.Minute)
|
||||
defer cancel()
|
||||
|
||||
Tester = integration.NewTester(ctx)
|
||||
@@ -55,7 +55,7 @@ retry:
|
||||
s = resp.GetSession()
|
||||
break retry
|
||||
}
|
||||
if status.Convert(err).Code() == codes.NotFound {
|
||||
if code := status.Convert(err).Code(); code == codes.NotFound || code == codes.PermissionDenied {
|
||||
select {
|
||||
case <-CTX.Done():
|
||||
t.Fatal(CTX.Err(), err)
|
||||
|
@@ -3,13 +3,13 @@ package user
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"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"
|
||||
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/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 {
|
||||
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{
|
||||
Details: object.DomainToDetailsPb(details.ObjectDetails),
|
||||
PasskeyId: details.PasskeyID,
|
||||
Details: objectDetails,
|
||||
PasskeyId: details.ID,
|
||||
PublicKeyCredentialCreationOptions: options,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
|
||||
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
|
||||
pkc, err := protojson.Marshal(req.GetPublicKeyCredential())
|
||||
pkc, err := req.GetPublicKeyCredential().MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal")
|
||||
}
|
||||
|
@@ -94,7 +94,8 @@ func TestServer_RegisterPasskey(t *testing.T) {
|
||||
},
|
||||
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",
|
||||
args: args{
|
||||
|
@@ -50,7 +50,7 @@ func Test_passkeyAuthenticatorToDomain(t *testing.T) {
|
||||
|
||||
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
type args struct {
|
||||
details *domain.PasskeyRegistrationDetails
|
||||
details *domain.WebAuthNRegistrationDetails
|
||||
err error
|
||||
}
|
||||
tests := []struct {
|
||||
@@ -70,13 +70,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
{
|
||||
name: "unmarshall error",
|
||||
args: args{
|
||||
details: &domain.PasskeyRegistrationDetails{
|
||||
details: &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyID: "123",
|
||||
ID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte(`\\`),
|
||||
},
|
||||
err: nil,
|
||||
@@ -86,13 +86,13 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
details: &domain.PasskeyRegistrationDetails{
|
||||
details: &domain.WebAuthNRegistrationDetails{
|
||||
ObjectDetails: &domain.ObjectDetails{
|
||||
Sequence: 22,
|
||||
EventDate: time.Unix(3000, 22),
|
||||
ResourceOwner: "me",
|
||||
},
|
||||
PasskeyID: "123",
|
||||
ID: "123",
|
||||
PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`),
|
||||
},
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user