Skip to content

Conversation

@NickMeves
Copy link
Contributor

@NickMeves NickMeves commented Dec 25, 2020

#966

Implements OIDC Nonce setting:

  • A nonce is always created and sent as a parameter to all provider's GetLoginURL (even if they don't use it).
  • Once a session is created, the nonce is added to session.Nonce for any providers to use during the lifespan of the session to validate the nonce.

In the initial OAuth flow, after LoginURL, Redeem, EnrichSession - ValidateSession is now called as well (for all providers). For OIDC, this allows a final check on the ID Token nonce claim before minting the session as valid.

The --insecure-oidc-skip-nonce flag is true by default for now in case any existing OIDC IdPs don't support it properly.

CSRF management is refactored to its own struct in cookies package. It handles both the OAuth2 state nonce & the OIDC nonce. The cookie value is now additionally signed and verified before any CSRF checks with the OAuth2 callback or OIDC ID Token nonce claim.

Any nonce checks use hmac.Equal for constant time comparisons to not leak time details.

(Slight Scope Creep) Unit tests added to the cookies package for existing code in addition to new CSRF code.

Checklist:

  • My change requires a change to the documentation or CHANGELOG.
  • I have updated the documentation/CHANGELOG accordingly.
  • I have created a feature (non-master) branch for my PR.

@NickMeves NickMeves added enhancement WIP Work in progress labels Dec 25, 2020
@NickMeves
Copy link
Contributor Author

I'm pretty sure I did this wrong and the nonce parameter needs to be on the initial authentication request and not the second token request.

I don't think I have a session available to sync the nonce at that earlier stage, I'll likely need to do something similar to the state tracking for OAuth in the CSRF cookie 🤔

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from e99479d to e7e6e5d Compare December 26, 2020 21:21
@NickMeves
Copy link
Contributor Author

Ok all fixed up. A unique OIDC nonce (distinct from the OAuth state nonce for added security benefits) is set in the LoginURL and persisted between calls in the CSRF token adjacent to the OAuth state nonce.

CSRF tracking logic could potentially use some refactoring now that it is more complex -- get it out of OAuthProxy and into its own package.

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch 3 times, most recently from 021c8d2 to 8d0ac72 Compare December 29, 2020 22:58
@NickMeves
Copy link
Contributor Author

This is in a pre-reviewable state now. I just need to write tests for the new CSRF code now in the cookie package.

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from 8d0ac72 to cf6e269 Compare December 31, 2020 00:05
@NickMeves NickMeves removed the WIP Work in progress label Dec 31, 2020
@NickMeves
Copy link
Contributor Author

@JoelSpeed The code is ready for a review.

I want to find time to hook this into a few test OIDC instances to manually peek at the ID Token and see the nonce claim. I especially want to verify how it behaves with a fresh ID Token after a token refresh.

@NickMeves NickMeves marked this pull request as ready for review December 31, 2020 04:40
@NickMeves NickMeves requested a review from a team as a code owner December 31, 2020 04:40
@NickMeves NickMeves force-pushed the oidc-nonce-validation branch 2 times, most recently from b912e65 to 2f8eb7e Compare January 1, 2021 01:24
Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! Nothing major to report but found a few bits and have a few questions as I'm going through

Very pleased to be reusing the cookie options directly into the main package, there really was no reason for us to copy all those values over, thanks for that!

CookieRefresh time.Duration
CookieSameSite string
Validator func(string) bool
CookieOptions *options.Cookie
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh! I like this :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first step in a long journey of figuring out what the heck is going on in the OAuthProxy struct -- and why some fields are public and others private 😅

And why other fields even exist at all.

