authelia/internal/model/webauthn_test.go

421 lines
12 KiB
Go

package model_test
import (
"crypto/rand"
"database/sql"
"testing"
"time"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model"
)
func TestWebAuthnUser(t *testing.T) {
testCases := []struct {
name string
have model.WebAuthnUser
expectedFIDOU2F bool
}{
{
"ShouldNotHaveFIDOU2FByDefault",
model.WebAuthnUser{},
false,
},
{
"ShouldNotHaveFIDOU2FWithIncorrectAttestationType",
model.WebAuthnUser{
Credentials: []model.WebAuthnCredential{
{
AttestationType: "random",
},
},
},
false,
},
{
"ShouldHaveFIDOU2FWithCorrectAttestationType",
model.WebAuthnUser{
Credentials: []model.WebAuthnCredential{
{
AttestationType: "fido-u2f",
},
},
},
true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expectedFIDOU2F, tc.have.HasFIDOU2F())
})
}
}
func TestWebAuthnCredential(t *testing.T) {
testCases := []struct {
name string
have *model.WebAuthnCredential
config *webauthn.Config
now time.Time
authenticator webauthn.Authenticator
expected *model.WebAuthnCredential
}{
{
name: "ShouldUpdate",
have: &model.WebAuthnCredential{
SignCount: 1,
RPID: "",
LastUsedAt: sql.NullTime{Time: time.Unix(0, 0), Valid: true},
},
config: &webauthn.Config{RPID: "https://example.com", RPOrigins: []string{"org.example.com"}},
now: time.Unix(10, 0),
authenticator: webauthn.Authenticator{SignCount: 2, CloneWarning: false},
expected: &model.WebAuthnCredential{
SignCount: 2,
RPID: "https://example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
},
},
{
name: "ShouldUpdateFIDOU2F",
have: &model.WebAuthnCredential{
SignCount: 1,
RPID: "",
LastUsedAt: sql.NullTime{Time: time.Unix(0, 0), Valid: true},
AttestationType: "fido-u2f",
},
config: &webauthn.Config{RPID: "https://example.com", RPOrigins: []string{"org.example.com"}},
now: time.Unix(10, 0),
authenticator: webauthn.Authenticator{SignCount: 2, CloneWarning: false},
expected: &model.WebAuthnCredential{
SignCount: 2,
RPID: "org.example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
AttestationType: "fido-u2f",
},
},
{
name: "ShouldNotUpdateExistingRPID",
have: &model.WebAuthnCredential{
SignCount: 1,
LastUsedAt: sql.NullTime{Time: time.Unix(0, 0), Valid: true},
AttestationType: "fido-u2f",
RPID: "another.example.com",
},
config: &webauthn.Config{RPID: "https://example.com", RPOrigins: []string{"org.example.com"}},
now: time.Unix(10, 0),
authenticator: webauthn.Authenticator{SignCount: 2, CloneWarning: false},
expected: &model.WebAuthnCredential{
SignCount: 2,
RPID: "another.example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
AttestationType: "fido-u2f",
},
},
{
name: "ShouldUpdateCloneWarning",
have: &model.WebAuthnCredential{
SignCount: 1,
LastUsedAt: sql.NullTime{Time: time.Unix(0, 0), Valid: true},
AttestationType: "fido-u2f",
RPID: "another.example.com",
},
config: &webauthn.Config{RPID: "https://example.com", RPOrigins: []string{"org.example.com"}},
now: time.Unix(10, 0),
authenticator: webauthn.Authenticator{SignCount: 2, CloneWarning: true},
expected: &model.WebAuthnCredential{
SignCount: 2,
RPID: "another.example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
AttestationType: "fido-u2f",
CloneWarning: true,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tc.have.UpdateSignInInfo(tc.config, tc.now, tc.authenticator)
assert.Equal(t, tc.expected, tc.have)
})
}
}
func TestWebAuthnCredential_ToData(t *testing.T) {
toTimePtr := func(in time.Time) *time.Time {
return &in
}
toStrPtr := func(in string) *string {
return &in
}
testCases := []struct {
name string
have model.WebAuthnCredential
expected model.WebAuthnCredentialData
}{
{
"ShouldParseToData",
model.WebAuthnCredential{
SignCount: 2,
RPID: "org.example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
AttestationType: "fido-u2f",
Transport: "nfc,usb",
},
model.WebAuthnCredentialData{
SignCount: 2,
RPID: "org.example.com",
Transports: []string{"nfc", "usb"},
LastUsedAt: toTimePtr(time.Unix(10, 0)),
AttestationType: "fido-u2f",
},
},
{
"ShouldParseToData",
model.WebAuthnCredential{
KID: model.NewBase64([]byte("abc")),
SignCount: 2,
RPID: "org.example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
AttestationType: "fido-u2f",
Transport: "nfc,usb",
PublicKey: []byte("abc"),
AAGUID: uuid.NullUUID{UUID: uuid.Must(uuid.Parse("b4e159da-a52b-4690-81dd-08972950db5f")), Valid: true},
},
model.WebAuthnCredentialData{
KID: "YWJj",
SignCount: 2,
RPID: "org.example.com",
Transports: []string{"nfc", "usb"},
LastUsedAt: toTimePtr(time.Unix(10, 0)),
AttestationType: "fido-u2f",
PublicKey: "YWJj",
AAGUID: toStrPtr("b4e159da-a52b-4690-81dd-08972950db5f"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := tc.have.ToData()
assert.Equal(t, tc.expected, actual)
actual2, err := actual.ToCredential()
require.NoError(t, err)
assert.Equal(t, tc.have, *actual2)
})
}
}
func TestWebAuthnCredentialData_ToCredential(t *testing.T) {
toTimePtr := func(in time.Time) *time.Time {
return &in
}
toStrPtr := func(in string) *string {
return &in
}
testCases := []struct {
name string
have model.WebAuthnCredentialData
expected *model.WebAuthnCredential
err string
}{
{
"ShouldParseToData",
model.WebAuthnCredentialData{
SignCount: 2,
RPID: "org.example.com",
Transports: []string{"nfc", "usb"},
LastUsedAt: toTimePtr(time.Unix(10, 0)),
AttestationType: "fido-u2f",
},
&model.WebAuthnCredential{
SignCount: 2,
RPID: "org.example.com",
LastUsedAt: sql.NullTime{Time: time.Unix(10, 0), Valid: true},
AttestationType: "fido-u2f",
Transport: "nfc,usb",
},
"",
},
{
"ShouldErrBadAAGUID",
model.WebAuthnCredentialData{
SignCount: 2,
RPID: "org.example.com",
Transports: []string{"nfc", "usb"},
LastUsedAt: toTimePtr(time.Unix(10, 0)),
AttestationType: "fido-u2f",
AAGUID: toStrPtr("not-a-aaguid"),
},
nil,
"error occurred parsing aaguid: invalid UUID length: 12",
},
{
"ShouldErrBadKID",
model.WebAuthnCredentialData{
SignCount: 2,
RPID: "org.example.com",
Transports: []string{"nfc", "usb"},
LastUsedAt: toTimePtr(time.Unix(10, 0)),
AttestationType: "fido-u2f",
KID: "---123===123",
},
nil,
"error occurred deocding kid: illegal base64 data at input byte 0",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual, err := tc.have.ToCredential()
assert.Equal(t, tc.expected, actual)
if len(tc.err) == 0 {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tc.err)
}
})
}
}
func TestNewWebAuthnCredential(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
mock.Ctx.Clock = &mock.Clock
testCases := []struct {
name string
rpid, username, description string
credential *webauthn.Credential
expected model.WebAuthnCredential
}{
{
"ShouldGenerateStandard",
"abc.example.com",
"john",
"example",
&webauthn.Credential{
Authenticator: webauthn.Authenticator{
AAGUID: []byte{180, 225, 89, 218, 165, 43, 70, 144, 129, 221, 8, 151, 41, 80, 219, 95},
},
Transport: []protocol.AuthenticatorTransport{
protocol.NFC,
protocol.USB,
},
},
model.WebAuthnCredential{
Username: "john",
RPID: "abc.example.com",
Description: "example",
Transport: "nfc,usb",
CreatedAt: mock.Clock.Now(),
AAGUID: uuid.NullUUID{UUID: uuid.Must(uuid.Parse("b4e159da-a52b-4690-81dd-08972950db5f")), Valid: true},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := model.NewWebAuthnCredential(mock.Ctx, tc.rpid, tc.username, tc.description, tc.credential)
assert.Equal(t, tc.expected, actual)
})
}
}
func TestWebAuthnCredentialImportExport(t *testing.T) {
have := model.WebAuthnCredentialExport{
WebAuthnCredentials: []model.WebAuthnCredential{
{
ID: 0,
CreatedAt: time.Now(),
LastUsedAt: sql.NullTime{Time: time.Now(), Valid: true},
RPID: "example",
Username: "john",
Description: "akey",
KID: model.NewBase64(MustRead(20)),
PublicKey: MustRead(128),
AttestationType: "fido-u2f",
Transport: "",
AAGUID: model.MustNullUUID(model.NewRandomNullUUID()),
SignCount: 20,
CloneWarning: false,
},
{
ID: 0,
CreatedAt: time.Now(),
LastUsedAt: sql.NullTime{Valid: false},
RPID: "example2",
Username: "john2",
Description: "bkey",
KID: model.NewBase64(MustRead(60)),
PublicKey: MustRead(64),
AttestationType: "packed",
Transport: "",
AAGUID: uuid.NullUUID{Valid: false},
SignCount: 30,
CloneWarning: true,
},
},
}
out, err := yaml.Marshal(&have)
require.NoError(t, err)
imported := model.WebAuthnCredentialExport{}
require.NoError(t, yaml.Unmarshal(out, &imported))
require.Equal(t, len(have.WebAuthnCredentials), len(imported.WebAuthnCredentials))
for i, actual := range imported.WebAuthnCredentials {
t.Run(actual.Description, func(t *testing.T) {
expected := have.WebAuthnCredentials[i]
assert.Equal(t, expected.KID, actual.KID)
assert.Equal(t, expected.PublicKey, actual.PublicKey)
assert.Equal(t, expected.SignCount, actual.SignCount)
assert.Equal(t, expected.AttestationType, actual.AttestationType)
assert.Equal(t, expected.RPID, actual.RPID)
assert.Equal(t, expected.AAGUID.Valid, actual.AAGUID.Valid)
assert.Equal(t, expected.AAGUID.UUID, actual.AAGUID.UUID)
assert.WithinDuration(t, expected.CreatedAt, actual.CreatedAt, time.Second)
assert.WithinDuration(t, expected.LastUsedAt.Time, actual.LastUsedAt.Time, time.Second)
assert.Equal(t, expected.LastUsedAt.Valid, actual.LastUsedAt.Valid)
assert.Equal(t, expected.CloneWarning, actual.CloneWarning)
assert.Equal(t, expected.Description, actual.Description)
assert.Equal(t, expected.Username, actual.Username)
})
}
}
func MustRead(n int) []byte {
data := make([]byte, n)
if _, err := rand.Read(data); err != nil {
panic(err)
}
return data
}