Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor RefreshSessionIfNeeded to RefreshSession #856

Closed
NickMeves opened this issue Oct 20, 2020 · 19 comments
Closed

Refactor RefreshSessionIfNeeded to RefreshSession #856

NickMeves opened this issue Oct 20, 2020 · 19 comments

Comments

@NickMeves
Copy link
Member

Currently, when we are passed the cookie-refresh TTL, we only refresh the session if the Access or ID Token is expired (depending on which the provider uses for the ExpiresOn field).

This leads to a lot of issues on the border of the expiration - where a session might for a split second be valid and pass at the proxy layer, and pass an Access or ID Token to the upstream that will be expired by the time it gets there.

We should support opportunistic ability to refresh tokens/sessions ahead of expiration (E.g. 80% of token lifespan).

Expected Behavior

Our RefreshSessions provider methods make no judgments on when to refresh. They will refresh if called.

This gives more control to the end user to set the time appropriately with cookie-refresh.

Current Behavior

Sessions only refresh if they are already expired.

Possible Solution

This logic throughout providers ditches the ExpiresOn checks:

if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {

This global manager that handles this at the middleware level will also need to change:

https://github.com/oauth2-proxy/oauth2-proxy/blob/3fa42edb7350219d317c4bd47faf5da6192dc70f/pkg/middleware/stored_session.go

I lean toward if we want any logic about only at 80% of greater of the life of a session, we do that in stored_session.go (as a safety precaution for people setting overeager refresh times that result in refresh requests every request).

@JoelSpeed
Copy link
Member

Agreed, we should have the decision and logic on when to refresh centrally and then have the providers implement the refreshing.

I wonder if we can interpret somehow the config so that a provider knows that is has been configured for refreshing? If so, it could expose a method RefreshEnabled that returns a bool that would allow stored_session.go to know whether it should even try to attempt refreshing the session or not 🤔 Maybe overthinking this

@arcivanov
Copy link
Contributor

arcivanov commented Oct 28, 2020

Actually it looks to me that the cookie is never refreshed when the token expires, or at least is not refreshed via the /auth endpoint.

Case in point - the id_token on Keycloak by default only lives 5 minutes. After five minutes with pretty much eternal cookie the id_token returned by oauth2-proxy is expired and the session does not change.

@arcivanov
Copy link
Contributor

FYI, I'm working on a solution, will submit today, hopefully.

arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 2, 2020
Currently when a OIDC tokens expire the session cookie is not refreshed by default resulting in proxy providing a useless stale token.
The cookie_refresh mode that is based on TTL is largely useless as that TTL needs to be synchronized with settings in OIDC provider, which also makes it largely unusable.

Thus, an additional automatic cookie refresh mode is added as follows:

    cookie_refresh = -1
    cookie_refresh_grace_pcnt = 85

When cookie_refresh is set to -1 (default is still 0 for disabled to preserve backwards compatibility) every cookie refresh will undergo a check through the provider to see if the cookie needs to be refreshed based on token expiry. The refresh grace percentage (default to 100%) is then used to determine when to set the expiry. The grace percentage of 100% means that the token/cookie will be refreshed at 100% of the expiration period. As in the example above setting it to any other number X between 1 - 99 will refresh the token after X% of the token lifetime has expired.

As an additional piece of logic, the expiry time for OIDC provider is now set to the earliest of ID Token and Access Token expiration.

A bonus fix - when selecting a provider, the choice is now printed and a warning is issued if provider name is not recognized (I got burned by this personally not realizing "openidc" is not "oidc" and there would be a default to "google").

Lastly, architecturally, the APIs for providers now carry ProxyState as a parameter, granting providers access to relevant common configuration. I only threw there what I needed for this.

fixes oauth2-proxy#856
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 3, 2020
Currently when a OIDC tokens expire the session cookie is not refreshed by default resulting in proxy providing a useless stale token.
The cookie_refresh mode that is based on TTL is largely useless as that TTL needs to be synchronized with settings in OIDC provider, which also makes it largely unusable.

Thus, an additional automatic cookie refresh mode is added as follows:

    cookie_refresh = -1
    cookie_refresh_grace_pcnt = 85

When cookie_refresh is set to -1 (default is still 0 for disabled to preserve backwards compatibility) every cookie refresh will undergo a check through the provider to see if the cookie needs to be refreshed based on token expiry. The refresh grace percentage (default to 100%) is then used to determine when to set the expiry. The grace percentage of 100% means that the token/cookie will be refreshed at 100% of the expiration period. As in the example above setting it to any other number X between 1 - 99 will refresh the token after X% of the token lifetime has expired.

As an additional piece of logic, the expiry time for OIDC provider is now set to the earliest of ID Token and Access Token expiration.

A bonus fix - when selecting a provider, the choice is now printed and a warning is issued if provider name is not recognized (I got burned by this personally not realizing "openidc" is not "oidc" and there would be a default to "google").

Lastly, architecturally, the APIs for providers now carry ProxyState as a parameter, granting providers access to relevant common configuration. I only threw there what I needed for this.

fixes oauth2-proxy#856
@NickMeves NickMeves linked a pull request Nov 3, 2020 that will close this issue
3 tasks
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 12, 2020
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 12, 2020
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 13, 2020
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 13, 2020
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Nov 13, 2020
@Jc2k
Copy link

Jc2k commented Nov 13, 2020

Have been bitten by this. Currently trying out #915 and it seems to be working for my needs so far. Cheers @arcivanov !

arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Dec 5, 2020
arcivanov added a commit to arcivanov/oauth2-proxy that referenced this issue Dec 5, 2020
@NickMeves
Copy link
Member Author

So in looking over #946 I think I caught a very subtle but very dangerous interaction between RefreshSession & ValidateSession that might cause nasty bugs in the future: #946 (comment)

I'm thinking it might be prudent for us to completely decouple the Refresh & Validate into two separate timers that users can controls as they wish (session-refresh & session-validate). This will let users fully control if they want earlier session validation for providers who under the hood would make a network request to a validateURL

@NickMeves
Copy link
Member Author

Related - I think the decoupled manner we manage the underlying session fields (via public fields across disparate middleware & provider implementations) is ripe for confusion and bugs. We've seen many times where contributors add refresh logic to a provider and don't manage the time settings properly.

So I think we should refactor these to being owned by the SessionState so it can fully control timing logic (and potentially add its own LastRefreshed & LastValidated timers).

Something like this:

func (s *SessionState) Refresh(ctx context.Context, p *provider.Provider) error {
  # Do stuff with p.RefreshSession(ctx, s)
  
  # Then forcing timing sync logix now, e.g. something like
  s.CreatedAt = time.Now()
}

To help centralize the disparate logic between individual providers and here:

func (s *storedSessionLoader) refreshSessionIfNeeded(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) error {

AND

func (s *SessionState) Validate(ctx context.Context, p *provider.Provider) error

For this:

func (s *storedSessionLoader) validateSession(ctx context.Context, session *sessionsapi.SessionState) error {

@github-actions
Copy link
Contributor

github-actions bot commented Feb 4, 2021

This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.

@Bibob7
Copy link
Contributor

Bibob7 commented Mar 21, 2021

This issue is still relevant for me!

@arcivanov
Copy link
Contributor

@Bibob7 Try #915 as it works for most people.

@lathspell
Copy link
Contributor

A quick and dirty but at least minimal invasive hack that also refreshes the OIDC Token 30s before expiration. Forked from 2021-04-03 master:

diff --git a/providers/oidc.go b/providers/oidc.go
index df133f4d..47f5c52e 100644
--- a/providers/oidc.go
+++ b/providers/oidc.go
@@ -107,16 +107,25 @@ func (p *OIDCProvider) enrichFromProfileURL(ctx context.Context, s *sessions.Ses
 	return nil
 }
 
+const RefreshTokenOffset = 30 * time.Second
+
 // ValidateSession checks that the session's IDToken is still valid
 func (p *OIDCProvider) ValidateSession(ctx context.Context, s *sessions.SessionState) bool {
 	_, err := p.Verifier.Verify(ctx, s.IDToken)
+
+	if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.Sub(time.Now()) < RefreshTokenOffset) || s.RefreshToken == "" {
+		logger.Printf("session will expire is less than 30 seconds: %s", s)
+		return false
+	}
+
 	return err == nil
 }
 
 // RefreshSessionIfNeeded checks if the session has expired and uses the
 // RefreshToken to fetch a new Access Token (and optional ID token) if required
 func (p *OIDCProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
-	if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {
+
+	if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.Sub(time.Now()) > RefreshTokenOffset) || s.RefreshToken == "" {
 		return false, nil
 	}
 

@pierluigilenoci
Copy link
Contributor

@NickMeves any news about this?

@NickMeves
Copy link
Member Author

I have a draft PR started, but I will be slow. Job commitments at the moment are drastically limiting the time I have to work on OAuth2 Proxy.

@pierluigilenoci
Copy link
Contributor

@NickMeves thanks anyway! ❤️

@krin-wang
Copy link

@NickMeves Thank you for your replies. Is this issue still on track?

@NickMeves
Copy link
Member Author

I plan to dive back into this draft and reassess this weekend or next: #1086

We merged a prereq I wanted to get in place for this refactor to manage time throughout our codebase in a more streamlined way, so it shouldn't have any blockers besides me getting back into the flow of OAuth2 Proxy now that I'm onboarded at my new job.

@github-actions
Copy link
Contributor

This issue has been inactive for 60 days. If the issue is still relevant please comment to re-activate the issue. If no action is taken within 7 days, the issue will be marked closed.

@github-actions github-actions bot added the Stale label Aug 11, 2021
@gysel
Copy link

gysel commented Aug 11, 2021

This issue is still relevant to us.

@pierluigilenoci
Copy link
Contributor

pierluigilenoci commented Aug 11, 2021

@gysel the PR #1086 is merged in master since June.
You need to wait for the next release but it is close to coming! 😄

@NickMeves make sense for you to close this issue?

@JoelSpeed JoelSpeed removed the Stale label Aug 11, 2021
@NickMeves
Copy link
Member Author

Fixed in #1086

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment