Skip to content

Commit

Permalink
test: add login tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr committed Oct 19, 2021
1 parent 6959565 commit a71cadd
Show file tree
Hide file tree
Showing 13 changed files with 391 additions and 38 deletions.
10 changes: 10 additions & 0 deletions schema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ func NewNoTOTPDeviceRegistered() error {
})
}

func NewNoLookupDefined() error {
return errors.WithStack(&ValidationError{
ValidationError: &jsonschema.ValidationError{
Message: `you have no TOTP device set up`,
InstancePtr: "#/",
},
Messages: new(text.Messages).Add(text.NewErrorValidationNoLookup()),
})
}

func NewNoWebAuthnRegistered() error {
return errors.WithStack(&ValidationError{
ValidationError: &jsonschema.ValidationError{
Expand Down
3 changes: 2 additions & 1 deletion selfservice/strategy/lookup/credentials.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package lookup

import (
"time"

"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"time"

"github.com/ory/x/sqlxx"
)
Expand Down
5 changes: 3 additions & 2 deletions selfservice/strategy/lookup/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package lookup
import (
_ "embed"
"encoding/json"
"github.com/ory/x/assertx"
"github.com/ory/x/sqlxx"
"testing"
"time"

"github.com/ory/x/assertx"
"github.com/ory/x/sqlxx"
)

//go:embed fixtures/node.json
Expand Down
54 changes: 54 additions & 0 deletions selfservice/strategy/lookup/fixtures/login/with.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
[
{
"attributes": {
"disabled": false,
"name": "lookup_secret",
"required": true,
"type": "text",
"value": ""
},
"group": "lookup_secret",
"messages": [],
"meta": {
"label": {
"context": {},
"id": 1010007,
"text": "Backup recovery code",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "method",
"type": "submit",
"value": "lookup_secret"
},
"group": "lookup_secret",
"messages": [],
"meta": {
"label": {
"context": {},
"id": 1010001,
"text": "Sign in",
"type": "info"
}
},
"type": "input"
},
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"required": true,
"type": "hidden",
"value": "anhicmI2dWFhMWlwbHlydWNhZnF4cW13dXhmOW1ucDc="
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
}
]
2 changes: 1 addition & 1 deletion selfservice/strategy/lookup/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (s *Strategy) Login(w http.ResponseWriter, r *http.Request, f *login.Flow,

i, c, err := s.d.PrivilegedIdentityPool().FindByCredentialsIdentifier(r.Context(), s.ID(), ss.IdentityID.String())
if err != nil {
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoTOTPDeviceRegistered()))
return nil, s.handleLoginError(r, f, errors.WithStack(schema.NewNoLookupDefined()))
}

var o CredentialsConfig
Expand Down
291 changes: 291 additions & 0 deletions selfservice/strategy/lookup/login_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
package lookup_test

import (
"bytes"
"context"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"time"

"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tidwall/gjson"

"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/internal"
"github.com/ory/kratos/internal/testhelpers"
"github.com/ory/kratos/selfservice/flow/login"
"github.com/ory/kratos/selfservice/strategy/lookup"
"github.com/ory/kratos/text"
"github.com/ory/kratos/ui/node"
"github.com/ory/kratos/x"
"github.com/ory/x/assertx"
)

//go:embed fixtures/login/with.json
var loginFixtureWithLookup []byte

var lookupCodeGJSONQuery = "ui.nodes.#(attributes.name==" + identity.CredentialsTypeLookup.String() + ")"

func TestCompleteLogin(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)
conf.MustSet(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword)+".enabled", false)
conf.MustSet(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeLookup)+".enabled", true)

router := x.NewRouterPublic()
publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin())

errTS := testhelpers.NewErrorTestServer(t, reg)
uiTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)
redirTS := testhelpers.NewRedirSessionEchoTS(t, reg)

// Overwrite these two to make it more explicit when tests fail
conf.MustSet(config.ViperKeySelfServiceErrorUI, errTS.URL+"/error-ts")
conf.MustSet(config.ViperKeySelfServiceLoginUI, uiTS.URL+"/login-ts")

conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/login.schema.json")
conf.MustSet(config.ViperKeySecretsDefault, []string{"not-a-secure-session-key"})

t.Run("case=lookup payload is set when identity has lookup", func(t *testing.T) {
id, _ := createIdentity(t, reg)

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

t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)

apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})

t.Run("case=lookup payload is not set when identity has no lookup", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)

apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
assertx.EqualAsJSON(t, nil, f.Ui.Nodes)
})

doAPIFlow := func(t *testing.T, v func(url.Values), id *identity.Identity) (string, *http.Response) {
apiClient := testhelpers.NewHTTPClientWithIdentitySessionToken(t, reg, id)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", identity.CredentialsTypeLookup.String())
v(values)
payload := testhelpers.EncodeFormAsJSON(t, true, values)
return testhelpers.LoginMakeRequest(t, true, false, f, apiClient, payload)
}

doBrowserFlow := func(t *testing.T, spa bool, v func(url.Values), id *identity.Identity) (string, *http.Response) {
browserClient := testhelpers.NewHTTPClientWithIdentitySessionCookie(t, reg, id)
f := testhelpers.InitializeLoginFlowViaBrowser(t, browserClient, publicTS, false, spa, testhelpers.InitFlowWithAAL(identity.AuthenticatorAssuranceLevel2))
values := testhelpers.SDKFormFieldsToURLValues(f.Ui.Nodes)
values.Set("method", identity.CredentialsTypeLookup.String())
v(values)
return testhelpers.LoginMakeRequest(t, false, spa, f, browserClient, values.Encode())
}

