mirror of https://github.com/authelia/authelia.git
421 lines
12 KiB
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
|
|
}
|