Skip to content

Commit a6f2793

Browse files
author
Ajay Kelkar
authored
feat: immutable cookie session values (#2761)
Closes #2701
1 parent cdaf68d commit a6f2793

File tree

5 files changed

+123
-1
lines changed

5 files changed

+123
-1
lines changed

selfservice/flow/login/handler_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"testing"
1212
"time"
1313

14+
"github.com/ory/x/urlx"
15+
1416
"github.com/ory/x/sqlxx"
1517

1618
"github.com/ory/kratos/selfservice/flow"
@@ -233,6 +235,73 @@ func TestFlowLifecycle(t *testing.T) {
233235
assert.NotEqual(t, gjson.Get(b, "session.id").String(), gjson.Get(a, "id").String())
234236
})
235237
})
238+
239+
t.Run("case=changed kratos session identifiers when refresh is true", func(t *testing.T) {
240+
t.Cleanup(func() {
241+
conf.MustSet(ctx, config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh")
242+
})
243+
244+
t.Run("type=browser", func(t *testing.T) {
245+
// Setup flow
246+
f := login.Flow{Type: flow.TypeBrowser, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: false, RequestedAAL: "aal1"}
247+
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
248+
249+
// Submit Login
250+
hc := testhelpers.NewClientWithCookies(t)
251+
res, err := hc.PostForm(ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {x.FakeCSRFToken}})
252+
require.NoError(t, err)
253+
254+
// Check response and session cookie presence
255+
assert.Equal(t, http.StatusOK, res.StatusCode)
256+
require.Len(t, hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL+login.RouteGetFlow)), 1)
257+
require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL))), "ory_kratos_session")
258+
cookies1 := hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL + login.RouteGetFlow))
259+
260+
req, err := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil)
261+
require.NoError(t, err)
262+
263+
res, err = hc.Do(req)
264+
require.NoError(t, err)
265+
assert.Equal(t, http.StatusOK, res.StatusCode)
266+
firstSession := x.MustReadAll(res.Body)
267+
require.NoError(t, res.Body.Close())
268+
269+
// Refresh
270+
f = login.Flow{Type: flow.TypeBrowser, ExpiresAt: time.Now().Add(time.Minute), IssuedAt: time.Now(), UI: container.New(""), Refresh: true, RequestedAAL: "aal1"}
271+
require.NoError(t, reg.LoginFlowPersister().CreateLoginFlow(context.Background(), &f))
272+
273+
vv := testhelpers.EncodeFormAsJSON(t, false, url.Values{"method": {"password"}, "password_identifier": {id1mail}, "password": {"foobar"}, "csrf_token": {x.FakeCSRFToken}})
274+
275+
req, err = http.NewRequest("POST", ts.URL+login.RouteSubmitFlow+"?flow="+f.ID.String(), strings.NewReader(vv))
276+
require.NoError(t, err)
277+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
278+
279+
// Submit Login
280+
res, err = hc.Do(req)
281+
require.NoError(t, err)
282+
283+
// Check response and session cookie presence
284+
assert.Equal(t, http.StatusOK, res.StatusCode)
285+
require.Len(t, hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL+login.RouteGetFlow)), 1)
286+
require.Contains(t, fmt.Sprintf("%v", hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL))), "ory_kratos_session")
287+
cookies2 := hc.Jar.Cookies(urlx.ParseOrPanic(ts.URL + login.RouteGetFlow))
288+
289+
req, err = http.NewRequest("GET", ts.URL+"/sessions/whoami", nil)
290+
require.NoError(t, err)
291+
292+
res, err = hc.Do(req)
293+
require.NoError(t, err)
294+
assert.Equal(t, http.StatusOK, res.StatusCode)
295+
secondSession := x.MustReadAll(res.Body)
296+
require.NoError(t, res.Body.Close())
297+
298+
// Sessions should still be resolvable despite different kratos session identifier due to nonce
299+
assert.NotEqual(t, cookies1[0].String(), cookies2[0].String())
300+
assert.Equal(t, id1mail, gjson.Get(string(firstSession), "identity.traits.username").String())
301+
assert.Equal(t, id1mail, gjson.Get(string(secondSession), "identity.traits.username").String())
302+
assert.Equal(t, gjson.Get(string(secondSession), "id").String(), gjson.Get(string(firstSession), "id").String())
303+
})
304+
})
236305
})
237306

