feat: implement register Passkey user API v2 (#5873)

* command/crypto: DRY the code

- reuse the the algorithm switch to create a secret generator
- add a verifyCryptoCode function

* command: crypto code tests

* migrate webauthn package

* finish integration tests with webauthn mock client
This commit is contained in:
Tim Möhlmann
2023-05-24 13:22:00 +03:00
committed by GitHub
parent 6839a5c203
commit a301c40f9f
44 changed files with 2528 additions and 517 deletions

View File

@@ -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})
}

View File

@@ -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())
})

View File

@@ -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
}

View File

@@ -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())
})
}
}

View File

@@ -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
}

View File

@@ -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())
})
}
}