Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Webauthn creation tests #1155

Merged
merged 2 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package actions

import (
"fmt"
"github.com/go-webauthn/webauthn/protocol"
webauthnLib "github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/flow_api_basic_construct/common"
"github.com/teamhanko/hanko/backend/flowpilot"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/test"
"net/http"
"testing"
"time"
)

func TestGetWaCreationOptions(t *testing.T) {
s := new(getWaCreationOptions)

suite.Run(t, s)
}

type getWaCreationOptions struct {
test.Suite
}

func (s *getWaCreationOptions) TestGetWaCreationOptions_Execute() {
tests := []struct {
name string
input string
flowId string
cfg config.Config
expectedState flowpilot.StateName
statusCode int
}{
{
name: "get webauthn creation options with username and email",
input: "",
flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431",
cfg: config.Config{},
expectedState: common.StateOnboardingVerifyPasskeyAttestation,
statusCode: http.StatusOK,
},
{
name: "get webauthn creation options with only email",
input: "",
flowId: "a77e23b2-7ca5-4c76-a20b-c17b7dbcb117",
cfg: config.Config{},
expectedState: common.StateOnboardingVerifyPasskeyAttestation,
statusCode: http.StatusOK,
},
{
name: "get webauthn creation options with only username",
input: "",
flowId: "de87cfc6-a6e2-434d-bbe8-5e5004c9deda",
cfg: config.Config{},
expectedState: common.StateOnboardingVerifyPasskeyAttestation,
statusCode: http.StatusOK,
},
}

for _, currentTest := range tests {
s.Run(currentTest.name, func() {
s.SetupTest()
defer s.TearDownTest()

err := s.LoadFixtures("../../test/fixtures/actions/get_wa_creation_options")
s.Require().NoError(err)

wa, err := s.getWebauthnLib()
s.Require().NoError(err)

passkeySubFlow, err := flowpilot.NewSubFlow().
State(common.StateOnboardingCreatePasskey, NewGetWACreationOptions(currentTest.cfg, s.Storage, wa)).
State(common.StateOnboardingVerifyPasskeyAttestation).
Build()
s.Require().NoError(err)

flow, err := flowpilot.NewFlow("/registration_test").
State(common.StateRegistrationPreflight).
State(common.StateSuccess).
SubFlows(passkeySubFlow).
InitialState(common.StateRegistrationPreflight).
ErrorState(common.StateError).
Build()
s.Require().NoError(err)

tx := s.Storage.GetConnection()
db := models.NewFlowDB(tx)
actionParam := fmt.Sprintf("get_wa_creation_options@%s", currentTest.flowId)
inputData := flowpilot.InputData{JSONString: currentTest.input}
result, err := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(inputData))
s.Require().NoError(err)

s.Equal(currentTest.statusCode, result.Status())
s.Equal(currentTest.expectedState, result.Response().StateName)

// TODO: check that the schema of the action returns the correct error_code e.g.
// result.Response().PublicActions[0].PublicSchema[0].PublicError.Code == ErrorValueInvalid
})
}
}

