Skip to content

Commit

Permalink
feat: add MFA enforcment option to whoami and settings
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr committed Oct 19, 2021
1 parent 3a82c88 commit 554d725
Show file tree
Hide file tree
Showing 18 changed files with 421 additions and 138 deletions.
208 changes: 106 additions & 102 deletions driver/config/config.go

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1215,7 +1215,6 @@
"additionalProperties": false
}
},

"if": {
"properties": {
"enabled": {
Expand Down
20 changes: 19 additions & 1 deletion selfservice/flow/settings/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"net/url"
"time"

"github.com/ory/kratos/session"

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

"github.com/pkg/errors"
Expand Down Expand Up @@ -111,7 +113,23 @@ func (s *ErrorHandler) WriteFlowError(
return
}

if e := new(FlowExpiredError); errors.As(err, &e) {
if errors.Is(err, session.ErrNoActiveSessionFound) {
if f.Type == flow.TypeAPI || x.IsJSONRequest(r) {
s.d.Writer().WriteError(w, r, err)
} else {
http.Redirect(w, r, urlx.AppendPaths(s.d.Config(r.Context()).SelfPublicURL(r), login.RouteInitBrowserFlow).String(), http.StatusSeeOther)
}
return
} else if errors.Is(err, session.ErrAALNotSatisfied) {
if f.Type == flow.TypeAPI || x.IsJSONRequest(r) {
s.d.Writer().WriteError(w, r, err)
} else {
http.Redirect(w, r, urlx.CopyWithQuery(
urlx.AppendPaths(s.d.Config(r.Context()).SelfPublicURL(r), login.RouteInitBrowserFlow),
url.Values{"aal": {string(identity.AuthenticatorAssuranceLevel2)}}).String(), http.StatusSeeOther)
}
return
} else if e := new(FlowExpiredError); errors.As(err, &e) {
if id == nil {
s.forward(w, r, f, err)
return
Expand Down
80 changes: 77 additions & 3 deletions selfservice/flow/settings/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"testing"
"time"

"github.com/pkg/errors"

"github.com/gofrs/uuid"

"github.com/ory/kratos/ui/node"
Expand Down Expand Up @@ -46,8 +48,8 @@ func TestHandleError(t *testing.T) {
t.Cleanup(ts.Close)

_ = testhelpers.NewSettingsUIFlowEchoServer(t, reg)
testhelpers.NewErrorTestServer(t, reg)
testhelpers.NewLoginUIFlowEchoServer(t, reg)
errorTS := testhelpers.NewErrorTestServer(t, reg)
loginTS := testhelpers.NewLoginUIFlowEchoServer(t, reg)

h := reg.SettingsFlowErrorHandler()
sdk := testhelpers.NewSDKClient(admin)
Expand All @@ -59,7 +61,7 @@ func TestHandleError(t *testing.T) {
require.NoError(t, faker.FakeData(&id))
id.SchemaID = "default"
id.State = identity.StateActive
reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &id)
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &id))

router.GET("/error", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
h.WriteFlowError(w, r, flowMethod, settingsFlow, &id, flowError)
Expand Down Expand Up @@ -193,6 +195,42 @@ func TestHandleError(t *testing.T) {
assert.Equal(t, settingsFlow.ID.String(), gjson.GetBytes(body, "id").String())
})

t.Run("case=no active session", func(t *testing.T) {
t.Cleanup(reset)

settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser)
settingsFlow.IdentityID = id.ID
flowError = errors.WithStack(session.ErrNoActiveSessionFound)
flowMethod = settings.StrategyProfile

res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)

body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, session.ErrNoActiveSessionFound.Reason(), gjson.GetBytes(body, "error.reason").String(), "%s", body)
})

t.Run("case=aal too low", func(t *testing.T) {
t.Cleanup(reset)

settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser)
settingsFlow.IdentityID = id.ID
flowError = errors.WithStack(session.ErrAALNotSatisfied)
flowMethod = settings.StrategyProfile

res, err := ts.Client().Do(testhelpers.NewHTTPGetJSONRequest(t, ts.URL+"/error"))
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusForbidden, res.StatusCode)

body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
assert.Equal(t, session.ErrAALNotSatisfied.Reason(), gjson.GetBytes(body, "error.reason").String(), "%s", body)
})

t.Run("case=generic error", func(t *testing.T) {
t.Cleanup(reset)

Expand Down Expand Up @@ -248,6 +286,42 @@ func TestHandleError(t *testing.T) {
assert.EqualValues(t, settingsFlow.ID, lf.ID)
})

t.Run("case=no active session error", func(t *testing.T) {
t.Cleanup(reset)

settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser)
settingsFlow.IdentityID = id.ID
flowError = errors.WithStack(session.ErrNoActiveSessionFound)
flowMethod = settings.StrategyProfile

res, err := ts.Client().Get(ts.URL + "/error")
require.NoError(t, err)
require.NoError(t, res.Body.Close())
assert.Contains(t, res.Request.URL.String(), loginTS.URL)

lf, err := reg.LoginFlowPersister().GetLoginFlow(context.Background(), uuid.FromStringOrNil(res.Request.URL.Query().Get("flow")))
require.NoError(t, err)
assert.Equal(t, identity.AuthenticatorAssuranceLevel1, lf.RequestedAAL)
})