func LoadCSRFCookie(req *http.Request, opts *options.Cookie) (*CSRF, error) {
cookie, err := req.Cookie(csrfCookieName(opts))
if err != nil {
// Don't wrap this error to allow `err == http.ErrNoCookie` checks
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change err == http.ErrNoCookie to use errors.Is, then we can wrap to our hearts content!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this comment came over when refactoring from other cookie areas 🤔

I think the only place that cares about http.ErrNoCookie in our codebase is unrelated to this and is related to session store management.

Should we just remove this comment here you think?

@NickMeves
Copy link
Contributor Author

@JoelSpeed What are your thoughts on the ValidateSession usage in general in the project? This PR changes it to explicitly be called at the end of the initial authentication flow before we do a final Authorize on it (as the place to check the OIDC ID Token nonce claim).

I uncovered some existing issues with its usage and documented them here: #856 (comment)

My current thinking: we repurposes the usage of those functions in the project. For sure after the initial authentication request. Even for providers that have a network round trip in ValidateSession to a ValidateURL, that slowness doesn't matter at this stage.

The other point in time is immediately after a RefreshSession call where we now potentially have fresh Access & IDTokens in our session. While a call to ValidateSession might be nice to ensure security -- the network round trip in some providers might be an issuer here. The RefreshSession is already in the middle of a normal proxied request, so the RefreshSession RTT already adds a huge increase to the request proxying time.

Maybe if we decide validating is good there, we add a refresh bool argument to ValidateSession so providers can alter its behavior in the post RefreshSession flow? The OIDC Provider for instance would benefit from ValidateSession post RefreshSession because its implementation has no network calls and only checks token signatures and claim contents.

Either way -- I think the legacy usage of ValidateSession is broken if it is only called when RefreshSessionIfNeeded doesn't trigger. I think some providers that don't implement RefreshSessionIfNeeded but implement ValidateSession are unintentionally getting validate RTTs every request after the --cookie-refresh time if set has passed.


// CSRF manages various nonces stored in the CSRF cookie during the initial
// authentication flows.
type CSRF interface {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be under pkg/apis with some of the other interfaces?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends whether it's causing circular imports, I think it's fine here for now

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from ad7b823 to afca1a6 Compare February 9, 2021 03:04
@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from afca1a6 to 6414933 Compare February 20, 2021 01:17
@NickMeves
Copy link
Contributor Author

@JoelSpeed This is ready for a final pass. Just a couple new commits (1 alignment to 1.16, 1 bugfix).

I did manual testing with both Dex & OneLogin OIDC and confirmed the nonce claim is present in the ID Token in both the initial redeem & again after a refresh with the new refreshed ID Token.

@NickMeves NickMeves linked an issue Feb 20, 2021 that may be closed by this pull request
}
// MakeCookieFromOptions constructs a cookie based on the given *options.CookieOptions,
// value and creation time
func MakeCookieFromOptions(req *http.Request, name string, value string, opts *options.Cookie, expiration time.Duration, now time.Time) *http.Cookie {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future thought for a separate PR, but I wonder if we should create some cookie builder abstraction kind of like the request builder so that you don't have to pass it the basic options every time and you can just add the bits you want to override

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah - that's a great idea! I like it -- I remember trying to fight with this method to see if there was a way I could get its method parameter count down to cleanup that codeclimate finding and gave up 😅

"github.com/vmihailenco/msgpack/v4"
)

var now = time.Now
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to make this part of the csrf itself? With an internal method that allows it to be overridden? How have we done this elsewhere 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in a lot of other cases, we frequently passed in a now time.Time method argument following the dependency injection pattern that maybe lives on from its Java roots? I think in all cases we've done it, its been for time mocking in tests (this is the same case here, I just use it for cookie new expiration unit tests we hadn't had before).

As an alternative, I've tried this to expose time in tests while keeping the method singatures clean in normal code. For now I've left it private since this only has time manipulated by my cookies package unit tests.

After working with jwt-go here: https://github.com/oauth2-proxy/mockoidc/blob/65483e3cdabeaeb796f9180b6e4d5dd253d05985/mockoidc.go#L189

I see the huge advantage in potentially exporting TimeFunc in our various packages more. Allows us to simplify method signatures (removing the dummy now time.Now variables we have bloating them up) while giving access to time.Now to manipulate across packages (in case a sub-dependency in a test also needs a custom view of time).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoelSpeed I think this was the last piece of outstanding feedback in this PR? Did you have any final thoughts on this design?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still lean to creating a timeFunc member in the csrf struct, then rather than overriding a package level var, you override it just for the instance of csrf.

I did the following recently when I needed to mock time in my day job, WDYT?

type csrf struct {
  nowFunc func() time.Time
}

func (c csrf) now() time.Time {
  if c.nowFunc != nil {
    return c.nowFunc()
  }
  return time.Now()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I think this can work too.

I just want us to get away from method signatures that have now time.Time as a parameter for the sole purpose of time mocking in tests -- but having the baggage of extra arguments in normal code flows is annoying.

The downside to this & my original code (private package var now = time.Now) is we can't fully mock time in units tests that might have dependencies across packages. We definitely have that in our test suite, and in that scenario - I see the testing elegance in public TimeFunc variables (like in the jwt-go sample I mentioned).

I'll jump to this pattern for now, but let's chat more about a good architecture to standardize how we handle time.Now in the codebase.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


// CSRF manages various nonces stored in the CSRF cookie during the initial
// authentication flows.
type CSRF interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends whether it's causing circular imports, I think it's fine here for now

@NickMeves
Copy link
Contributor Author

Semi-related to this PR - I got a intermittent CSRF failure myself for once and caught the log: http: named cookie not present

I have the CSRF cookie in my browser, no other tabs, and this interesting line in the logs:
47.144.131.12 - nick.meves@greenhouse.io [2021/03/04 19:50:52] [AuthFailure] Invalid authentication via OAuth2: unable to obtain CSRF cookie

Invalid authentication... but I'm very obviously authenticated since my username is in the logs 🤔

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from 53dc04c to e7d86ce Compare March 6, 2021 16:54
@NickMeves
Copy link
Contributor Author

Not sure where we are with this right now, looks like it needs a rebase and a new review? Did we decide how we wanted to handle the time mocking in tests?

It was pretty much done, I think we decided to punt until after v7.1 since its content was more related to a Provider focus v7.2 will have.

I moved time.Now stubbing to be a private struct field as per your suggestion. But you are right, it might be good for us to put our heads together on a good strategy project wide for how we want to manage this in tests.

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from e7d86ce to f1f91fb Compare April 20, 2021 02:09
@NickMeves
Copy link
Contributor Author

OOF - that rebase was brutal after the multiple provider support PR merged 😅

I have tests passing... but I'm gonna want to eyeball this whole thing again myself to make sure I didn't accidentally clobber something during the rebase.

@NickMeves NickMeves force-pushed the oidc-nonce-validation branch from f1f91fb to 3148dcf Compare April 20, 2021 02:20
Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to update the documentation to be more detailed, otherwise I'm happy for this to be merged. WDYT to my suggestion?

Comment on lines 135 to 136
// InsecureSkipNonce skips verifying the ID Token's nonce claim
InsecureSkipNonce bool `json:"insecureSkipNonce,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we want to expand on the documentation on this field. This is what gets generated into the reference docs after all.

I'd say let's include the default as others are and maybe even the little warning about it changing in a future release

Copy link
Member

@JoelSpeed JoelSpeed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks @NickMeves

@JoelSpeed JoelSpeed merged commit 7eeaea0 into oauth2-proxy:master Apr 21, 2021
samirachoadi pushed a commit to samirachoadi/oauth2-proxy that referenced this pull request Jun 13, 2021
* Set and verify a nonce with OIDC

* Create a CSRF object to manage nonces & cookies

* Add missing generic cookie unit tests

* Add config flag to control OIDC SkipNonce

* Send hashed nonces in authentication requests

* Encrypt the CSRF cookie

* Add clarity to naming & add more helper methods

* Make CSRF an interface and keep underlying nonces private

* Add ReverseProxy scope to cookie tests

* Align to new 1.16 SameSite cookie default

* Perform SecretBytes conversion on CSRF cookie crypto

* Make state encoding signatures consistent

* Mock time in CSRF struct via Clock

* Improve InsecureSkipNonce docstring
samirachoadi pushed a commit to samirachoadi/oauth2-proxy that referenced this pull request Jun 13, 2021
* Set and verify a nonce with OIDC

* Create a CSRF object to manage nonces & cookies

* Add missing generic cookie unit tests

* Add config flag to control OIDC SkipNonce

* Send hashed nonces in authentication requests

* Encrypt the CSRF cookie

* Add clarity to naming & add more helper methods

* Make CSRF an interface and keep underlying nonces private

* Add ReverseProxy scope to cookie tests

* Align to new 1.16 SameSite cookie default

* Perform SecretBytes conversion on CSRF cookie crypto

* Make state encoding signatures consistent

* Mock time in CSRF struct via Clock

* Improve InsecureSkipNonce docstring
goshlanguage pushed a commit to goshlanguage/oauth2-proxy that referenced this pull request Sep 20, 2021
* Set and verify a nonce with OIDC

* Create a CSRF object to manage nonces & cookies

* Add missing generic cookie unit tests

* Add config flag to control OIDC SkipNonce

* Send hashed nonces in authentication requests

* Encrypt the CSRF cookie

* Add clarity to naming & add more helper methods

* Make CSRF an interface and keep underlying nonces private

* Add ReverseProxy scope to cookie tests

* Align to new 1.16 SameSite cookie default

* Perform SecretBytes conversion on CSRF cookie crypto

* Make state encoding signatures consistent

* Mock time in CSRF struct via Clock

* Improve InsecureSkipNonce docstring
@NickMeves NickMeves mentioned this pull request Feb 20, 2022
3 tasks
k-jell pushed a commit to liquidinvestigations/oauth2-proxy that referenced this pull request Apr 6, 2022
* Set and verify a nonce with OIDC

* Create a CSRF object to manage nonces & cookies

* Add missing generic cookie unit tests

* Add config flag to control OIDC SkipNonce

* Send hashed nonces in authentication requests

* Encrypt the CSRF cookie

* Add clarity to naming & add more helper methods

* Make CSRF an interface and keep underlying nonces private

* Add ReverseProxy scope to cookie tests

* Align to new 1.16 SameSite cookie default

* Perform SecretBytes conversion on CSRF cookie crypto

* Make state encoding signatures consistent

* Mock time in CSRF struct via Clock

* Improve InsecureSkipNonce docstring
Jing-ze pushed a commit to Jing-ze/oauth2-proxy that referenced this pull request Nov 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support Nonce Parameter & Claim in OIDC Providers

2 participants