mirror of
https://github.com/teamhanko/hanko.git
synced 2025-10-26 13:27:57 +08:00
320 lines
12 KiB
Go
320 lines
12 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
"github.com/gofrs/uuid"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/teamhanko/hanko/backend/v2/persistence/models"
|
|
"github.com/teamhanko/hanko/backend/v2/test"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWebauthnSuite(t *testing.T) {
|
|
t.Parallel()
|
|
suite.Run(t, new(webauthnSuite))
|
|
}
|
|
|
|
type webauthnSuite struct {
|
|
test.Suite
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_NewHandler() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
manager := getDefaultSessionManager(s.Storage)
|
|
handler, err := NewWebauthnHandler(&test.DefaultConfig, s.Storage, manager, test.NewAuditLogger(), nil)
|
|
s.NoError(err)
|
|
s.NotEmpty(handler)
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_BeginRegistration() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn")
|
|
s.Require().NoError(err)
|
|
|
|
userId := uuid.FromStringOrNil("ec4ef049-5b88-4321-a173-21b0eff06a04")
|
|
|
|
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
|
|
|
|
cookie, err := generateSessionCookie(s.Storage, userId)
|
|
s.Require().NoError(err)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/registration/initialize", nil)
|
|
req.AddCookie(cookie)
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
if s.Equal(http.StatusOK, rec.Code) {
|
|
creationOptions := protocol.CredentialCreation{}
|
|
err = json.Unmarshal(rec.Body.Bytes(), &creationOptions)
|
|
s.NoError(err)
|
|
|
|
uId, err := base64.RawURLEncoding.DecodeString(creationOptions.Response.User.ID.(string))
|
|
s.Require().NoError(err)
|
|
|
|
s.NotEmpty(creationOptions.Response.Challenge)
|
|
s.Equal(uuid.FromStringOrNil(userId.String()).Bytes(), uId)
|
|
s.Equal(test.DefaultConfig.Webauthn.RelyingParty.Id, creationOptions.Response.RelyingParty.ID)
|
|
s.Equal(protocol.ResidentKeyRequirementRequired, creationOptions.Response.AuthenticatorSelection.ResidentKey)
|
|
s.Equal(protocol.VerificationPreferred, creationOptions.Response.AuthenticatorSelection.UserVerification)
|
|
s.True(*creationOptions.Response.AuthenticatorSelection.RequireResidentKey)
|
|
}
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_FinalizeRegistration() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn_registration")
|
|
s.Require().NoError(err)
|
|
|
|
userId := uuid.FromStringOrNil("ec4ef049-5b88-4321-a173-21b0eff06a04")
|
|
|
|
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
|
|
|
|
cookie, err := generateSessionCookie(s.Storage, userId)
|
|
s.Require().NoError(err)
|
|
|
|
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"
|
|
}
|
|
}`
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/registration/finalize", strings.NewReader(body))
|
|
req.AddCookie(cookie)
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
if s.Equal(http.StatusOK, rec.Code) {
|
|
s.Equal(`{"credential_id":"AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH","user_id":"ec4ef049-5b88-4321-a173-21b0eff06a04"}`, strings.TrimSpace(rec.Body.String()))
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodPost, "/webauthn/registration/finalize", strings.NewReader(body))
|
|
req2.AddCookie(cookie)
|
|
rec2 := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec2, req2)
|
|
s.Equal(http.StatusBadRequest, rec2.Code)
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_FinalizeRegistration_SessionDataExpired() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn_registration")
|
|
s.Require().NoError(err)
|
|
|
|
userId := uuid.FromStringOrNil("ec4ef049-5b88-4321-a173-21b0eff06a04")
|
|
|
|
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
|
|
|
|
cookie, err := generateSessionCookie(s.Storage, userId)
|
|
s.Require().NoError(err)
|
|
|
|
body := `{
|
|
"id": "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM",
|
|
"rawId": "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM",
|
|
"type": "public-key",
|
|
"response": {
|
|
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAQECAwQFBgcIAQIDBAUGBwgAIOIlWRhTf45LVyZsJgZmkqtEK-E-tk9I1-z0u13IAFpTpQECAyYgASFYIAeA_nt5TQ8c7bc8hN9_3zqzp3coXO5aplEeHMOQG0hrIlggf_KVxZI_nIedc1XMrwwOMaYNd0qxVpFK7vU79fGBoxY",
|
|
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRmVNYzdzUjlFbGVod0VVNVR0RVdGaTdyUFAzLWtkWlhnbndMdGxiM0NoWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODg4OCIsImNyb3NzT3JpZ2luIjpmYWxzZX0"
|
|
}
|
|
}`
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/registration/finalize", strings.NewReader(body))
|
|
req.AddCookie(cookie)
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
s.Equal(http.StatusBadRequest, rec.Code)
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_BeginAuthentication() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn")
|
|
s.Require().NoError(err)
|
|
|
|
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/login/initialize", nil)
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
if s.Equal(http.StatusOK, rec.Code) {
|
|
assertionOptions := protocol.CredentialAssertion{}
|
|
err := json.Unmarshal(rec.Body.Bytes(), &assertionOptions)
|
|
s.Require().NoError(err)
|
|
s.NotEmpty(assertionOptions.Response.Challenge)
|
|
s.Equal(assertionOptions.Response.UserVerification, protocol.VerificationPreferred)
|
|
s.Equal(test.DefaultConfig.Webauthn.RelyingParty.Id, assertionOptions.Response.RelyingPartyID)
|
|
}
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_FinalizeAuthentication() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn")
|
|
s.Require().NoError(err)
|
|
|
|
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
|
|
|
|
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"
|
|
}
|
|
}`
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
if s.Equal(http.StatusOK, rec.Code) {
|
|
s.Empty(rec.Header().Get("X-Auth-Token"))
|
|
cookies := rec.Result().Cookies()
|
|
if s.NotEmpty(cookies) {
|
|
for _, cookie := range cookies {
|
|
if cookie.Name == "hanko" {
|
|
s.Regexp(".*\\..*\\..*", cookie.Value) // check if cookie contains a jwt
|
|
}
|
|
}
|
|
}
|
|
s.Equal(`{"credential_id":"AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH","user_id":"ec4ef049-5b88-4321-a173-21b0eff06a04"}`, strings.TrimSpace(rec.Body.String()))
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
|
|
rec2 := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec2, req2)
|
|
|
|
if s.Equal(http.StatusUnauthorized, rec2.Code) {
|
|
httpError := echo.HTTPError{}
|
|
err = json.Unmarshal(rec2.Body.Bytes(), &httpError)
|
|
s.NoError(err)
|
|
s.Equal("Stored challenge and received challenge do not match", httpError.Message)
|
|
}
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_FinalizeAuthentication_SessionDataExpired() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn")
|
|
s.Require().NoError(err)
|
|
|
|
e := NewPublicRouter(&test.DefaultConfig, s.Storage, nil, nil)
|
|
|
|
body := `{
|
|
"id": "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM",
|
|
"rawId": "4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM",
|
|
"type": "public-key",
|
|
"response": {
|
|
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABA",
|
|
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiQVVnTmtWOG5tVnd2LXJsOC1hSzFaRVg1RmxlbllDc2FUWTh2ZEVjSktKUSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODg4OCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9",
|
|
"signature": "MEUCIQD46qneg2W9izFrJ-houyPia_QIcFR_9ZZIoWAqMywRmgIgMed7NAgcXkgCSWtVNknb_D70sn8b-fGwQQBlBCgIJ-c",
|
|
"userHandle": "RmJoNvLbTsCHUoWLVEy8eA"
|
|
}
|
|
}`
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
s.Equal(http.StatusUnauthorized, rec.Code)
|
|
}
|
|
|
|
func (s *webauthnSuite) TestWebauthnHandler_FinalizeAuthentication_TokenInHeader() {
|
|
if testing.Short() {
|
|
s.T().Skip("skipping test in short mode")
|
|
}
|
|
|
|
err := s.LoadFixtures("../test/fixtures/webauthn")
|
|
s.Require().NoError(err)
|
|
|
|
cfg := test.DefaultConfig
|
|
cfg.Session.EnableAuthTokenHeader = true
|
|
e := NewPublicRouter(&cfg, s.Storage, nil, nil)
|
|
|
|
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"
|
|
}
|
|
}`
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec, req)
|
|
|
|
if s.Equal(http.StatusOK, rec.Code) {
|
|
s.Empty(rec.Result().Cookies())
|
|
token := rec.Header().Get("X-Auth-Token")
|
|
s.NotEmpty(token)
|
|
s.Regexp(".*\\..*\\..*", token)
|
|
s.Equal(`{"credential_id":"AaFdkcD4SuPjF-jwUoRwH8-ZHuY5RW46fsZmEvBX6RNKHaGtVzpATs06KQVheIOjYz-YneG4cmQOedzl0e0jF951ukx17Hl9jeGgWz5_DKZCO12p2-2LlzjH","user_id":"ec4ef049-5b88-4321-a173-21b0eff06a04"}`, strings.TrimSpace(rec.Body.String()))
|
|
}
|
|
|
|
req2 := httptest.NewRequest(http.MethodPost, "/webauthn/login/finalize", strings.NewReader(body))
|
|
rec2 := httptest.NewRecorder()
|
|
|
|
e.ServeHTTP(rec2, req2)
|
|
|
|
if s.Equal(http.StatusUnauthorized, rec2.Code) {
|
|
httpError := echo.HTTPError{}
|
|
err = json.Unmarshal(rec2.Body.Bytes(), &httpError)
|
|
s.NoError(err)
|
|
s.Equal("Stored challenge and received challenge do not match", httpError.Message)
|
|
}
|
|
}
|
|
|
|
var userId = "ec4ef049-5b88-4321-a173-21b0eff06a04"
|
|
|
|
var uId, _ = uuid.FromString(userId)
|
|
|
|
var emails = []models.Email{
|
|
{
|
|
ID: uId,
|
|
Address: "john.doe@example.com",
|
|
PrimaryEmail: &models.PrimaryEmail{ID: uId},
|
|
},
|
|
}
|