Skip to content

Commit

Permalink
feat: implement AAL for login and sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr committed Oct 19, 2021
1 parent 6184fe3 commit 45467e0
Show file tree
Hide file tree
Showing 37 changed files with 660 additions and 166 deletions.
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ replace (
go.mongodb.org/mongo-driver => go.mongodb.org/mongo-driver v1.4.6
golang.org/x/sys => golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac
gopkg.in/DataDog/dd-trace-go.v1 => gopkg.in/DataDog/dd-trace-go.v1 v1.27.1-0.20201005154917-54b73b3e126a
github.com/gobuffalo/pop/v5 => github.com/gobuffalo/pop/v5 v5.3.4-0.20210608105745-bb07a373cc0e
github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.7-0.20210414154423-1157a4212dcb
github.com/ory/x => github.com/ory/x v0.0.276
github.com/oleiade/reflections => github.com/oleiade/reflections v1.0.1
github.com/luna-duclos/instrumentedsql => github.com/ory/instrumentedsql v1.2.0
github.com/luna-duclos/instrumentedsql/opentracing => github.com/ory/instrumentedsql/opentracing v0.0.0-20210903114257-c8963b546c5c
)

require (
Expand Down
6 changes: 3 additions & 3 deletions internal/testhelpers/handler_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ func MockSetSession(t *testing.T, reg mockDeps, conf *config.Config) httprouter.
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), i))

activeSession, _ := session.NewActiveSession(i, conf, time.Now().UTC())
require.NoError(t, reg.SessionManager().CreateAndIssueCookie(context.Background(), w, r, activeSession))
activeSession, _ := session.NewActiveSession(i, conf, time.Now().UTC(), identity.CredentialsTypePassword)
require.NoError(t, reg.SessionManager().UpsertAndIssueCookie(context.Background(), w, r, activeSession))

w.WriteHeader(http.StatusOK)
}
Expand Down Expand Up @@ -112,7 +112,7 @@ func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identit
require.NoError(t, err)
sess.Identity = inserted

require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), &sess))
require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), &sess))
require.Len(t, inserted.Credentials, len(i.Credentials))

return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
Expand Down
4 changes: 2 additions & 2 deletions internal/testhelpers/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func CreateSession(t *testing.T, reg driver.Registry) *session.Session {
ctx := context.Background()
i := identity.NewIdentity(config.DefaultIdentityTraitsSchemaID)
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(ctx, i))
sess, err := session.NewActiveSession(i, reg.Config(ctx), time.Now().UTC())
sess, err := session.NewActiveSession(i, reg.Config(ctx), time.Now().UTC(), identity.CredentialsTypePassword)
require.NoError(t, err)
require.NoError(t, reg.SessionPersister().CreateSession(ctx, sess))
require.NoError(t, reg.SessionPersister().UpsertSession(ctx, sess))
return sess
}
8 changes: 5 additions & 3 deletions internal/testhelpers/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func maybePersistSession(t *testing.T, reg *driver.RegistryDefault, sess *sessio
sess.Identity = id
sess.IdentityID = id.ID

require.NoError(t, err, reg.SessionPersister().CreateSession(context.Background(), sess))
require.NoError(t, err, reg.SessionPersister().UpsertSession(context.Background(), sess))
}

func NewHTTPClientWithSessionCookie(t *testing.T, reg *driver.RegistryDefault, sess *session.Session) *http.Client {
Expand Down Expand Up @@ -96,6 +96,7 @@ func NewHTTPClientWithArbitrarySessionToken(t *testing.T, reg *driver.RegistryDe
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
NewSessionLifespanProvider(time.Hour),
time.Now(),
identity.CredentialsTypePassword,
)
require.NoError(t, err, "Could not initialize session from identity.")

Expand All @@ -107,21 +108,22 @@ func NewHTTPClientWithArbitrarySessionCookie(t *testing.T, reg *driver.RegistryD
&identity.Identity{ID: x.NewUUID(), State: identity.StateActive},
NewSessionLifespanProvider(time.Hour),
time.Now(),
identity.CredentialsTypePassword,
)
require.NoError(t, err, "Could not initialize session from identity.")

return NewHTTPClientWithSessionCookie(t, reg, s)
}