checkURL := func(t *testing.T, shouldRedirect bool, res *http.Response) {
if shouldRedirect {
assert.Contains(t, res.Request.URL.String(), uiTS.URL+"/login-ts")
} else {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
}
}

t.Run("case=should fail if code is invalid", func(t *testing.T) {
for _, tc := range []struct {
d string
code string
message string
}{
{d: "empty", message: text.NewErrorValidationLookupInvalid().Text},
{d: "invalid", code: "invalid", message: text.NewErrorValidationLookupInvalid().Text},
{d: "already-used", code: "key-1", message: text.NewErrorValidationLookupAlreadyUsed().Text},
} {
t.Run(fmt.Sprintf("code=%s", tc.d), func(t *testing.T) {
id, _ := createIdentity(t, reg)
payload := func(v url.Values) {
v.Set(node.LookupCodeEnter, tc.code)
}

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, tc.message, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}

t.Run("type=api", func(t *testing.T) {
body, res := doAPIFlow(t, payload, id)
check(t, false, body, res)
})

t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, id)
check(t, true, body, res)
})

t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, id)
check(t, false, body, res)
})
})
}
})

t.Run("case=should fail if lookup was not set up for identity", func(t *testing.T) {
id := createIdentityWithoutLookup(t, reg)

payload := func(v url.Values) {
v.Set(node.LookupCodeEnter, "1111111")
}

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.NewErrorValidationNoLookup().Text, gjson.Get(body, "ui.messages.0.text").String(), "%s", body)
}

t.Run("type=api", func(t *testing.T) {
body, res := doAPIFlow(t, payload, id)
check(t, false, body, res)
})

t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload, id)
check(t, true, body, res)
})

t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload, id)
check(t, false, body, res)
})
})

t.Run("case=should pass when code is supplied correctly", func(t *testing.T) {
id, _ := createIdentity(t, reg)
payload := func(code string) func(v url.Values) {
return func(v url.Values) {
v.Set(node.LookupCodeEnter, code)
}
}

startAt := time.Now()
check := func(t *testing.T, shouldRedirect bool, body string, res *http.Response, usedKey string) {
prefix := "session."
if shouldRedirect {
assert.Contains(t, res.Request.URL.String(), redirTS.URL+"/return-ts")
prefix = ""
} else {
assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
}
assert.True(t, gjson.Get(body, prefix+"active").Bool(), "%s", body)
assert.EqualValues(t, identity.AuthenticatorAssuranceLevel2, gjson.Get(body, prefix+"authenticator_assurance_level").String())
require.Len(t, gjson.Get(body, prefix+"authentication_methods").Array(), 2)
assert.EqualValues(t, identity.CredentialsTypePassword, gjson.Get(body, prefix+"authentication_methods.0.method").String(), 2)
assert.True(t, gjson.Get(body, prefix+"authentication_methods.0.completed_at").Time().After(startAt), 2)
assert.EqualValues(t, identity.CredentialsTypeLookup, gjson.Get(body, prefix+"authentication_methods.1.method").String(), 2)
assert.True(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().After(startAt), 2)
assert.True(t, gjson.Get(body, prefix+"authentication_methods.1.completed_at").Time().After(gjson.Get(body, prefix+"authentication_methods.0.completed_at").Time()), 2)

actual, err := reg.PrivilegedIdentityPool().GetIdentityConfidential(context.Background(), uuid.FromStringOrNil(gjson.Get(body, prefix+"identity.id").String()))
require.NoError(t, err)
creds, ok := actual.GetCredentials(identity.CredentialsTypeLookup)
require.True(t, ok)

var conf lookup.CredentialsConfig
require.NoError(t, json.Unmarshal(creds.Config, &conf))

var found bool
for _, rc := range conf.RecoveryCodes {
if rc.Code == usedKey {
found = true
require.False(t, time.Time(rc.UsedAt).IsZero())
}
}

require.True(t, found)
}

t.Run("type=api", func(t *testing.T) {
body, res := doAPIFlow(t, payload("key-0"), id)
check(t, false, body, res, "key-0")
})

t.Run("type=browser", func(t *testing.T) {
body, res := doBrowserFlow(t, false, payload("key-2"), id)
check(t, true, body, res, "key-2")
})

t.Run("type=spa", func(t *testing.T) {
body, res := doBrowserFlow(t, true, payload("key-3"), id)
check(t, false, body, res, "key-3")
})
})

t.Run("case=should fail because lookup can not handle AAL1", func(t *testing.T) {
apiClient := testhelpers.NewDebugClient(t)
f := testhelpers.InitializeLoginFlowViaAPI(t, apiClient, publicTS, false)

update, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(f.Id))
require.NoError(t, err)
update.RequestedAAL = identity.AuthenticatorAssuranceLevel1
require.NoError(t, reg.LoginFlowPersister().UpdateLoginFlow(context.Background(), update))

req, err := http.NewRequest("POST", f.Ui.Action, bytes.NewBufferString(`{"method":"lookup"}`))
require.NoError(t, err)
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")

res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
body := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())
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(node.LookupCodeEnter, "111111")
}, id)

assert.Contains(t, res.Request.URL.String(), publicTS.URL+login.RouteSubmitFlow)
assert.Equal(t, text.NewErrorValidationLookupInvalid().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(node.LookupCodeEnter, "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(node.LookupCodeEnter, "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)
})
})
}

0 comments on commit a71cadd

Please sign in to comment.