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
Conversation
I'm pretty sure I did this wrong and the 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 |
e99479d
to
e7e6e5d
Compare
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. |
021c8d2
to
8d0ac72
Compare
This is in a pre-reviewable state now. I just need to write tests for the new CSRF code now in the cookie package. |
8d0ac72
to
cf6e269
Compare
@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 |
b912e65
to
2f8eb7e
Compare
There was a problem hiding this 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
pkg/cookies/csrf.go
Outdated
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 |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
@JoelSpeed What are your thoughts on the 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 The other point in time is immediately after a Maybe if we decide validating is good there, we add a Either way -- I think the legacy usage of |
|
||
// CSRF manages various nonces stored in the CSRF cookie during the initial | ||
// authentication flows. | ||
type CSRF interface { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
ad7b823
to
afca1a6
Compare
afca1a6
to
6414933
Compare
@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 |
} | ||
// 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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 😅
pkg/cookies/csrf.go
Outdated
"github.com/vmihailenco/msgpack/v4" | ||
) | ||
|
||
var now = time.Now |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
0bcafcc
to
53dc04c
Compare
Semi-related to this PR - I got a intermittent CSRF failure myself for once and caught the log: I have the CSRF cookie in my browser, no other tabs, and this interesting line in the logs: Invalid authentication... but I'm very obviously authenticated since my username is in the logs 🤔 |
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 |
e7d86ce
to
f1f91fb
Compare
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. |
f1f91fb
to
3148dcf
Compare
There was a problem hiding this 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?
pkg/apis/options/providers.go
Outdated
// InsecureSkipNonce skips verifying the ID Token's nonce claim | ||
InsecureSkipNonce bool `json:"insecureSkipNonce,omitempty"` |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks @NickMeves
* 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
* 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
* 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
* 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
#966
Implements OIDC Nonce setting:
GetLoginURL
(even if they don't use it).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 istrue
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: