Skip to content

Commit bf6f27e

Browse files
CaptainStandbyaeneasrory-bot
authored
fix: re-issue outdated cookie in /whoami (#2598)
Closes #2562 Co-authored-by: aeneasr <[email protected]> Co-authored-by: ory-bot <[email protected]>
1 parent 1d7381a commit bf6f27e

File tree

4 files changed

+111
-5
lines changed

4 files changed

+111
-5
lines changed

session/handler.go

+6
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P
199199
// Set userId as the X-Kratos-Authenticated-Identity-Id header.
200200
w.Header().Set("X-Kratos-Authenticated-Identity-Id", s.Identity.ID.String())
201201

202+
if err := h.r.SessionManager().RefreshCookie(r.Context(), w, r, s); err != nil {
203+
h.r.Audit().WithRequest(r).WithError(err).Info("Could not re-issue cookie.")
204+
h.r.Writer().WriteError(w, r, err)
205+
return
206+
}
207+
202208
h.r.Writer().Write(w, r, s)
203209
}
204210

session/handler_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,54 @@ func TestHandlerSelfServiceSessionManagement(t *testing.T) {
635635
assert.Equal(t, http.StatusNoContent, resp.StatusCode, "case=%d", j)
636636
}
637637
})
638+
639+
t.Run("case=whoami should not issue cookie for up to date session", func(t *testing.T) {
640+
client, _, _ := setup(t)
641+
642+
req, _ := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil)
643+
resp, err := client.Do(req)
644+
require.NoError(t, err)
645+
assert.Equal(t, http.StatusOK, resp.StatusCode)
646+
647+
assert.Empty(t, resp.Cookies())
648+
})
649+
650+
t.Run("case=whoami should reissue cookie for outdated session", func(t *testing.T) {
651+
client, _, session := setup(t)
652+
oldExpires := session.ExpiresAt
653+
654+
session.ExpiresAt = time.Now().Add(time.Hour * 24 * 30).UTC().Round(time.Hour)
655+
err := reg.SessionPersister().UpsertSession(context.Background(), session)
656+
require.NoError(t, err)
657+
658+
resp, err := client.Get(ts.URL + "/sessions/whoami")
659+
require.NoError(t, err)
660+
assert.Equal(t, http.StatusOK, resp.StatusCode)
661+
662+
require.Len(t, resp.Cookies(), 1)
663+
for _, c := range resp.Cookies() {
664+
assert.WithinDuration(t, session.ExpiresAt, c.Expires, 5*time.Second, "Ensure the expiry does not deviate +- 5 seconds from the expiry of the session for cookie: %s", c.Name)
665+
assert.NotEqual(t, oldExpires, c.Expires, "%s", c.Name)
666+
}
667+
})
668+
669+
t.Run("case=whoami should not issue cookie if request is token based", func(t *testing.T) {
670+
_, _, session := setup(t)
671+
672+
session.ExpiresAt = time.Now().Add(time.Hour * 24 * 30).UTC().Round(time.Hour)
673+
err := reg.SessionPersister().UpsertSession(context.Background(), session)
674+
require.NoError(t, err)
675+
676+
req, err := http.NewRequest("GET", ts.URL+"/sessions/whoami", nil)
677+
require.NoError(t, err)
678+
req.Header.Set("Authorization", "Bearer "+session.Token)
679+
680+
resp, err := http.DefaultClient.Do(req)
681+
require.NoError(t, err)
682+
assert.Equal(t, http.StatusOK, resp.StatusCode)
683+
684+
require.Len(t, resp.Cookies(), 0)
685+
})
638686
}
639687