238307
t.Run("case=ensure aal is checked for upgradeability on session", func(t *testing.T) {

selfservice/flow/settings/handler_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,32 @@ func TestHandler(t *testing.T) {
402402
assert.Equal(t, "You must restart the flow because the resumable session was initiated by another person.", gjson.Get(actual, "ui.messages.0.text").String(), actual)
403403
})
404404
})
405+
406+
t.Run("description=submit - kratos session cookie issued", func(t *testing.T) {
407+
t.Run("type=spa", func(t *testing.T) {
408+
_, body := initFlow(t, primaryUser, false)
409+
var f kratos.SelfServiceSettingsFlow
410+
require.NoError(t, json.Unmarshal(body, &f))
411+
412+
actual, res := testhelpers.SettingsMakeRequest(t, false, true, &f, primaryUser, fmt.Sprintf(`{"method":"profile", "numby": 15, "csrf_token": "%s"}`, x.FakeCSRFToken))
413+
assert.Equal(t, http.StatusOK, res.StatusCode)
414+
require.Len(t, primaryUser.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL+login.RouteGetFlow)), 1)
415+
require.Contains(t, fmt.Sprintf("%v", primaryUser.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL))), "ory_kratos_session")
416+
assert.Equal(t, "Your changes have been saved!", gjson.Get(actual, "ui.messages.0.text").String(), actual)
417+
})
418+
419+
t.Run("type=browser", func(t *testing.T) {
420+
_, body := initFlow(t, primaryUser, false)
421+
var f kratos.SelfServiceSettingsFlow
422+
require.NoError(t, json.Unmarshal(body, &f))
423+
424+
actual, res := testhelpers.SettingsMakeRequest(t, false, false, &f, primaryUser, `method=profile&traits.numby=15&csrf_token=`+x.FakeCSRFToken)
425+
assert.Equal(t, http.StatusOK, res.StatusCode)
426+
require.Len(t, primaryUser.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL+login.RouteGetFlow)), 1)
427+
require.Contains(t, fmt.Sprintf("%v", primaryUser.Jar.Cookies(urlx.ParseOrPanic(publicTS.URL))), "ory_kratos_session")
428+
assert.Equal(t, "Your changes have been saved!", gjson.Get(actual, "ui.messages.0.text").String(), actual)
429+
})
430+
})
405431
})
406432

407433
t.Run("case=relative redirect when self-service settings ui is a relative url", func(t *testing.T) {

selfservice/flow/settings/hook.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/http"
77
"time"
88

9+
"github.com/ory/kratos/session"
10+
911
"github.com/ory/kratos/text"
1012
"github.com/ory/kratos/ui/container"
1113
"github.com/ory/kratos/ui/node"
@@ -48,6 +50,7 @@ type (
4850
executorDependencies interface {
4951
identity.ManagementProvider
5052
identity.ValidationProvider
53+
session.ManagementProvider
5154
config.Provider
5255

5356
HandlerProvider
@@ -271,7 +274,21 @@ func (e *HookExecutor) PostSettingsHook(w http.ResponseWriter, r *http.Request,
271274
WithField("flow_method", settingsType).
272275
Debug("Completed all PostSettingsPrePersistHooks and PostSettingsPostPersistHooks.")
273276

274-
if ctxUpdate.Flow.Type == flow.TypeAPI || x.IsJSONRequest(r) {
277+
if ctxUpdate.Flow.Type == flow.TypeAPI {
278+
updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(r.Context(), ctxUpdate.Flow.ID)
279+
if err != nil {
280+
return err
281+
}
282+
283+
e.d.Writer().Write(w, r, updatedFlow)
284+
return nil
285+
}
286+
287+
if err := e.d.SessionManager().IssueCookie(r.Context(), w, r, ctxUpdate.Session); err != nil {
288+
return errors.WithStack(err)
289+
}
290+
291+
if x.IsJSONRequest(r) {
275292
updatedFlow, err := e.d.SettingsFlowPersister().GetSettingsFlow(r.Context(), ctxUpdate.Flow.ID)
276293
if err != nil {
277294
return err

session/manager_http.go

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"net/url"
77
"time"
88

9+
"github.com/ory/x/randx"
10+
911
"github.com/gorilla/sessions"
1012

1113
"github.com/ory/x/urlx"
@@ -129,6 +131,7 @@ func (s *ManagerHTTP) IssueCookie(ctx context.Context, w http.ResponseWriter, r
129131

130132
cookie.Values["session_token"] = session.Token
131133
cookie.Values["expires_at"] = session.ExpiresAt.UTC().Format(time.RFC3339Nano)
134+
cookie.Values["nonce"] = randx.MustString(8, randx.Alpha) // Guarantee new kratos session identifier
132135

133136
if err := cookie.Save(r, w); err != nil {
134137
return errors.WithStack(err)

session/manager_http_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ func TestManagerHTTP(t *testing.T) {
104104
return rec.Result().Cookies()[0]
105105
}
106106

107+
t.Run("case=immutability", func(t *testing.T) {
108+
cookie1 := getCookie(t, x.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil))
109+
cookie2 := getCookie(t, x.NewTestHTTPRequest(t, "GET", "https://baseurl.com/bar", nil))
110+
111+
assert.NotEqual(t, cookie1.Value, cookie2.Value)
112+
})
113+
107114
t.Run("case=with default options", func(t *testing.T) {
108115
actual := getCookie(t, httptest.NewRequest("GET", "https://baseurl.com/bar", nil))
109116
assert.EqualValues(t, "", actual.Domain, "Domain is empty because unset as a config option")

0 commit comments

Comments
 (0)