func NewHTTPClientWithIdentitySessionCookie(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
s, err := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now())
s, err := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword)
require.NoError(t, err, "Could not initialize session from identity.")

return NewHTTPClientWithSessionCookie(t, reg, s)
}

func NewHTTPClientWithIdentitySessionToken(t *testing.T, reg *driver.RegistryDefault, id *identity.Identity) *http.Client {
s, err := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now())
s, err := session.NewActiveSession(id, NewSessionLifespanProvider(time.Hour), time.Now(), identity.CredentialsTypePassword)
require.NoError(t, err, "Could not initialize session from identity.")

return NewHTTPClientWithSessionToken(t, reg, s)
Expand Down
2 changes: 1 addition & 1 deletion persistence/sql/persister_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (p *Persister) ForceLoginFlow(ctx context.Context, id uuid.UUID) error {
return err
}

lr.Forced = true
lr.Refresh = true
return tx.Save(lr, "nid")
})
}
10 changes: 10 additions & 0 deletions schema/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,13 @@ func NewAddressNotVerifiedError() error {
Messages: new(text.Messages).Add(text.NewErrorValidationAddressNotVerified()),
})
}

func NewInvalidTOTPCode() error {
return errors.WithStack(&ValidationError{
ValidationError: &jsonschema.ValidationError{
Message: `the authentication code is not correct`,
InstancePtr: "#/",
},
Messages: new(text.Messages).Add(text.NewErrorValidationInvalidTOTPCode()),
})
}
15 changes: 15 additions & 0 deletions selfservice/flow/login/aal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package login

import (
"github.com/pkg/errors"

"github.com/ory/kratos/identity"
"github.com/ory/kratos/selfservice/flow"
)

func CheckAAL(f *Flow, expected identity.AuthenticatorAssuranceLevel) error {
if f.RequestedAAL != expected {
return errors.WithStack(flow.ErrStrategyNotResponsible)
}
return nil
}
6 changes: 6 additions & 0 deletions selfservice/flow/login/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ var (
ErrHookAbortFlow = errors.New("aborted login hook execution")
ErrAlreadyLoggedIn = herodot.ErrBadRequest.WithReason("A valid session was detected and thus login is not possible. Did you forget to set `?refresh=true`?")
ErrAddressNotVerified = herodot.ErrBadRequest.WithReason("The email address is not verified yet.")

// ErrSessionHasAALAlready is returned when one attempts to upgrade the AAL of an active session which already has that AAL.
ErrSessionHasAALAlready = herodot.ErrUnauthorized.WithError("session has the requested authenticator assurance level already").WithReason("The session has the requested AAL already.")

// ErrSessionRequiredForHigherAAL is returned when someone requests AAL2 or AAL3 even though no active session exists yet.
ErrSessionRequiredForHigherAAL = herodot.ErrUnauthorized.WithError("aal2 and aal3 can only be requested if a session exists already").WithReason("You can not requested a higher AAL (AAL2/AAL3) without an active session.")
)

type (
Expand Down
4 changes: 3 additions & 1 deletion selfservice/flow/login/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"testing"
"time"

"github.com/ory/kratos/identity"

"github.com/gofrs/uuid"

"github.com/ory/kratos/ui/node"
Expand Down Expand Up @@ -62,7 +64,7 @@ func TestHandleError(t *testing.T) {
req := &http.Request{URL: urlx.ParseOrPanic("/")}
f := login.NewFlow(conf, ttl, "csrf_token", req, ft)
for _, s := range reg.LoginStrategies(context.Background()) {
require.NoError(t, s.PopulateLoginMethod(req, f))
require.NoError(t, s.PopulateLoginMethod(req, identity.AuthenticatorAssuranceLevel1, f))
}

require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), f))
Expand Down
19 changes: 15 additions & 4 deletions selfservice/flow/login/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"time"

"github.com/ory/x/stringsx"

"github.com/ory/kratos/driver/config"

