Skip to content

Commit

Permalink
feat(recovery): allow invalidation of existing sessions
Browse files Browse the repository at this point in the history
You can now use the `revoke_active_sessions` hook in the recovery flow. It invalidates all of an identity's sessions on successful account recovery.

Closes #1077
  • Loading branch information
aeneasr committed Mar 7, 2022
1 parent bf10868 commit 5029884
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 28 deletions.
17 changes: 16 additions & 1 deletion embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,21 @@
"uniqueItems": true,
"additionalItems": false
},
"selfServiceAfterRecoveryHooks": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/selfServiceWebHook"
},
{
"$ref": "#/definitions/selfServiceSessionRevokerHook"
}
]
},
"uniqueItems": true,
"additionalItems": false
},
"selfServiceAfterSettingsMethod": {
"type": "object",
"additionalProperties": false,
Expand Down Expand Up @@ -742,7 +757,7 @@
"$ref": "#/definitions/defaultReturnTo"
},
"hooks": {
"$ref": "#/definitions/selfServiceHooks"
"$ref": "#/definitions/selfServiceAfterRecoveryHooks"
}
}
},
Expand Down
11 changes: 10 additions & 1 deletion selfservice/hook/session_destroyer.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package hook

import (
"github.com/ory/kratos/selfservice/flow/recovery"
"net/http"

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

var _ login.PostHookExecutor = new(SessionDestroyer)
var _ recovery.PostHookExecutor = new(SessionDestroyer)

type (
sessionDestroyerDependencies interface {
Expand All @@ -24,7 +26,14 @@ func NewSessionDestroyer(r sessionDestroyerDependencies) *SessionDestroyer {
}

func (e *SessionDestroyer) ExecuteLoginPostHook(_ http.ResponseWriter, r *http.Request, _ *login.Flow, s *session.Session) error {
if err := e.r.SessionPersister().DeleteSessionsByIdentity(r.Context(), s.Identity.ID); err != nil {
if _, err := e.r.SessionPersister().RevokeSessionsIdentityExcept(r.Context(), s.Identity.ID, s.ID); err != nil {
return err
}
return nil
}

func (e *SessionDestroyer) ExecutePostRecoveryHook(_ http.ResponseWriter, r *http.Request, _ *recovery.Flow, s *session.Session) error {
if _, err := e.r.SessionPersister().RevokeSessionsIdentityExcept(r.Context(), s.Identity.ID, s.ID); err != nil {
return err
}
return nil
Expand Down
81 changes: 56 additions & 25 deletions selfservice/hook/session_destroyer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/ory/kratos/internal"
"github.com/ory/kratos/selfservice/hook"
"github.com/ory/kratos/session"
"github.com/ory/x/sqlcon"
)

func init() {
Expand All @@ -35,32 +34,64 @@ func TestSessionDestroyer(t *testing.T) {

h := hook.NewSessionDestroyer(reg)

t.Run("method=ExecuteLoginPostHook", func(t *testing.T) {
var i identity.Identity
require.NoError(t, faker.FakeData(&i))
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))
for _, tc := range []struct {
name string
hook func(*identity.Identity) error
}{
{
name: "ExecuteLoginPostHook",
hook: func(i *identity.Identity) error {
return h.ExecuteLoginPostHook(
httptest.NewRecorder(),
new(http.Request),
nil,
&session.Session{Identity: i},
)
},
},
{
name: "ExecutePostRecoveryHook",
hook: func(i *identity.Identity) error {
return h.ExecutePostRecoveryHook(
httptest.NewRecorder(),
new(http.Request),
nil,
&session.Session{Identity: i},
)
},
},
} {
t.Run(tc.name, func(t *testing.T) {
var i identity.Identity
require.NoError(t, faker.FakeData(&i))
require.NoError(t, reg.PrivilegedIdentityPool().CreateIdentity(context.Background(), &i))

sessions := make([]session.Session, 5)
for k := range sessions {
s := sessions[k] // keep this for pointers' sake ;)
require.NoError(t, faker.FakeData(&s))
s.IdentityID = uuid.Nil
s.Identity = &i
sessions := make([]session.Session, 5)
for k := range sessions {
s := sessions[k] // keep this for pointers' sake ;)
require.NoError(t, faker.FakeData(&s))
s.IdentityID = uuid.Nil
s.Identity = &i
s.Active = true

require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), &s))
}
require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), &s))
sessions[k] = s
}

// Should revoke all the sessions.
require.NoError(t, h.ExecuteLoginPostHook(
httptest.NewRecorder(),
new(http.Request),
nil,
&session.Session{Identity: &i},
))
for k := range sessions {
sess, err := reg.SessionPersister().GetSession(context.Background(), sessions[k].ID)
require.NoError(t, err)
assert.True(t, sess.IsActive())
}

for k := range sessions {
_, err := reg.SessionPersister().GetSession(context.Background(), sessions[k].ID)
assert.EqualError(t, err, sqlcon.ErrNoRows.Error())
}
})
// Should revoke all the sessions.
require.NoError(t, tc.hook(&i))

for k := range sessions {
sess, err := reg.SessionPersister().GetSession(context.Background(), sessions[k].ID)
require.NoError(t, err)
assert.False(t, sess.IsActive())
}
})
}
}
50 changes: 49 additions & 1 deletion selfservice/strategy/link/strategy_recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -194,7 +195,7 @@ func TestRecovery(t *testing.T) {

public, _ := testhelpers.NewKratosServerWithCSRF(t, reg)

var createIdentityToRecover = func(email string) {
var createIdentityToRecover = func(email string) *identity.Identity {
var id = &identity.Identity{
Credentials: map[identity.CredentialsType]identity.Credentials{
"password": {Type: "password", Identifiers: []string{email}, Config: sqlxx.JSONRawMessage(`{"hashed_password":"foo"}`)}},
Expand All @@ -208,6 +209,7 @@ func TestRecovery(t *testing.T) {
assert.False(t, addr.Verified)
assert.Nil(t, addr.VerifiedAt)
assert.Equal(t, identity.VerifiableAddressStatusPending, addr.Status)
return id
}

var expect = func(t *testing.T, hc *http.Client, isAPI, isSPA bool, values func(url.Values), c int) string {
Expand Down Expand Up @@ -512,6 +514,52 @@ func TestRecovery(t *testing.T) {
check(t, expectSuccess(t, nil, false, false, values))
})

t.Run("description=should recover and invalidate all other sessions if hook is set", func(t *testing.T) {
conf.MustSet(config.HookStrategyKey(config.ViperKeySelfServiceRecoveryAfter, config.HookGlobal), []config.SelfServiceHook{{Name: "revoke_active_sessions"}})
t.Cleanup(func() {
conf.MustSet(config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter, identity.CredentialsTypePassword.String()), nil)
})

recoveryEmail := strings.ToLower(testhelpers.RandomEmail())
email := recoveryEmail
id := createIdentityToRecover(email)

sess, err := session.NewActiveSession(id, conf, time.Now(), identity.CredentialsTypePassword, identity.AuthenticatorAssuranceLevel1)
require.NoError(t, err)
require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), sess))

actualSession, err := reg.SessionPersister().GetSession(context.Background(), sess.ID)
require.NoError(t, err)
assert.True(t, actualSession.IsActive())

var check = func(t *testing.T, actual string) {
message := testhelpers.CourierExpectMessage(t, reg, recoveryEmail, "Recover access to your account")
recoveryLink := testhelpers.CourierExpectLinkInMessage(t, message, 1)

cl := testhelpers.NewClientWithCookies(t)
cl.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
res, err := cl.Get(recoveryLink)
require.NoError(t, err)
require.NoError(t, res.Body.Close())
assert.Equal(t, http.StatusSeeOther, res.StatusCode)
require.Len(t, cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)), 2)
cookies := spew.Sdump(cl.Jar.Cookies(urlx.ParseOrPanic(public.URL)))
assert.Contains(t, cookies, "ory_kratos_session")

actualSession, err := reg.SessionPersister().GetSession(context.Background(), sess.ID)
require.NoError(t, err)
assert.False(t, actualSession.IsActive())
}

var values = func(v url.Values) {
v.Set("email", recoveryEmail)
}

check(t, expectSuccess(t, nil, false, false, values))
})

t.Run("description=should not be able to use an invalid link", func(t *testing.T) {
c := testhelpers.NewClientWithCookies(t)
f := testhelpers.InitializeRecoveryFlowViaBrowser(t, c, false, public, nil)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[
{
"attributes": {
"disabled": false,
"name": "csrf_token",
"node_type": "input",
"required": true,
"type": "hidden"
},
"group": "default",
"messages": [],
"meta": {},
"type": "input"
}
]

0 comments on commit 5029884

Please sign in to comment.