Files
hanko/backend/handler/webauthn_test.go

274 lines
10 KiB
Go

package handler
import (
"encoding/json"
"github.com/go-webauthn/webauthn/protocol"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/test"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
var userId = "ec4ef049-5b88-4321-a173-21b0eff06a04"
var userIdBytes = []byte{0xec, 0x4e, 0xf0, 0x49, 0x5b, 0x88, 0x43, 0x21, 0xa1, 0x73, 0x21, 0xb0, 0xef, 0xf0, 0x6a, 0x4}
func TestNewWebauthnHandler(t *testing.T) {
p := test.NewPersister(nil, nil, nil, nil, nil, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogger())
assert.NoError(t, err)
assert.NotEmpty(t, handler)
}
func TestWebauthnHandler_BeginRegistration(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/webauthn/registration/initialize", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
token := jwt.New()
err := token.Set(jwt.SubjectKey, userId)
require.NoError(t, err)
c.Set("session", token)
p := test.NewPersister(users, nil, nil, credentials, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogger())
require.NoError(t, err)
if assert.NoError(t, handler.BeginRegistration(c)) {
creationOptions := protocol.CredentialCreation{}
err = json.Unmarshal(rec.Body.Bytes(), &creationOptions)
assert.NoError(t, err)
assert.NotEmpty(t, creationOptions.Response.Challenge)
assert.Equal(t, userIdBytes, creationOptions.Response.User.ID)
assert.Equal(t, defaultConfig.Webauthn.RelyingParty.Id, creationOptions.Response.RelyingParty.ID)
assert.Equal(t, creationOptions.Response.AuthenticatorSelection.ResidentKey, protocol.ResidentKeyRequirementRequired)
assert.Equal(t, creationOptions.Response.AuthenticatorSelection.UserVerification, protocol.VerificationRequired)
assert.True(t, *creationOptions.Response.AuthenticatorSelection.RequireResidentKey)
}
}
func TestWebauthnHandler_FinishRegistration(t *testing.T) {
body := `{
"id": "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH",
"rawId": "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH",
"type": "public-key",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjeSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFYmehnq3OAAI1vMYKZIsLJfHwVQMAWgGhXZHA-Erj4xfo8FKEcB_PmR7mOUVuOn7GZhLwV-kTSh2hrVc6QE7NOikFYXiDo2M_mJ3huHJkDnnc5dHtIxfedbpMdex5fY3hoFs-fwymQjtdqdvti5c4x6UBAgMmIAEhWCDxvVrRgK4vpnr6JxTx-KfpSNyQUtvc47ryryZmj-P5kSJYIDox8N9bHQBrxN-b5kXqfmj3GwAJW7nNCh8UPbus3B6I",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidE9yTkRDRDJ4UWY0ekZqRWp3eGFQOGZPRXJQM3p6MDhyTW9UbEpHdG5LVSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"
}
}`
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/webauthn/registration/finalize", strings.NewReader(body))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
token := jwt.New()
err := token.Set(jwt.SubjectKey, userId)
require.NoError(t, err)
c.Set("session", token)
p := test.NewPersister(users, nil, nil, nil, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogger())
require.NoError(t, err)
if assert.NoError(t, handler.FinishRegistration(c)) {
assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
assert.Regexp(t, `{"credential_id":".*"}`, rec.Body.String())
}
req2 := httptest.NewRequest(http.MethodPost, "/webauthn/registration/finalize", strings.NewReader(body))
rec2 := httptest.NewRecorder()
c2 := e.NewContext(req2, rec2)
token2 := jwt.New()
err = token.Set(jwt.SubjectKey, userId)
require.NoError(t, err)
c2.Set("session", token2)
err = handler.FinishRegistration(c2)
if assert.Error(t, err) {
httpError := dto.ToHttpError(err)
assert.Equal(t, http.StatusBadRequest, httpError.Code)
assert.Equal(t, "Stored challenge and received challenge do not match: sessionData not found", err.Error())
}
}
func TestWebauthnHandler_BeginAuthentication(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/webauthn/login/initialize", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, nil, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogger())
require.NoError(t, err)
if assert.NoError(t, handler.BeginAuthentication(c)) {
assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
assertionOptions := protocol.CredentialAssertion{}
err = json.Unmarshal(rec.Body.Bytes(), &assertionOptions)
assert.NoError(t, err)
assert.NotEmpty(t, assertionOptions.Response.Challenge)
assert.Equal(t, assertionOptions.Response.UserVerification, protocol.VerificationRequired)
assert.Equal(t, defaultConfig.Webauthn.RelyingParty.Id, assertionOptions.Response.RelyingPartyID)
}
}
func TestWebauthnHandler_FinishAuthentication(t *testing.T) {
body := `{
"id": "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH",
"rawId": "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH",
"type": "public-key",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFYmezOw",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZ0tKS21oOTB2T3BZTzU1b0hwcWFIWF9vTUNxNG9UWnQtRDBiNnRlSXpyRSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEYCIQDi2vYVspG6pf38I4GyQCPOojGbvX4nwSPXCi0hm80twAIhAO3EWjhAnj0UpjU_l0AH5sEh3zq4LDvkvo3AUqaqfGYD",
"userHandle": "7E7wSVuIQyGhcyGw7_BqBA"
}
}`
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
p := test.NewPersister(users, nil, nil, credentials, sessionData, nil, nil)
handler, err := NewWebauthnHandler(&defaultConfig, p, sessionManager{}, test.NewAuditLogger())
require.NoError(t, err)
if assert.NoError(t, handler.FinishAuthentication(c)) {
assert.Equal(t, http.StatusOK, rec.Result().StatusCode)
cookies := rec.Result().Cookies()
if assert.NotEmpty(t, cookies) {
for _, cookie := range cookies {
if cookie.Name == "hanko" {
assert.Equal(t, userId, cookie.Value)
}
}
}
}
req2 := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
rec2 := httptest.NewRecorder()
c2 := e.NewContext(req2, rec2)
err = handler.FinishAuthentication(c2)
if assert.Error(t, err) {
httpError := dto.ToHttpError(err)
assert.Equal(t, http.StatusUnauthorized, httpError.Code)
assert.Equal(t, "Stored challenge and received challenge do not match: sessionData not found", err.Error())
}
}
var defaultConfig = config.Config{
Webauthn: config.WebauthnSettings{
RelyingParty: config.RelyingParty{
Id: "localhost",
DisplayName: "Test Relying Party",
Icon: "",
Origin: "http://localhost:8080",
},
Timeout: 60000,
},
}
type sessionManager struct {
}
func (s sessionManager) GenerateJWT(uuid uuid.UUID) (string, error) {
return userId, nil
}
func (s sessionManager) GenerateCookie(token string) (*http.Cookie, error) {
return &http.Cookie{
Name: "hanko",
Value: token,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}, nil
}
func (s sessionManager) Verify(token string) (jwt.Token, error) {
return nil, nil
}
var credentials = []models.WebauthnCredential{
func() models.WebauthnCredential {
uId, _ := uuid.FromString(userId)
aaguid, _ := uuid.FromString("adce0002-35bc-c60a-648b-0b25f1f05503")
return models.WebauthnCredential{
ID: "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH",
UserId: uId,
PublicKey: "pQECAyYgASFYIPG9WtGAri-mevonFPH4p-lI3JBS29zjuvKvJmaP4_mRIlggOjHw31sdAGvE35vmRep-aPcbAAlbuc0KHxQ9u6zcHog",
AttestationType: "none",
AAGUID: aaguid,
SignCount: 1650958750,
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
}
}(),
func() models.WebauthnCredential {
uId, _ := uuid.FromString(userId)
aaguid, _ := uuid.FromString("adce0002-35bc-c60a-648b-0b25f1f05503")
return models.WebauthnCredential{
ID: "AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjK",
UserId: uId,
PublicKey: "pQECAyYgASFYIPG9WtGAri-mevonFPH4p-lI3JBS29zjuvKvJmaP4_mRIlggOjHw31sdAGvE35vmRep-aPcbAAlbuc0KHxQ9u6zcHoj",
AttestationType: "none",
AAGUID: aaguid,
SignCount: 1650958750,
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
}
}(),
}
var sessionData = []models.WebauthnSessionData{
func() models.WebauthnSessionData {
id, _ := uuid.NewV4()
uId, _ := uuid.FromString(userId)
return models.WebauthnSessionData{
ID: id,
Challenge: "tOrNDCD2xQf4zFjEjwxaP8fOErP3zz08rMoTlJGtnKU",
UserId: uId,
UserVerification: string(protocol.VerificationRequired),
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
Operation: models.WebauthnOperationRegistration,
AllowedCredentials: nil,
}
}(),
func() models.WebauthnSessionData {
id, _ := uuid.NewV4()
return models.WebauthnSessionData{
ID: id,
Challenge: "gKJKmh90vOpYO55oHpqaHX_oMCq4oTZt-D0b6teIzrE",
UserId: uuid.UUID{},
UserVerification: string(protocol.VerificationRequired),
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
Operation: models.WebauthnOperationAuthentication,
AllowedCredentials: nil,
}
}(),
}
var users = []models.User{
func() models.User {
uId, _ := uuid.FromString(userId)
return models.User{
ID: uId,
Email: "john.doe@example.com",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}(),
}