"github.com/ory/kratos/ui/container"
Expand Down Expand Up @@ -82,6 +85,11 @@ type Flow struct {

// Refresh stores whether this login flow should enforce re-authentication.
Refresh bool `json:"forced" db:"forced"`

// RequestedAAL stores if the flow was requested to update the authenticator assurance level.
//
// This value can be one of "aal1", "aal2", "aal3".
RequestedAAL identity.AuthenticatorAssuranceLevel `json:"requested_aal" db:"requested_aal"`
}

func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Request, flowType flow.Type) *Flow {
Expand All @@ -95,10 +103,13 @@ func NewFlow(conf *config.Config, exp time.Duration, csrf string, r *http.Reques
Method: "POST",
Action: flow.AppendFlowTo(urlx.AppendPaths(conf.SelfPublicURL(r), RouteSubmitFlow), id).String(),
},
RequestURL: x.RequestURL(r).String(),
CSRFToken: csrf,
Type: flowType,
Refresh: r.URL.Query().Get("refresh") == "true",
RequestURL: x.RequestURL(r).String(),
CSRFToken: csrf,
Type: flowType,
Refresh: r.URL.Query().Get("refresh") == "true",
RequestedAAL: identity.AuthenticatorAssuranceLevel(strings.ToLower(stringsx.Coalesce(
r.URL.Query().Get("aal"),
string(identity.AuthenticatorAssuranceLevel1)))),
}
}

Expand Down
61 changes: 38 additions & 23 deletions selfservice/flow/login/flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,47 @@ func TestFakeFlow(t *testing.T) {

func TestNewFlow(t *testing.T) {
conf, _ := internal.NewFastRegistryWithMocks(t)
t.Run("case=0", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("/"),
Host: "ory.sh", TLS: &tls.ConnectionState{},
}, flow.TypeBrowser)
assert.EqualValues(t, r.IssuedAt, r.ExpiresAt)
assert.Equal(t, flow.TypeBrowser, r.Type)
assert.False(t, r.Refresh)
assert.Equal(t, "https://ory.sh/", r.RequestURL)
})

t.Run("case=1", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("/?refresh=true"),
Host: "ory.sh"}, flow.TypeAPI)
assert.Equal(t, r.IssuedAt, r.ExpiresAt)
assert.Equal(t, flow.TypeAPI, r.Type)
assert.True(t, r.Refresh)
assert.Equal(t, "http://ory.sh/?refresh=true", r.RequestURL)
t.Run("type=browser", func(t *testing.T) {
t.Run("case=regular flow creation without a session", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("/"),
Host: "ory.sh", TLS: &tls.ConnectionState{},
}, flow.TypeBrowser)
assert.EqualValues(t, r.IssuedAt, r.ExpiresAt)
assert.Equal(t, flow.TypeBrowser, r.Type)
assert.False(t, r.Refresh)
assert.Equal(t, "https://ory.sh/", r.RequestURL)
})

t.Run("case=regular flow creation", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("https://ory.sh/"),
Host: "ory.sh"}, flow.TypeBrowser)
assert.Equal(t, "https://ory.sh/", r.RequestURL)
})
})

t.Run("case=2", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("https://ory.sh/"),
Host: "ory.sh"}, flow.TypeBrowser)
assert.Equal(t, "https://ory.sh/", r.RequestURL)
t.Run("type=api", func(t *testing.T) {
t.Run("case=flow with refresh", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("/?refresh=true"),
Host: "ory.sh"}, flow.TypeAPI)
assert.Equal(t, r.IssuedAt, r.ExpiresAt)
assert.Equal(t, flow.TypeAPI, r.Type)
assert.True(t, r.Refresh)
assert.Equal(t, "http://ory.sh/?refresh=true", r.RequestURL)
})

t.Run("case=flow without refresh", func(t *testing.T) {
r := login.NewFlow(conf, 0, "csrf", &http.Request{
URL: urlx.ParseOrPanic("/"),
Host: "ory.sh"}, flow.TypeAPI)
assert.Equal(t, r.IssuedAt, r.ExpiresAt)
assert.Equal(t, flow.TypeAPI, r.Type)
assert.False(t, r.Refresh)
assert.Equal(t, "http://ory.sh/", r.RequestURL)
})
})
}

Expand Down

0 comments on commit 45467e0

Please sign in to comment.