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

Support nonce checks in OIDC Provider #967

Merged
merged 14 commits into from Apr 21, 2021

Conversation

NickMeves
Copy link
Member

@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
Member 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
Copy link
Member 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
Member 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 removed the WIP Work in progress label Dec 31, 2020
@NickMeves
Copy link
Member 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
Member 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.

oauthproxy.go Outdated Show resolved Hide resolved
pkg/cookies/csrf.go Outdated Show resolved Hide resolved
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
Member 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?

pkg/cookies/csrf.go Outdated Show resolved Hide resolved
pkg/cookies/csrf_test.go Outdated Show resolved Hide resolved
pkg/encryption/nonce.go Show resolved Hide resolved
@NickMeves
Copy link
Member 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
Member 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 added this to In Progress (Pull Requests) in Release v7.1.0 Feb 10, 2021
@NickMeves
Copy link
Member 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.

oauthproxy.go Outdated Show resolved Hide resolved
@NickMeves NickMeves linked an issue Feb 20, 2021 that may be closed by this pull request
CHANGELOG.md Show resolved Hide resolved
oauthproxy.go Show resolved Hide resolved
pkg/cookies/cookies.go Outdated Show resolved Hide resolved
}
// 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
Member 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
Member 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
Member 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
Member 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
Member 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

pkg/cookies/csrf.go Outdated Show resolved Hide resolved
@NickMeves
Copy link
Member 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 🤔

@JoelSpeed
Copy link
Member

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?

@NickMeves
Copy link
Member 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
Copy link
Member 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.

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
Release 7.2.0 automation moved this from In progress to Done 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

Support Nonce Parameter & Claim in OIDC Providers
2 participants