640688
func TestHandlerRefreshSessionBySessionID(t *testing.T) {

session/manager.go

+3
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ type Manager interface {
9191
// Also regenerates CSRF tokens due to assumed principal change.
9292
IssueCookie(context.Context, http.ResponseWriter, *http.Request, *Session) error
9393

94+
// RefreshCookie checks if the request uses an outdated cookie and refreshes the cookie if needed.
95+
RefreshCookie(context.Context, http.ResponseWriter, *http.Request, *Session) error
96+
9497
// FetchFromRequest creates an HTTP session using cookies.
9598
FetchFromRequest(context.Context, *http.Request) (*Session, error)
9699

session/manager_http.go

+54-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"context"
55
"net/http"
66
"net/url"
7+
"time"
8+
9+
"github.com/gorilla/sessions"
710

811
"github.com/ory/x/urlx"
912

@@ -58,6 +61,29 @@ func (s *ManagerHTTP) UpsertAndIssueCookie(ctx context.Context, w http.ResponseW
5861
return nil
5962
}
6063

64+
func (s *ManagerHTTP) RefreshCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) error {
65+
// If it is a session token there is nothing to do.
66+
cookieHeader := r.Header.Get("X-Session-Cookie")
67+
_, cookieErr := r.Cookie(s.cookieName(r.Context()))
68+
if len(cookieHeader) == 0 && errors.Is(cookieErr, http.ErrNoCookie) {
69+
return nil
70+
}
71+
72+
cookie, err := s.getCookie(r)
73+
if err != nil {
74+
return err
75+
}
76+
77+
expiresAt := getCookieExpiry(cookie)
78+
if expiresAt == nil || expiresAt.Before(session.ExpiresAt) {
79+
if err := s.IssueCookie(ctx, w, r, session); err != nil {
80+
return err
81+
}
82+
}
83+
84+
return nil
85+
}
86+
6187
func (s *ManagerHTTP) IssueCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) error {
6288
cookie, err := s.r.CookieManager(r.Context()).Get(r, s.cookieName(ctx))
6389
// Fix for https://github.com/ory/kratos/issues/1695
@@ -94,28 +120,51 @@ func (s *ManagerHTTP) IssueCookie(ctx context.Context, w http.ResponseWriter, r
94120

95121
cookie.Options.MaxAge = 0
96122
if s.r.Config(ctx).SessionPersistentCookie() {
97-
cookie.Options.MaxAge = int(s.r.Config(ctx).SessionLifespan().Seconds())
123+
if session.ExpiresAt.IsZero() {
124+
cookie.Options.MaxAge = int(s.r.Config(ctx).SessionLifespan().Seconds())
125+
} else {
126+
cookie.Options.MaxAge = int(time.Until(session.ExpiresAt).Seconds())
127+
}
98128
}
99129

100130
cookie.Values["session_token"] = session.Token
131+
cookie.Values["expires_at"] = session.ExpiresAt.UTC().Format(time.RFC3339Nano)
132+
101133
if err := cookie.Save(r, w); err != nil {
102134
return errors.WithStack(err)
103135
}
104136
return nil
105137
}
106138

107-
func (s *ManagerHTTP) extractToken(r *http.Request) string {
108-
if token := r.Header.Get("X-Session-Token"); len(token) > 0 {
109-
return token
139+
func getCookieExpiry(s *sessions.Session) *time.Time {
140+
expiresAt, ok := s.Values["expires_at"].(string)
141+
if !ok {
142+
return nil
110143
}
111144

145+
n, err := time.Parse(time.RFC3339Nano, expiresAt)
146+
if err != nil {
147+
return nil
148+
}
149+
return &n
150+
}
151+
152+
func (s *ManagerHTTP) getCookie(r *http.Request) (*sessions.Session, error) {
112153
if cookie := r.Header.Get("X-Session-Cookie"); len(cookie) > 0 {
113154
rr := *r
114155
r = &rr
115156
r.Header = http.Header{"Cookie": []string{s.cookieName(r.Context()) + "=" + cookie}}
116157
}
117158

118-
cookie, err := s.r.CookieManager(r.Context()).Get(r, s.cookieName(r.Context()))
159+
return s.r.CookieManager(r.Context()).Get(r, s.cookieName(r.Context()))
160+
}
161+
162+
func (s *ManagerHTTP) extractToken(r *http.Request) string {
163+
if token := r.Header.Get("X-Session-Token"); len(token) > 0 {
164+
return token
165+
}
166+
167+
cookie, err := s.getCookie(r)
119168
if err != nil {
120169
token, _ := bearerTokenFromRequest(r)
121170
return token

0 commit comments

Comments
 (0)