Repository navigation
Conversation
|
Thanks for picking this up — I closed #3616 in favor of this and it's in much better shape than what I had. Three things from reading through it, one of which is a workflow preference rather than a defect. 1. The unscoped path will reject a gophercloud/openstack/identity/v3/websso/requests.go Lines 133 to 139 in ace4e9a
gophercloud/openstack/client.go Lines 205 to 222 in ace4e9a So I think this branch just wants 2. The scope is only validated after the browser flow completes. gophercloud/openstack/identity/v3/websso/requests.go Lines 118 to 128 in ace4e9a A 3. Caching the scoped token instead of the unscoped one costs a browser round trip per project. gophercloud/openstack/identity/v3/websso/requests.go Lines 74 to 84 in ace4e9a This is a preference rather than a bug. Day to day I move across a lot of projects, and the flow I rely on today caches the unscoped token and rescopes locally per project — one browser login in the morning and none after it. Keying the cache on scope means a new browser round trip on every project switch. Would you consider caching the unscoped token and doing the rescope against that cache instead? It also makes item 2 cheaper, since an invalid scope would never cost a login. |
|
Here's a peer change to keystoneauth: https://review.opendev.org/c/openstack/keystoneauth/+/1000759 It's still very much a WIP as I've just squashed together a number of commits in another repo and moved them over to keystoneauth and moved the tests over. Claude did that lifting so it needs to be tested. |
|
Thanks—these are addressed in |
|
Ran this against a live Keystone deployment with an OIDC federated identity provider, using a small file-backed
Two small notes on the new code, neither blocking:
That works against the deployments I can test, but it's an avoidable dependency on Keystone echoing the header back; carrying the known ID forward would be more robust. Separately, something I hit while writing the harness that isn't this PR's fault: Nothing blocking from my side — thanks for turning items 1-3 around so quickly. |
|
Thanks for testing this. Addressed both notes in 90a9b9d:
|
|
Re-tested against a live Keystone deployment with an OIDC federated IdP. Both notes from my last comment are resolved, and the caching behavior holds up well in practice:
One thing I'd still like to see: Close the browser window on success. The callback handler in |
|
Added the best-effort window.close() call in aef252e, with the existing success message retained as the fallback when the browser refuses to close the tab. |
|
This version looks great to me. The Nothing outstanding from my side, and I don't want to hold this up. To be clear about my earlier comment: the acceptance-test coverage is still worth adding, but I don't think it needs to block landing — it can follow in a separate change. Thanks for being so responsive through all of this. |
aef252e to
03ffe68
Compare
|
@stephenfin the Zun failure is unrelated so this can run again. |
An upstream fix has landed, so I'm rerunning the failing job. I'm also taking a look at the code right now. |
winiciusallan
left a comment
There was a problem hiding this comment.
hey @rm-you, thank you very much for working on this and updating the PR promptly! I've left a few comments that I judged important, let me know what you think.
@stephenfin since you were involved in the upstream review, it would be nice to have eyes on this.
| return time.Now().Add(tokenExpiryMargin).Before(ct.ExpiresAt) | ||
| } | ||
|
|
||
| // Load retrieves a cached token ID. Missing, malformed, and expired entries are |
There was a problem hiding this comment.
should not malformed entries return an error or at least a warning? I think it's reasonable to consider missing and/or expired as a cache miss, but a malformed entries could be a result of something wrong somewhere else in the code.
There was a problem hiding this comment.
I intentionally made the cache best-effort here. A malformed or stale local entry should not make authentication fail when we can delete it and continue through the normal browser flow; the next successful login repairs the entry. The cache package also has no logging facility, and returning a decoding error would make an optional cache less reliable than having no cache.
I agree that malformed data is distinct from an ordinary miss, though, and I can clarify that behavior in the docs. If the preferred contract is to expose backend or decoding errors, I can change Load to return an error, but my inclination is still delete-and-miss for malformed data and to reserve errors for failures that prevent recovery.
There was a problem hiding this comment.
[...] A malformed or stale local entry should not make authentication fail when we can delete it and continue through the normal browser flow; the next successful login repairs the entry. [...]
This has convinced me, so I'm okay keeping as-is. But let's keep the thread open in case anyone has objections.
| const tokenExpiryMargin = 5 * time.Minute | ||
|
|
||
| // Cache stores authentication results. Implementations must be safe for concurrent use. | ||
| type Cache interface { |
There was a problem hiding this comment.
As I can see, we don't have any concrete implementation of this interface throughout the PR (regardless the tests).
Is this being addressed in a follow-up or is it intended to be optional and implemented by the client?
There was a problem hiding this comment.
So the python client uses the keyring package to store the cache in the system key chain. It's ultimately implemented by the end user client. But it would require each client to implement this. So I'm not sure how to best address this. There could be a default implementation in gophercloud? I'd be happy to do it as a follow up.
There was a problem hiding this comment.
As a SDK point of view, we want to make the end users' life easier, so IMO it's worth having at least one basic implementation while delivering the cache benefits (it improves overall performance and avoid round tripping to keystone).
If the other people agree, I think we could do this in a follow-up PR yes 🙂.
There was a problem hiding this comment.
The interface is already consumed by WebSSO in this PR: before opening the browser, it loads and validates a cached unscoped token; if Keystone rejects that token, it deletes the entry; after a successful browser flow, it persists the newly captured token. What is not included here is a concrete persistent-storage backend.
The original version of this integration put the system-keyring implementation in gophercloud/utils, alongside the higher-level clientconfig wiring. As the rest of this work evolved, the core clouds.ParseV3 follow-up made that Utils layer unnecessary. I did not move the keyring backend into base Gophercloud because doing so would add go-keyring and its platform-specific transitive dependencies to the core module for every user. Instead, the provider integration currently supplies the implementation in internal/keyringcache and passes it through clouds.WithTokenCache.
I am not opposed to putting a concrete implementation here. If the maintainers are comfortable with those dependencies in the base Gophercloud module, I would be happy to move the keyring implementation into this PR or a small follow-up. I mainly wanted to avoid making that dependency decision implicitly.
For context, the complete dependency plan is:
- #3939, context-aware reauthentication, is merged and is the common prerequisite.
- #3984 adds the Identity v3 provider-construction path used by the higher-level parser.
- This PR adds WebSSO and the generic cache contract. WebSSO is its first consumer.
- #3942 adds OIDC client-credentials auth. Assuming the PR we're commenting on now merges first, I will update this OIDC PR to make it use the same cache contract.
- #3940 adds OAuth2 mTLS support.
- The unposted feat/federated-auth-clouds follow-up adds clouds.ParseV3, registers those auth methods, and replaces the remaining gophercloud/utils clientconfig role. This might be a little confusing to look at since it is currently my own kind of ... integration branch that I use for deploying, but once a couple more of these merge I will have some follow-ups. I wish GitHub would let me do stacked PRs from my fork. :(
- garm-provider-openstack#30 is the merged Gophercloud v2 baseline. Its integration follow-up consumes clouds.ParseV3 and supplies the OS-keyring backend shown above.
Keeping the cache contract generic lets WebSSO now and OIDC later share the key format and token envelope, while leaving storage selection outside the core module unless the maintainers prefer otherwise.
Again, I am happy to add an implementation here, I was actually not happy to leave it without one either, but it felt like ya'll might balk at the added dependencies, so say the word and I'll put it in here.
There was a problem hiding this comment.
Although the cache is a "should have" considering the performance improvements, it isn't a hard dependency and we can do it in a follow-up PR if we want, so I'll not push this too long.
Can we at least document that users need to implement this and provide one reliable source?
I will try to thoroughly review the PRs in the correct order. This seems a perfect use case for stacked PRs 🙂
There was a problem hiding this comment.
Yeah I wish they worked from a fork. If I had write access to this repo I could push them all to branches here and then created stacked PRs, but they don't work from a fork to the source repo (or fork to fork). :(
There was a problem hiding this comment.
Again, I would PREFER to add the cache implementation here NOW, the blocker for me was that I thought it would not be ideal to add client dependencies here on the keyring deps... but I am totally happy to just DO IT, if you give me a green light on that (and would rather just do it than put in temporary docs).
There was a problem hiding this comment.
Let's land this PR and let me pull it down in some local work and then stack some more PRs.
There was a problem hiding this comment.
Yeah, I mean the real issue is that I also am waiting for about three of these to land so that I can make some follow ups that tie them all together better, I literally have some branches just sitting waiting to be able to sanely make them a PR without the ability to stack :(
| return nil | ||
| } | ||
|
|
||
| func captureToken(ctx context.Context, client *gophercloud.ServiceClient, opts *AuthOptions, port int, timeout time.Duration) (string, error) { |
There was a problem hiding this comment.
There was a problem hiding this comment.
It's worth noting that the CSRF is happening locally so it's not really a big danger. But I am trying to mitigate it going forward.
There was a problem hiding this comment.
Good point. The listener is restricted to localhost or a loopback address, accepts only a form POST on the callback path, limits the body size, and validates the submitted token with Keystone before using it. That keeps it from being a remotely exposed arbitrary-token endpoint, but it does not bind the callback to the browser flow that opened it. A malicious local process, or a web page able to submit a form to the loopback listener, could race the legitimate callback and cause a denial of service or, with a valid token, an authentication-confusion result.
I agree that a per-flow unpredictable nonce is worthwhile defense in depth here. I can add the equivalent binding and focused tests in this PR. To be honest, I thought I remembered already doing that, but it might have been in one of my experimental branches. I'll just go ahead and handle that now.
There was a problem hiding this comment.
One correction after checking this while implementing it: a per-flow nonce cannot be added to the origin, because Keystone requires that value to match a trusted_dashboard entry verbatim. The current keystoneauth change documents the same limitation.
I added the equivalent request-shape hardening in 43bc756: the callback now requires top-level navigation Fetch Metadata, rejects direct navigation, and rejects mismatched Origin or Referer when those headers are available. Focused tests cover each case. This narrows what can reach token handling, but it cannot cryptographically bind the callback to the flow, so the remaining login-CSRF limitation is documented.
|
Yeah I'm a bit distracted this week but I'll try to look at this soon -- but at a high level, yes this is in the middle of a chain of a number of PRs both here and in GARM, so there's a good chance that things unused here are in fact for use somewhere else. I'll look at the specifics and give you some links later this week, thanks for taking a look! Edit: I had some time earlier than expected, so see in-line comments. |
03ffe68 to
43bc756
Compare
Refs gophercloud#3615 Co-authored-by: Taiwo Oyewole <taiwooyewole@geico.com>
Validate scope before browser authentication and preserve transient token validation errors. Accept Keystone 203 responses and restrict callback tokens to the form body.
Keystone cannot correlate callback POSTs with the browser flow that initiated them. Document the login-CSRF exposure and cache identity behavior, and cover listener ordering and namespace isolation.
4582477 to
e2780e9
Compare
stephenfin
left a comment
There was a problem hiding this comment.
Partial review. I need to pull this down locally and take a second swing at it.
| @@ -0,0 +1,99 @@ | |||
| // Package tokencache provides token caching for Identity v3 authentication. | |||
There was a problem hiding this comment.
So this is where our previously discussed lack of a keystoneauth equivalent comes back to bite us. Does this file belong here? Typically we only modules corresponding to APIs in openstack/<service>/<version> packages.
Would it be possible to have this (and it's test) in e.g. openstack/auth instead.
There was a problem hiding this comment.
Yep, or just auth/ which #4008 introduced (not sure whether that needs to move or not)
| // Authenticate performs browser-based WebSSO authentication. | ||
| func Authenticate(ctx context.Context, client *gophercloud.ServiceClient, builder tokens.AuthOptionsBuilder) (r tokens.CreateResult) { |
There was a problem hiding this comment.
I definitely feel like this doesn't belong here. I had a look for some of our more unusual existing auth mechanisms and found openstack/identity/v3/oauth1/doc.go. I note that that uses the more standard AuthenticateV3 function from openstack/client.go. I'm wondering if this PR ought to hang off of the pluggable auth PR (which I'm yet to review also). I need to think about this one...
Later: Maybe if we called this "Create" we could deal with this for now?
| // Authenticate performs browser-based WebSSO authentication. | |
| func Authenticate(ctx context.Context, client *gophercloud.ServiceClient, builder tokens.AuthOptionsBuilder) (r tokens.CreateResult) { | |
| // Create performs browser-based WebSSO authentication. | |
| func Create(ctx context.Context, client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { |
It's layering on more c***, but at least it's consistent c***?
There was a problem hiding this comment.
I'm just going to refactor completely on #4008
|
I think you are correct that this and the other methods should just be on top of #4008 so let's get that moving 😇 |
Fixes #3615
Adds Identity v3 authentication through Keystone's browser-based WebSSO flow. Gophercloud opens the identity-provider and protocol-specific WebSSO endpoint with a loopback origin, receives the unscoped token posted by Keystone, and validates or scopes it with the existing token API.
The callback listener is context-aware and validates the request method, content type, body size, and token. Browser opening is injectable. Reauthentication and optional caching of validated unscoped tokens are supported, allowing tokens to be rescoped without repeating browser authentication.
Links to the line numbers/files in the OpenStack source code that support the code in this PR:
Supersedes #3616, which its author closed in favor of this implementation.