Skip to content

Commit

Permalink
test: add totp settings tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr committed Oct 19, 2021
1 parent 5eb090b commit c5a0d0f
Show file tree
Hide file tree
Showing 9 changed files with 535 additions and 30 deletions.
4 changes: 2 additions & 2 deletions selfservice/strategy/totp/.schema/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"method": {
"type": "string"
},
"verification_totp": {
"totp_code": {
"type": "string",
"maxLength": 6,
"minLength": 6
},
"unlink_totp": {
"totp_unlink": {
"type": "boolean"
}
}
Expand Down
84 changes: 84 additions & 0 deletions selfservice/strategy/totp/fixtures/settings/totp_setup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"required": true,
"type": "hidden",
"value": "NzVxbWl1M3M4ZGFibWdwZmhkMTk4MDk2bm9mbDRjdDc="
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"id": "totp_secret_key",
"text": {
"context": {
"secret": "L5BRXD2H4Z3SVGXPDMD5MN2BVZIKBW5U"
},
"id": 1050006,
"text": "Your authenticator app secret: L5BRXD2H4Z3SVGXPDMD5MN2BVZIKBW5U",
"type": "info"
}
},
"group": "totp",
"messages": [],
"meta": {},
"type": "text"
},
{
"attributes": {
"id": "totp_qr",
"src": "data:image/png;base64,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"
},
"group": "totp",
"messages": [],
"meta": {
"label": {
"id": 1050005,
"text": "Authenticator app QR code",
"type": "info"
}
},
"type": "img"
},
{
"attributes": {
"disabled": false,
"name": "totp_code",
"required": true,
"type": "text"
},
"group": "totp",
"messages": [],
"meta": {
"label": {
"id": 1070006,
"text": "Verify code",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"type": "submit",
"value": "totp"
},
"group": "totp",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
}
]
52 changes: 52 additions & 0 deletions selfservice/strategy/totp/fixtures/settings/totp_unlink.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"required": true,
"type": "hidden",
"value": "NzVxbWl1M3M4ZGFibWdwZmhkMTk4MDk2bm9mbDRjdDc="
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "totp_unlink",
"required": true,
"type": "submit",
"value": "true"
},
"group": "totp",
"messages": [],
"meta": {
"label": {
"id": 1050004,
"text": "Unlink TOTP Authenticator App",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"type": "submit",
"value": "totp"
},
"group": "totp",
"messages": [],
"meta": {
"label": {
"id": 1070003,
"text": "Save",
"type": "info"
}
},
"type": "input"
}
]
2 changes: 1 addition & 1 deletion selfservice/strategy/totp/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,
}

if !totp.Validate(p.TOTPCode, key.Secret()) {
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewInvalidTOTPCode()))
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewTOTPVerifierWrongError("#/")))
}

f.Active = s.ID()
Expand Down
63 changes: 51 additions & 12 deletions selfservice/strategy/totp/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ import (
_ "embed"
)

const totpCodeGJSONQuery = "ui.nodes.#(attributes.name==totp_code)"

func createIdentityWithoutTOTP(t *testing.T, reg driver.Registry) *identity.Identity {
id, _ := createIdentity(t, reg)
delete(id.Credentials, identity.CredentialsTypeTOTP)
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
return id
}

func createIdentity(t *testing.T, reg driver.Registry) (*identity.Identity, *otp.Key) {
identifier := x.NewUUID().String() + "@ory.sh"
password := x.NewUUID().String()
Expand Down Expand Up @@ -70,8 +79,8 @@ func createIdentity(t *testing.T, reg driver.Registry) (*identity.Identity, *otp
return i, key
}

//go:embed fixtures/with_totp.json
var fixtureWithTOTP []byte
//go:embed fixtures/login/with_totp.json
var loginFixtureWithTOTP []byte

func TestCompleteLogin(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)
Expand All @@ -97,13 +106,11 @@ func TestCompleteLogin(t *testing.T) {

apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSONExcept(t, json.RawMessage(fixtureWithTOTP), f.Ui.Nodes, []string{"2.attributes.value"})
assertx.EqualAsJSONExcept(t, json.RawMessage(loginFixtureWithTOTP), f.Ui.Nodes, []string{"2.attributes.value"})
})

