git.iko.soy/typeable/oauth2-proxy
on master help sign in
Manage session time fields centrally
7fa6d2dNick Meves
oauthproxy.go+12−3
@@ -786,6 +786,15 @@ func (p *OAuthProxy) redeemCode(req *http.Request) (*sessionsapi.SessionState, e
786 if err != nil {
786 if err != nil {
787 return nil, err
787 return nil, err
788 }
788 }
789+
790+ // Force setting these in case the Provider didn't
791+ if s.CreatedAt == nil {
792+ s.CreatedAtNow()
793+ }
794+ if s.ExpiresOn == nil {
795+ s.ExpiresIn(p.CookieOptions.Expire)
796+ }
797+
789 return s, nil
798 return s, nil
790}
799}
791
800
@@ -861,9 +870,9 @@ func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
861
870
862// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
871// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
863var noCacheHeaders = map[string]string{
872var noCacheHeaders = map[string]string{
864− "Expires": time.Unix(0, 0).Format(time.RFC1123),
873+ "Expires": time.Unix(0, 0).Format(time.RFC1123),
865− "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
874+ "Cache-Control": "no-cache, no-store, must-revalidate, max-age=0",
866− "X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
875+ "X-Accel-Expire": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
867}
876}
868
877
869// prepareNoCache prepares headers for preventing browser caching.
878// prepareNoCache prepares headers for preventing browser caching.
pkg/apis/sessions/session_state.go+26−3
@@ -11,6 +11,7 @@ import (
11 "time"
11 "time"
12 "unicode/utf8"
12 "unicode/utf8"
13
13
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/clock"
14 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
15 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
15 "github.com/pierrec/lz4"
16 "github.com/pierrec/lz4"
16 "github.com/vmihailenco/msgpack/v4"
17 "github.com/vmihailenco/msgpack/v4"
@@ -32,7 +33,8 @@ type SessionState struct {
32 Groups []string `msgpack:"g,omitempty"`
33 Groups []string `msgpack:"g,omitempty"`
33 PreferredUsername string `msgpack:"pu,omitempty"`
34 PreferredUsername string `msgpack:"pu,omitempty"`
34
35
35− Lock Lock `msgpack:"-"`
36+ Clock clock.Clock `msgpack:"-"`
37+ Lock Lock `msgpack:"-"`
36}
38}
37
39
38func (s *SessionState) ObtainLock(ctx context.Context, expiration time.Duration) error {
40func (s *SessionState) ObtainLock(ctx context.Context, expiration time.Duration) error {
@@ -63,9 +65,30 @@ func (s *SessionState) PeekLock(ctx context.Context) (bool, error) {
63 return s.Lock.Peek(ctx)
65 return s.Lock.Peek(ctx)
64}
66}
65
67
68+// CreatedAtNow sets a SessionState's CreatedAt to now
69+func (s *SessionState) CreatedAtNow() {
70+ now := s.Clock.Now()
71+ s.CreatedAt = &now
72+}
73+
74+// SetExpiresOn sets an expiration
75+func (s *SessionState) SetExpiresOn(exp time.Time) {
76+ s.ExpiresOn = &exp
77+}
78+
79+// ExpiresIn sets an expiration a certain duration from CreatedAt.
80+// CreatedAt will be set to time.Now if it is unset.
81+func (s *SessionState) ExpiresIn(d time.Duration) {
82+ if s.CreatedAt == nil {
83+ s.CreatedAtNow()
84+ }
85+ exp := s.CreatedAt.Add(d)
86+ s.ExpiresOn = &exp
87+}
88+
66// IsExpired checks whether the session has expired
89// IsExpired checks whether the session has expired
67func (s *SessionState) IsExpired() bool {
90func (s *SessionState) IsExpired() bool {
68− if s.ExpiresOn != nil && !s.ExpiresOn.IsZero() && s.ExpiresOn.Before(time.Now()) {
91+ if s.ExpiresOn != nil && !s.ExpiresOn.IsZero() && s.ExpiresOn.Before(s.Clock.Now()) {
69 return true
92 return true
70 }
93 }
71 return false
94 return false
@@ -74,7 +97,7 @@ func (s *SessionState) IsExpired() bool {
74// Age returns the age of a session
97// Age returns the age of a session
75func (s *SessionState) Age() time.Duration {
98func (s *SessionState) Age() time.Duration {
76 if s.CreatedAt != nil && !s.CreatedAt.IsZero() {
99 if s.CreatedAt != nil && !s.CreatedAt.IsZero() {
77− return time.Now().Truncate(time.Second).Sub(*s.CreatedAt)
100+ return s.Clock.Now().Truncate(time.Second).Sub(*s.CreatedAt)
78 }
101 }
79 return 0
102 return 0
80}
103}
pkg/middleware/stored_session.go+1−2
@@ -142,8 +142,7 @@ func (s *storedSessionLoader) refreshSession(rw http.ResponseWriter, req *http.R
142 }
142 }
143
143
144 // If we refreshed, update the `CreatedAt` time to reset the refresh timer
144 // If we refreshed, update the `CreatedAt` time to reset the refresh timer
145− // TODO: Implement
146− // session.CreatedAtNow()
145+ session.CreatedAtNow()
147
146
148 // Because the session was refreshed, make sure to save it
147 // Because the session was refreshed, make sure to save it
149 err = s.store.Save(rw, req, session)
148 err = s.store.Save(rw, req, session)
pkg/sessions/cookie/session_store.go+1−2
@@ -36,8 +36,7 @@ type SessionStore struct {
36// within Cookies set on the HTTP response writer
36// within Cookies set on the HTTP response writer
37func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
37func (s *SessionStore) Save(rw http.ResponseWriter, req *http.Request, ss *sessions.SessionState) error {
38 if ss.CreatedAt == nil || ss.CreatedAt.IsZero() {
38 if ss.CreatedAt == nil || ss.CreatedAt.IsZero() {
39− now := time.Now()
39+ ss.CreatedAtNow()
40− ss.CreatedAt = &now
41 }
40 }
42 value, err := s.cookieForSession(ss)
41 value, err := s.cookieForSession(ss)
43 if err != nil {
42 if err != nil {
pkg/sessions/persistence/manager.go+1−2
@@ -30,8 +30,7 @@ func NewManager(store Store, cookieOpts *options.Cookie) *Manager {
30// from the persistent data store.
30// from the persistent data store.
31func (m *Manager) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
31func (m *Manager) Save(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
32 if s.CreatedAt == nil || s.CreatedAt.IsZero() {
32 if s.CreatedAt == nil || s.CreatedAt.IsZero() {
33− now := time.Now()
33+ s.CreatedAtNow()
34− s.CreatedAt = &now
35 }
34 }
36
35
37 tckt, err := decodeTicketFromRequest(req, m.Options)
36 tckt, err := decodeTicketFromRequest(req, m.Options)
providers/azure.go+12−18
@@ -142,16 +142,13 @@ func (p *AzureProvider) Redeem(ctx context.Context, redirectURL, code string) (*
142 return nil, err
142 return nil, err
143 }
143 }
144
144
145− created := time.Now()
146− expires := time.Unix(jsonResponse.ExpiresOn, 0)
147−
148 session := &sessions.SessionState{
145 session := &sessions.SessionState{
149 AccessToken: jsonResponse.AccessToken,
146 AccessToken: jsonResponse.AccessToken,
150 IDToken: jsonResponse.IDToken,
147 IDToken: jsonResponse.IDToken,
151− CreatedAt: &created,
152− ExpiresOn: &expires,
153 RefreshToken: jsonResponse.RefreshToken,
148 RefreshToken: jsonResponse.RefreshToken,
154 }
149 }
150+ session.CreatedAtNow()
151+ session.SetExpiresOn(time.Unix(jsonResponse.ExpiresOn, 0))
155
152
156 email, err := p.verifyTokenAndExtractEmail(ctx, session.IDToken)
153 email, err := p.verifyTokenAndExtractEmail(ctx, session.IDToken)
157
154
@@ -239,10 +236,9 @@ func (p *AzureProvider) verifyTokenAndExtractEmail(ctx context.Context, token st
239 return email, nil
236 return email, nil
240}
237}
241
238
242−// RefreshSessionIfNeeded checks if the session has expired and uses the
243−// RefreshToken to fetch a new ID token if required
239+// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
244−func (p *AzureProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
240+func (p *AzureProvider) RefreshSession(ctx context.Context, s *sessions.SessionState) (bool, error) {
245− if s == nil || s.ExpiresOn.After(time.Now()) || s.RefreshToken == "" {
241+ if s == nil || s.RefreshToken == "" {
246 return false, nil
242 return false, nil
247 }
243 }
248
244
@@ -257,7 +253,7 @@ func (p *AzureProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.
257 return true, nil
253 return true, nil
258}
254}
259
255
260−func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) (err error) {
256+func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
261 params := url.Values{}
257 params := url.Values{}
262 params.Add("client_id", p.ClientID)
258 params.Add("client_id", p.ClientID)
263 params.Add("client_secret", p.ClientSecret)
259 params.Add("client_secret", p.ClientSecret)
@@ -271,25 +267,23 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
271 IDToken string `json:"id_token"`
267 IDToken string `json:"id_token"`
272 }
268 }
273
269
274− err = requests.New(p.RedeemURL.String()).
270+ err := requests.New(p.RedeemURL.String()).
275 WithContext(ctx).
271 WithContext(ctx).
276 WithMethod("POST").
272 WithMethod("POST").
277 WithBody(bytes.NewBufferString(params.Encode())).
273 WithBody(bytes.NewBufferString(params.Encode())).
278 SetHeader("Content-Type", "application/x-www-form-urlencoded").
274 SetHeader("Content-Type", "application/x-www-form-urlencoded").
279 Do().
275 Do().
280 UnmarshalInto(&jsonResponse)
276 UnmarshalInto(&jsonResponse)
281−
282 if err != nil {
277 if err != nil {
283− return
278+ return err
284 }
279 }
285
280
286− now := time.Now()
287− expires := time.Unix(jsonResponse.ExpiresOn, 0)
288 s.AccessToken = jsonResponse.AccessToken
281 s.AccessToken = jsonResponse.AccessToken
289 s.IDToken = jsonResponse.IDToken
282 s.IDToken = jsonResponse.IDToken
290 s.RefreshToken = jsonResponse.RefreshToken
283 s.RefreshToken = jsonResponse.RefreshToken
291− s.CreatedAt = &now
284+
292− s.ExpiresOn = &expires
285+ s.CreatedAtNow()
286+ s.SetExpiresOn(time.Unix(jsonResponse.ExpiresOn, 0))
293
287
294 email, err := p.verifyTokenAndExtractEmail(ctx, s.IDToken)
288 email, err := p.verifyTokenAndExtractEmail(ctx, s.IDToken)
295
289
@@ -312,7 +306,7 @@ func (p *AzureProvider) redeemRefreshToken(ctx context.Context, s *sessions.Sess
312 }
306 }
313 }
307 }
314
308
315− return
309+ return nil
316}
310}
317
311
318func makeAzureHeader(accessToken string) http.Header {
312func makeAzureHeader(accessToken string) http.Header {
providers/gitlab.go+7−5
@@ -259,14 +259,16 @@ func (p *GitLabProvider) createSession(ctx context.Context, token *oauth2.Token)
259 }
259 }
260 }
260 }
261
261
262− created := time.Now()
263− return &sessions.SessionState{
262+ ss := &sessions.SessionState{
264 AccessToken: token.AccessToken,
263 AccessToken: token.AccessToken,
265 IDToken: getIDToken(token),
264 IDToken: getIDToken(token),
266 RefreshToken: token.RefreshToken,
265 RefreshToken: token.RefreshToken,
267− CreatedAt: &created,
266+ }
267+
268+ ss.CreatedAtNow()
268− ExpiresOn: &idToken.Expiry,
269+ ss.SetExpiresOn(idToken.Expiry)
270+
269− }, nil
271+ return ss, nil
270}
272}
271
273
272// ValidateSession checks that the session's IDToken is still valid
274// ValidateSession checks that the session's IDToken is still valid
providers/google.go+12−13
@@ -163,23 +163,22 @@ func (p *GoogleProvider) Redeem(ctx context.Context, redirectURL, code string) (
163 return nil, err
163 return nil, err
164 }
164 }
165
165
166− created := time.Now()
167− expires := time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second)
168−
169− return &sessions.SessionState{
166+ ss := &sessions.SessionState{
170 AccessToken: jsonResponse.AccessToken,
167 AccessToken: jsonResponse.AccessToken,
171 IDToken: jsonResponse.IDToken,
168 IDToken: jsonResponse.IDToken,
172− CreatedAt: &created,
173− ExpiresOn: &expires,
174 RefreshToken: jsonResponse.RefreshToken,
169 RefreshToken: jsonResponse.RefreshToken,
175 Email: c.Email,
170 Email: c.Email,
176 User: c.Subject,
171 User: c.Subject,
172+ }
173+ ss.CreatedAtNow()
174+ ss.ExpiresIn(time.Duration(jsonResponse.ExpiresIn) * time.Second)
175+
177− }, nil
176+ return ss, nil
178}
177}
179
178
180// EnrichSession checks the listed Google Groups configured and adds any
179// EnrichSession checks the listed Google Groups configured and adds any
181// that the user is a member of to session.Groups.
180// that the user is a member of to session.Groups.
182−func (p *GoogleProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
181+func (p *GoogleProvider) EnrichSession(_ context.Context, s *sessions.SessionState) error {
183 // TODO (@NickMeves) - Move to pure EnrichSession logic and stop
182 // TODO (@NickMeves) - Move to pure EnrichSession logic and stop
184 // reusing legacy `groupValidator`.
183 // reusing legacy `groupValidator`.
185 //
184 //
@@ -272,7 +271,7 @@ func (p *GoogleProvider) RefreshSession(ctx context.Context, s *sessions.Session
272 return false, nil
271 return false, nil
273 }
272 }
274
273
275− newToken, newIDToken, duration, err := p.redeemRefreshToken(ctx, s.RefreshToken)
274+ newToken, newIDToken, ttl, err := p.redeemRefreshToken(ctx, s.RefreshToken)
276 if err != nil {
275 if err != nil {
277 return false, err
276 return false, err
278 }
277 }
@@ -285,12 +284,12 @@ func (p *GoogleProvider) RefreshSession(ctx context.Context, s *sessions.Session
285 return false, fmt.Errorf("%s is no longer in the group(s)", s.Email)
284 return false, fmt.Errorf("%s is no longer in the group(s)", s.Email)
286 }
285 }
287
286
288− origExpiration := s.ExpiresOn
289− expires := time.Now().Add(duration).Truncate(time.Second)
290 s.AccessToken = newToken
287 s.AccessToken = newToken
291 s.IDToken = newIDToken
288 s.IDToken = newIDToken
292− s.ExpiresOn = &expires
289+
293− logger.Printf("refreshed access token %s (expired on %s)", s, origExpiration)
290+ s.CreatedAtNow()
291+ s.ExpiresIn(ttl)
292+
294 return true, nil
293 return true, nil
295}
294}
296
295
providers/logingov.go+8−9
@@ -159,7 +159,7 @@ func emailFromUserInfo(ctx context.Context, accessToken string, userInfoEndpoint
159}
159}
160
160
161// Redeem exchanges the OAuth2 authentication token for an ID token
161// Redeem exchanges the OAuth2 authentication token for an ID token
162−func (p *LoginGovProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
162+func (p *LoginGovProvider) Redeem(ctx context.Context, _, code string) (*sessions.SessionState, error) {
163 if code == "" {
163 if code == "" {
164 return nil, ErrMissingCode
164 return nil, ErrMissingCode
165 }
165 }
@@ -214,17 +214,16 @@ func (p *LoginGovProvider) Redeem(ctx context.Context, redirectURL, code string)
214 return nil, err
214 return nil, err
215 }
215 }
216
216
217− created := time.Now()
218− expires := time.Now().Add(time.Duration(jsonResponse.ExpiresIn) * time.Second).Truncate(time.Second)
219−
220− // Store the data that we found in the session state
221− return &sessions.SessionState{
217+ session := &sessions.SessionState{
222 AccessToken: jsonResponse.AccessToken,
218 AccessToken: jsonResponse.AccessToken,
223 IDToken: jsonResponse.IDToken,
219 IDToken: jsonResponse.IDToken,
224− CreatedAt: &created,
225− ExpiresOn: &expires,
226 Email: email,
220 Email: email,
227− }, nil
221+ }
222+
223+ session.CreatedAtNow()
224+ session.ExpiresIn(time.Duration(jsonResponse.ExpiresIn) * time.Second)
225+
226+ return session, nil
228}
227}
229
228
230// GetLoginURL overrides GetLoginURL to add login.gov parameters
229// GetLoginURL overrides GetLoginURL to add login.gov parameters
providers/oidc.go+5−4
@@ -226,7 +226,9 @@ func (p *OIDCProvider) CreateSessionFromToken(ctx context.Context, token string)
226 ss.AccessToken = token
226 ss.AccessToken = token
227 ss.IDToken = token
227 ss.IDToken = token
228 ss.RefreshToken = ""
228 ss.RefreshToken = ""
229+
230+ ss.CreatedAtNow()
229− ss.ExpiresOn = &idToken.Expiry
231+ ss.SetExpiresOn(idToken.Expiry)
230
232
231 return ss, nil
233 return ss, nil
232}
234}
@@ -256,9 +258,8 @@ func (p *OIDCProvider) createSession(ctx context.Context, token *oauth2.Token, r
256 ss.RefreshToken = token.RefreshToken
258 ss.RefreshToken = token.RefreshToken
257 ss.IDToken = getIDToken(token)
259 ss.IDToken = getIDToken(token)
258
260
259− created := time.Now()
261+ ss.CreatedAtNow()
260− ss.CreatedAt = &created
261− ss.ExpiresOn = &token.Expiry
262+ ss.SetExpiresOn(token.Expiry)
262
263
263 return ss, nil
264 return ss, nil
264}
265}
providers/provider_default.go+6−3
@@ -6,7 +6,6 @@ import (
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8 "net/url"
8 "net/url"
9− "time"
10
9
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
10 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/middleware"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
@@ -85,9 +84,13 @@ func (p *ProviderData) Redeem(ctx context.Context, redirectURL, code string) (*s
85 if err != nil {
84 if err != nil {
86 return nil, err
85 return nil, err
87 }
86 }
87+ // TODO (@NickMeves): Uses OAuth `expires_in` to set an expiration
88 if token := values.Get("access_token"); token != "" {
88 if token := values.Get("access_token"); token != "" {
89− created := time.Now()
90− return &sessions.SessionState{AccessToken: token, CreatedAt: &created}, nil
89+ ss := &sessions.SessionState{
90+ AccessToken: token,
91+ }
92+ ss.CreatedAtNow()
93+ return ss, nil
91 }
94 }
92
95
93 return nil, fmt.Errorf("no access token found %s", result.Body())
96 return nil, fmt.Errorf("no access token found %s", result.Body())