diff --git a/internal/api/grpc/user/v2/passkey.go b/internal/api/grpc/user/v2/passkey.go index eb1b276f8f..e68feb6815 100644 --- a/internal/api/grpc/user/v2/passkey.go +++ b/internal/api/grpc/user/v2/passkey.go @@ -3,6 +3,9 @@ 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" @@ -42,16 +45,24 @@ func passkeyRegistrationDetailsToPb(details *domain.PasskeyRegistrationDetails, 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, - PublicKeyCredentialCreationOptions: details.PublicKeyCredentialCreationOptions, + PublicKeyCredentialCreationOptions: options, }, 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()) + pkc, err := protojson.Marshal(req.GetPublicKeyCredential()) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "USERv2-Pha2o", "Errors.Internal") + } + objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), resourceOwner, req.GetPasskeyName(), "", pkc) if err != nil { return nil, err } diff --git a/internal/api/grpc/user/v2/passkey_integration_test.go b/internal/api/grpc/user/v2/passkey_integration_test.go index 623b2544fd..5bad7ecfd3 100644 --- a/internal/api/grpc/user/v2/passkey_integration_test.go +++ b/internal/api/grpc/user/v2/passkey_integration_test.go @@ -10,19 +10,18 @@ import ( "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" + "google.golang.org/protobuf/types/known/structpb" ) func TestServer_RegisterPasskey(t *testing.T) { - userID := createHumanUser(t).GetUserId() + userID := Tester.CreateHumanUser(CTX).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 @@ -125,7 +124,7 @@ func TestServer_RegisterPasskey(t *testing.T) { if tt.want != nil { assert.NotEmpty(t, got.GetPasskeyId()) assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions()) - _, err := client.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions()) + _, err = Tester.WebAuthN.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions()) require.NoError(t, err) } }) @@ -133,7 +132,7 @@ func TestServer_RegisterPasskey(t *testing.T) { } func TestServer_VerifyPasskeyRegistration(t *testing.T) { - userID := createHumanUser(t).GetUserId() + userID := Tester.CreateHumanUser(CTX).GetUserId() reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{ UserId: userID, Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{}, @@ -147,8 +146,7 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { 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()) + attestationResponse, err := Tester.WebAuthN.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions()) require.NoError(t, err) type args struct { @@ -167,7 +165,7 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { ctx: CTX, req: &user.VerifyPasskeyRegistrationRequest{ PasskeyId: pkr.GetPasskeyId(), - PublicKeyCredential: []byte(attestationResponse), + PublicKeyCredential: attestationResponse, PasskeyName: "nice name", }, }, @@ -195,10 +193,12 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { args: args{ ctx: CTX, req: &user.VerifyPasskeyRegistrationRequest{ - UserId: userID, - PasskeyId: pkr.GetPasskeyId(), - PublicKeyCredential: []byte("attestationResponseattestationResponseattestationResponse"), - PasskeyName: "nice name", + UserId: userID, + PasskeyId: pkr.GetPasskeyId(), + PublicKeyCredential: &structpb.Struct{ + Fields: map[string]*structpb.Value{"foo": {Kind: &structpb.Value_StringValue{StringValue: "bar"}}}, + }, + PasskeyName: "nice name", }, }, wantErr: true, @@ -219,7 +219,7 @@ func TestServer_VerifyPasskeyRegistration(t *testing.T) { } func TestServer_CreatePasskeyRegistrationLink(t *testing.T) { - userID := createHumanUser(t).GetUserId() + userID := Tester.CreateHumanUser(CTX).GetUserId() type args struct { ctx context.Context diff --git a/internal/api/grpc/user/v2/passkey_test.go b/internal/api/grpc/user/v2/passkey_test.go index d1934c3ca2..b8988a73e7 100644 --- a/internal/api/grpc/user/v2/passkey_test.go +++ b/internal/api/grpc/user/v2/passkey_test.go @@ -7,10 +7,13 @@ import ( "github.com/stretchr/testify/assert" "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" ) @@ -51,9 +54,10 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { err error } tests := []struct { - name string - args args - want *user.RegisterPasskeyResponse + name string + args args + want *user.RegisterPasskeyResponse + wantErr error }{ { name: "an error", @@ -61,6 +65,23 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { details: nil, err: io.ErrClosedPipe, }, + wantErr: io.ErrClosedPipe, + }, + { + name: "unmarshall error", + args: args{ + details: &domain.PasskeyRegistrationDetails{ + ObjectDetails: &domain.ObjectDetails{ + Sequence: 22, + EventDate: time.Unix(3000, 22), + ResourceOwner: "me", + }, + PasskeyID: "123", + PublicKeyCredentialCreationOptions: []byte(`\\`), + }, + err: nil, + }, + wantErr: caos_errs.ThrowInternal(nil, "USERv2-Dohr6", "Errors.Internal"), }, { name: "ok", @@ -72,7 +93,7 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { ResourceOwner: "me", }, PasskeyID: "123", - PublicKeyCredentialCreationOptions: []byte{1, 2, 3}, + PublicKeyCredentialCreationOptions: []byte(`{"foo": "bar"}`), }, err: nil, }, @@ -85,16 +106,20 @@ func Test_passkeyRegistrationDetailsToPb(t *testing.T) { }, ResourceOwner: "me", }, - PasskeyId: "123", - PublicKeyCredentialCreationOptions: []byte{1, 2, 3}, + PasskeyId: "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 := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err) - require.ErrorIs(t, err, tt.args.err) - assert.Equal(t, tt.want, got) + 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()) } diff --git a/internal/webauthn/client.go b/internal/webauthn/client.go index ac7fbe766a..511378feed 100644 --- a/internal/webauthn/client.go +++ b/internal/webauthn/client.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/descope/virtualwebauthn" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/structpb" ) type Client struct { @@ -25,12 +27,40 @@ func NewClient(name, domain, origin string) *Client { } } -func (c *Client) CreateAttestationResponse(options []byte) ([]byte, error) { +func (c *Client) CreateAttestationResponse(optionsPb *structpb.Struct) (*structpb.Struct, error) { + options, err := protojson.Marshal(optionsPb) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) + } parsedAttestationOptions, err := virtualwebauthn.ParseAttestationOptions(string(options)) if err != nil { return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) } - return []byte(virtualwebauthn.CreateAttestationResponse( + resp := new(structpb.Struct) + err = protojson.Unmarshal([]byte(virtualwebauthn.CreateAttestationResponse( c.rp, c.auth, c.credential, *parsedAttestationOptions, - )), nil + )), resp) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAttestationResponse: %w", err) + } + return resp, nil +} + +func (c *Client) CreateAssertionResponse(optionsPb *structpb.Struct) (*structpb.Struct, error) { + options, err := protojson.Marshal(optionsPb) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err) + } + parsedAssertionOptions, err := virtualwebauthn.ParseAssertionOptions(string(options)) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err) + } + resp := new(structpb.Struct) + err = protojson.Unmarshal([]byte(virtualwebauthn.CreateAssertionResponse( + c.rp, c.auth, c.credential, *parsedAssertionOptions, + )), resp) + if err != nil { + return nil, fmt.Errorf("webauthn.Client.CreateAssertionResponse: %w", err) + } + return resp, nil }