t.Run("case=totp payload is set when identity has no totp", func(t *testing.T) {
id, _ := createIdentity(t, reg)
id.Credentials = nil
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
id := createIdentityWithoutTOTP(t, reg)

apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
Expand Down Expand Up @@ -180,7 +187,7 @@ func TestCompleteLogin(t *testing.T) {
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, "length must be >= 6, but got 0", gjson.Get(body, "ui.nodes.#(attributes.name==totp_code).messages.0.text").String(), "%s", body)
assert.Equal(t, "length must be >= 6, but got 0", gjson.Get(body, totpCodeGJSONQuery+".messages.0.text").String(), "%s", body)
}

t.Run("type=api", func(t *testing.T) {
Expand Down Expand Up @@ -208,7 +215,7 @@ func TestCompleteLogin(t *testing.T) {
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationInvalidTOTPCode().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}

t.Run("type=api", func(t *testing.T) {
Expand Down Expand Up @@ -236,7 +243,7 @@ func TestCompleteLogin(t *testing.T) {
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response) {
checkURL(t, shouldRedirect, res)
assert.NotEmpty(t, gjson.Get(body, "id").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationInvalidTOTPCode().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}

t.Run("type=api", func(t *testing.T) {
Expand All @@ -256,9 +263,7 @@ func TestCompleteLogin(t *testing.T) {
})

t.Run("case=should fail if TOTP was not set up for identity", func(t *testing.T) {
id, _ := createIdentity(t, reg)
id.Credentials = nil
require.NoError(t, reg.PrivilegedIdentityPool().UpdateIdentity(context.Background(), id))
id := createIdentityWithoutTOTP(t, reg)

payload := func(v url.Values) {
v.Set("totp_code", "111111")
Expand Down Expand Up @@ -350,4 +355,38 @@ func TestCompleteLogin(t *testing.T) {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
assert.Equal(t, text.NewErrorValidationLoginNoStrategyFound().Text, gjson.GetBytes(body, "ui.messages.0.text").String())
})

t.Run("case=should pass without csrf if API flow", func(t *testing.T) {
id, _ := createIdentity(t, reg)
body, res := doAPIFlow(t, func(v url.Values) {
v.Del("csrf_token")
v.Set("totp_code", "111111")
}, id)

assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
assert.Equal(t, text.NewErrorValidationTOTPVerifierWrong().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
})

t.Run("case=should fail if CSRF token is invalid", func(t *testing.T) {
id, _ := createIdentity(t, reg)
t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, func(v url.Values) {
v.Del("csrf_token")
v.Set("totp_code", "111111")
}, id)

assert.Contains(t, res.Request.URL.String(), errTS.URL)
assert.Equal(t, x.ErrInvalidCSRFToken.Reason(), gjson.Get(body, "reason").String(), body)
})

t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, func(v url.Values) {
v.Del("csrf_token")
v.Set("totp_code", "111111")
}, id)

assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
assert.Equal(t, x.ErrInvalidCSRFToken.Reason(), gjson.Get(body, "error.reason").String(), body)
})
})
}
8 changes: 4 additions & 4 deletions selfservice/strategy/totp/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func NewVerifyTOTPNode() *node.Node {
return node.NewInputField("verification_totp", nil, node.TOTPGroup,
return node.NewInputField(node.TOTPCode, nil, node.TOTPGroup,
node.InputAttributeTypeText,
node.WithRequiredInputAttribute).
WithMetaLabel(text.NewInfoNodeLabelVerifyOTP())
Expand All @@ -20,16 +20,16 @@ func NewTOTPImageQRNode(key *otp.Key) (*node.Node, error) {
return nil, err
}

return node.NewImageField("totp_key_qr", src, node.TOTPGroup).
return node.NewImageField(node.TOTPQR, src, node.TOTPGroup).
WithMetaLabel(text.NewInfoSelfServiceSettingsTOTPQRCode()), nil
}

func NewTOTPSourceURLNode(key *otp.Key) *node.Node {
return node.NewTextField("totp_key_secret", text.NewInfoSelfServiceSettingsTOTPSecret(key.Secret()), node.TOTPGroup)
return node.NewTextField(node.TOTPSecretKey, text.NewInfoSelfServiceSettingsTOTPSecret(key.Secret()), node.TOTPGroup)
}

func NewUnlinkTOTPNode() *node.Node {
return node.NewInputField("unlink_totp", "true", node.TOTPGroup,
return node.NewInputField(node.TOTPUnlink, "true", node.TOTPGroup,
node.InputAttributeTypeSubmit,
node.WithRequiredInputAttribute).
WithMetaLabel(text.NewInfoSelfServiceSettingsUpdateUnlinkTOTP())
Expand Down
10 changes: 5 additions & 5 deletions selfservice/strategy/totp/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ func (s *Strategy) SettingsStrategyID() string {
// swagger:model submitSelfServiceSettingsFlowWithTOTPMethodBody
type submitSelfServiceSettingsFlowWithTOTPMethodBody struct {
// ValidationTOTP must contain a valid TOTP based on the
ValidationTOTP string `json:"verification_totp"`
ValidationTOTP string `json:"totp_code"`

// UnlinkTOTP if true will remove the TOTP pairing,
// effectively removing the credential. This can be used
// to set up a new TOTP device.
UnlinkTOTP bool `json:"unlink_totp"`
UnlinkTOTP bool `json:"totp_unlink"`

// CSRFToken is the anti-CSRF token
CSRFToken string `json:"csrf_token"`
Expand Down Expand Up @@ -171,11 +171,11 @@ func (s *Strategy) continueSettingsFlowAddTOTP(w http.ResponseWriter, r *http.Re
}

if p.ValidationTOTP == "" {
return nil, schema.NewRequiredError("#/verification_totp", "verification_totp")
return nil, schema.NewRequiredError("#/totp_code", "totp_code")
}

if !totp.Validate(p.ValidationTOTP, key.Secret()) {
return nil, schema.NewTOTPVerifierWrongError("#/verification_totp")
return nil, schema.NewTOTPVerifierWrongError("#/totp_code")
}

co, err := json.Marshal(&CredentialsConfig{TOTPURL: key.URL()})
Expand Down Expand Up @@ -251,8 +251,8 @@ func (s *Strategy) PopulateSettingsMethod(r *http.Request, id *identity.Identity
return err
}

f.UI.Nodes.Upsert(qr)
f.UI.Nodes.Upsert(NewTOTPSourceURLNode(key))
f.UI.Nodes.Upsert(qr)
f.UI.Nodes.Upsert(NewVerifyTOTPNode())
}

Expand Down

0 comments on commit c5a0d0f

Please sign in to comment.