func (s *getWaCreationOptions) getWebauthnLib() (*webauthnLib.WebAuthn, error) {
f := false
return webauthnLib.New(&webauthnLib.Config{
RPID: "localhost",
RPDisplayName: "Test RP",
RPOrigins: []string{"http://localhost"},
AttestationPreference: protocol.PreferNoAttestation,
AuthenticatorSelection: protocol.AuthenticatorSelection{
RequireResidentKey: &f,
ResidentKey: protocol.ResidentKeyRequirementDiscouraged,
UserVerification: protocol.VerificationRequired,
},
Debug: false,
Timeouts: webauthnLib.TimeoutsConfig{
Login: webauthnLib.TimeoutConfig{
Enforce: true,
Timeout: 60000 * time.Millisecond,
},
Registration: webauthnLib.TimeoutConfig{
Enforce: true,
Timeout: 60000 * time.Millisecond,
},
},
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,23 @@ import (
"github.com/go-webauthn/webauthn/protocol"
webauthnLib "github.com/go-webauthn/webauthn/webauthn"
"github.com/gofrs/uuid"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/dto/intern"
"github.com/teamhanko/hanko/backend/flow_api_basic_construct/common"
"github.com/teamhanko/hanko/backend/flowpilot"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/session"
"strings"
)

func NewSendWAAttestationResponse(cfg config.Config, persister persistence.Persister, wa *webauthnLib.WebAuthn, sessionManager session.Manager, httpContext echo.Context) SendWAAttestationResponse {
func NewSendWAAttestationResponse(persister persistence.Persister, wa *webauthnLib.WebAuthn) SendWAAttestationResponse {
return SendWAAttestationResponse{
cfg,
persister,
wa,
sessionManager,
httpContext,
}
}

type SendWAAttestationResponse struct {
cfg config.Config
persister persistence.Persister
wa *webauthnLib.WebAuthn
sessionManager session.Manager
httpContext echo.Context
persister persistence.Persister
wa *webauthnLib.WebAuthn
}

func (m SendWAAttestationResponse) GetName() flowpilot.ActionName {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package actions

import (
"fmt"
"github.com/go-webauthn/webauthn/protocol"
webauthnLib "github.com/go-webauthn/webauthn/webauthn"
"github.com/stretchr/testify/suite"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/flow_api_basic_construct/common"
"github.com/teamhanko/hanko/backend/flowpilot"
"github.com/teamhanko/hanko/backend/persistence/models"
"github.com/teamhanko/hanko/backend/test"
"net/http"
"testing"
"time"
)

func TestSendWaAttestationResponse(t *testing.T) {
s := new(sendWaAttestationResponse)

suite.Run(t, s)
}

type sendWaAttestationResponse struct {
test.Suite
}

func (s *sendWaAttestationResponse) TestSendWaAttestationResponse_Execute() {
tests := []struct {
name string
input string
flowId string
expectedState flowpilot.StateName
statusCode int
}{
{
name: "send a correct attestation",
input: `{"public_key": "{\"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\"}}"}`,
flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431",
expectedState: common.StateSuccess,
statusCode: http.StatusOK,
},
{
name: "send a attestation with expired session data",
input: `{"public_key": "{\"id\": \"4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM\",\"rawId\": \"4iVZGFN_jktXJmwmBmaSq0Qr4T62T0jX7PS7XcgAWlM\",\"type\": \"public-key\",\"response\": {\"attestationObject\": \"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAQECAwQFBgcIAQIDBAUGBwgAIOIlWRhTf45LVyZsJgZmkqtEK-E-tk9I1-z0u13IAFpTpQECAyYgASFYIAeA_nt5TQ8c7bc8hN9_3zqzp3coXO5aplEeHMOQG0hrIlggf_KVxZI_nIedc1XMrwwOMaYNd0qxVpFK7vU79fGBoxY\",\"clientDataJSON\": \"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRmVNYzdzUjlFbGVod0VVNVR0RVdGaTdyUFAzLWtkWlhnbndMdGxiM0NoWSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODg4OCIsImNyb3NzT3JpZ2luIjpmYWxzZX0\"}}"}`,
flowId: "53d35f35-c87d-4533-b966-2b48686b9be9",
expectedState: common.StateOnboardingVerifyPasskeyAttestation,
statusCode: http.StatusBadRequest,
},
{
name: "send a attestation with wrong challenge",
input: `{"publicKey":"{\"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\": \"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidE9yTkRDRDJ4UWY0ekZqRWp3eGFQOGZPRXJQM3p6MDhyTW9UbEpHdG5LdSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCIsImNyb3NzT3JpZ2luIjpmYWxzZX0\"}}"}`,
flowId: "0b41f4dd-8e46-4a7c-bb4d-d60843113431",
expectedState: common.StateOnboardingVerifyPasskeyAttestation,
statusCode: http.StatusBadRequest,
},
}

for _, currentTest := range tests {
s.Run(currentTest.name, func() {
s.SetupTest()
defer s.TearDownTest()

err := s.LoadFixtures("../../test/fixtures/actions/send_wa_attestation_response")
s.Require().NoError(err)

wa, err := s.getWebauthnLib()
s.Require().NoError(err)

passkeySubFlow, err := flowpilot.NewSubFlow().
State(common.StateOnboardingCreatePasskey).
State(common.StateOnboardingVerifyPasskeyAttestation, NewSendWAAttestationResponse(s.Storage, wa)).
Build()
s.Require().NoError(err)

flow, err := flowpilot.NewFlow("/registration_test").
State(common.StateRegistrationPreflight).
State(common.StateSuccess).
SubFlows(passkeySubFlow).
InitialState(common.StateRegistrationPreflight).
ErrorState(common.StateError).
Debug(true).
Build()
s.Require().NoError(err)

tx := s.Storage.GetConnection()
db := models.NewFlowDB(tx)
actionParam := fmt.Sprintf("send_wa_attestation_response@%s", currentTest.flowId)
inputData := flowpilot.InputData{JSONString: currentTest.input}
result, err := flow.Execute(db, flowpilot.WithActionParam(actionParam), flowpilot.WithInputData(inputData))
s.Require().NoError(err)

s.Equal(currentTest.statusCode, result.Status())
s.Equal(currentTest.expectedState, result.Response().StateName)

// TODO: check that the schema of the action returns the correct error_code e.g.
// result.Response().PublicActions[0].PublicSchema[0].PublicError.Code == ErrorValueInvalid
})
}
}

func (s *sendWaAttestationResponse) getWebauthnLib() (*webauthnLib.WebAuthn, error) {
f := false
return webauthnLib.New(&webauthnLib.Config{
RPID: "localhost",
RPDisplayName: "Test RP",
RPOrigins: []string{"http://localhost:8080"},
AttestationPreference: protocol.PreferNoAttestation,
AuthenticatorSelection: protocol.AuthenticatorSelection{
RequireResidentKey: &f,
ResidentKey: protocol.ResidentKeyRequirementDiscouraged,
UserVerification: protocol.VerificationRequired,
},
Debug: false,
Timeouts: webauthnLib.TimeoutsConfig{
Login: webauthnLib.TimeoutConfig{
Enforce: true,
Timeout: 60000 * time.Millisecond,
},
Registration: webauthnLib.TimeoutConfig{
Enforce: true,
Timeout: 60000 * time.Millisecond,
},
},
})
}

func (s *sendWaAttestationResponse) GetConfig() config.Config {
return config.Config{
Webauthn: config.WebauthnSettings{
RelyingParty: config.RelyingParty{
Id: "localhost",
DisplayName: "Test Relying Party",
Icon: "",
Origins: []string{"http://localhost:8080"},
},
Timeout: 60000,
},
}
}
2 changes: 1 addition & 1 deletion backend/flow_api_basic_construct/common/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const (
ActionGetWARequestOptions flowpilot.ActionName = "get_wa_request_options"
ActionSendWAAssertionResponse flowpilot.ActionName = "send_wa_request_response"
ActionGetWACreationOptions flowpilot.ActionName = "get_wa_creation_options"
ActionSendWAAttestationResponse flowpilot.ActionName = "send_wa_attestation_options"
ActionSendWAAttestationResponse flowpilot.ActionName = "send_wa_attestation_response"
ActionSubmitPassword flowpilot.ActionName = "submit_password"
ActionSubmitNewPassword flowpilot.ActionName = "submit_new_password"
ActionSubmitTOTPCode flowpilot.ActionName = "submit_totp_code"
Expand Down
6 changes: 2 additions & 4 deletions backend/flow_api_basic_construct/flows/passkey_onboarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package flows
import (
"github.com/go-webauthn/webauthn/protocol"
webauthnLib "github.com/go-webauthn/webauthn/webauthn"
"github.com/labstack/echo/v4"
"github.com/teamhanko/hanko/backend/config"
"github.com/teamhanko/hanko/backend/flow_api_basic_construct/actions"
"github.com/teamhanko/hanko/backend/flow_api_basic_construct/common"
"github.com/teamhanko/hanko/backend/flowpilot"
"github.com/teamhanko/hanko/backend/persistence"
"github.com/teamhanko/hanko/backend/session"
"time"
)

func NewPasskeyOnboardingSubFlow(cfg config.Config, persister persistence.Persister, sessionManager session.Manager, httpContext echo.Context) (flowpilot.SubFlow, error) {
func NewPasskeyOnboardingSubFlow(cfg config.Config, persister persistence.Persister) (flowpilot.SubFlow, error) {
// TODO:
f := false
wa, err := webauthnLib.New(&webauthnLib.Config{
Expand Down Expand Up @@ -43,6 +41,6 @@ func NewPasskeyOnboardingSubFlow(cfg config.Config, persister persistence.Persis
}
return flowpilot.NewSubFlow().
State(common.StateOnboardingCreatePasskey, actions.NewGetWACreationOptions(cfg, persister, wa), actions.NewSkip(cfg)).
State(common.StateOnboardingVerifyPasskeyAttestation, actions.NewSendWAAttestationResponse(cfg, persister, wa, sessionManager, httpContext)).
State(common.StateOnboardingVerifyPasskeyAttestation, actions.NewSendWAAttestationResponse(persister, wa)).
Build()
}
2 changes: 1 addition & 1 deletion backend/flow_api_basic_construct/flows/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
)

func NewRegistrationFlow(cfg config.Config, persister persistence.Persister, passcodeService services.Passcode, sessionManager session.Manager, httpContext echo.Context) (flowpilot.Flow, error) {
passkeyOnboardingSubFlow, err := NewPasskeyOnboardingSubFlow(cfg, persister, sessionManager, httpContext)
passkeyOnboardingSubFlow, err := NewPasskeyOnboardingSubFlow(cfg, persister)
if err != nil {
return nil, err
}
Expand Down
21 changes: 21 additions & 0 deletions backend/test/fixtures/actions/get_wa_creation_options/flows.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
- id: 0b41f4dd-8e46-4a7c-bb4d-d60843113431
current_state: onboarding_create_passkey
stash_data: "{\"email\":\"example@example.com\",\"username\":\"john.doe\"}"
version: 0
expires_at: 2099-12-31 23:59:59
created_at: 2023-01-01 00:00:00
updated_at: 2023-01-01 00:00:00
- id: de87cfc6-a6e2-434d-bbe8-5e5004c9deda
current_state: onboarding_create_passkey
stash_data: "{\"username\":\"john.doe\"}"
version: 0
expires_at: 2099-12-31 23:59:59
created_at: 2023-01-01 00:00:00
updated_at: 2023-01-01 00:00:00
- id: a77e23b2-7ca5-4c76-a20b-c17b7dbcb117
current_state: onboarding_create_passkey
stash_data: "{\"email\":\"example@example.com\"}"
version: 0
expires_at: 2099-12-31 23:59:59
created_at: 2023-01-01 00:00:00
updated_at: 2023-01-01 00:00:00
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- id: 0b41f4dd-8e46-4a7c-bb4d-d60843113431
current_state: onboarding_verify_passkey_attestation
stash_data: "{\"webauthn_session_data_id\":\"adce0002-35bc-c60a-648b-0b25f1f05503\",\"user_id\":\"ec4ef049-5b88-4321-a173-21b0eff06a04\",\"email\":\"john.doe@example.com\",\"_\":{\"scheduled_states\":[\"success\"]}}"
version: 0
expires_at: 2099-12-31 23:59:59
created_at: 2023-01-01 00:00:00
updated_at: 2023-01-01 00:00:00
- id: 53d35f35-c87d-4533-b966-2b48686b9be9
current_state: onboarding_verify_passkey_attestation
stash_data: "{\"webauthn_session_data_id\":\"65f13ce2-d118-44f0-a38b-8e3ee918c6f3\",\"user_id\":\"ec4ef049-5b88-4321-a173-21b0eff06a04\",\"email\":\"john.doe@example.com\",\"_\":{\"scheduled_states\":[\"success\"]}}"
version: 0
expires_at: 2099-12-31 23:59:59
created_at: 2023-01-01 00:00:00
updated_at: 2023-01-01 00:00:00
Loading