t.Run("case=aal too low", func(t *testing.T) {
t.Cleanup(reset)

settingsFlow = newFlow(t, time.Minute, flow.TypeBrowser)
settingsFlow.IdentityID = id.ID
flowError = errors.WithStack(session.ErrAALNotSatisfied)
flowMethod = settings.StrategyProfile

res, err := ts.Client().Get(ts.URL + "/error")
require.NoError(t, err)
assert.Contains(t, res.Request.URL.String(), errorTS.URL)
body := x.MustReadAll(res.Body)
require.NoError(t, res.Body.Close())

// We end up at the error endpoint with an aal2 error message because ts.client has no session.
assert.Equal(t, "You can not requested a higher AAL (AAL2/AAL3) without an active session.", gjson.GetBytes(body, "reason").String(), "%s", body)
})

t.Run("case=session old error", func(t *testing.T) {
t.Cleanup(reset)

Expand Down
34 changes: 30 additions & 4 deletions selfservice/flow/settings/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package settings

import (
"net/http"
"net/url"
"time"

"github.com/ory/kratos/selfservice/flow/login"

"github.com/ory/kratos/ui/node"
"github.com/ory/x/sqlcon"

Expand Down Expand Up @@ -182,6 +185,11 @@ func (h *Handler) initApiFlow(w http.ResponseWriter, r *http.Request, _ httprout
return
}

if err := h.d.SessionManager().DoesSessionSatisfy(r.Context(), s, h.d.Config(r.Context()).SelfServiceSettingsRequiredAAL()); err != nil {
h.d.Writer().WriteError(w, r, err)
return
}

f, err := h.NewFlow(w, r, s.Identity, flow.TypeAPI)
if err != nil {
h.d.Writer().WriteError(w, r, err)
Expand Down Expand Up @@ -238,6 +246,20 @@ func (h *Handler) initBrowserFlow(w http.ResponseWriter, r *http.Request, ps htt
return
}

if err := h.d.SessionManager().DoesSessionSatisfy(r.Context(), s, h.d.Config(r.Context()).SelfServiceSettingsRequiredAAL()); errors.Is(err, session.ErrAALNotSatisfied) {
if x.IsJSONRequest(r) {
h.d.Writer().WriteError(w, r, err)
} else {
http.Redirect(w, r, urlx.CopyWithQuery(
urlx.AppendPaths(h.d.Config(r.Context()).SelfPublicURL(r), login.RouteInitBrowserFlow),
url.Values{"aal": {string(identity.AuthenticatorAssuranceLevel2)}}).String(), http.StatusSeeOther)
}
return
} else if err != nil {
h.d.Writer().WriteError(w, r, err)
return
}

f, err := h.NewFlow(w, r, s.Identity, flow.TypeBrowser)
if err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
Expand Down Expand Up @@ -336,6 +358,10 @@ func (h *Handler) fetchFlow(w http.ResponseWriter, r *http.Request) error {
return errors.WithStack(herodot.ErrForbidden.WithReasonf("The request was made for another identity and has been blocked for security reasons."))
}

if err := h.d.SessionManager().DoesSessionSatisfy(r.Context(), sess, h.d.Config(r.Context()).SelfServiceSettingsRequiredAAL()); err != nil {
return err
}

if pr.ExpiresAt.Before(time.Now().UTC()) {
if pr.Type == flow.TypeBrowser {
h.d.Writer().WriteError(w, r, errors.WithStack(x.ErrGone.
Expand Down Expand Up @@ -448,11 +474,11 @@ func (h *Handler) submitSettingsFlow(w http.ResponseWriter, r *http.Request, ps

ss, err := h.d.SessionManager().FetchFromRequest(r.Context(), r)
if err != nil {
if f.Type == flow.TypeBrowser && !x.IsJSONRequest(r) {
http.Redirect(w, r, h.d.Config(r.Context()).SelfServiceFlowLoginUI().String(), http.StatusFound)
return
}
h.d.SettingsFlowErrorHandler().WriteFlowError(w, r, node.DefaultGroup, f, nil, err)
return
}

if err := h.d.SessionManager().DoesSessionSatisfy(r.Context(), ss, h.d.Config(r.Context()).SelfServiceSettingsRequiredAAL()); err != nil {
h.d.SettingsFlowErrorHandler().WriteFlowError(w, r, node.DefaultGroup, f, nil, err)
return
}
Expand Down

0 comments on commit 554d725

Please sign in to comment.