Fix app redirection loop on browser's incognito mode and 3rd party cookie block#30321
Fix app redirection loop on browser's incognito mode and 3rd party cookie block#30321
Conversation
d1801e6 to
4de4101
Compare
|
Uh, this stuff is so complex. I looked through it and it looks fine, but I'm not very familiar that part, so I could have missed some things. Also, do you think we need a security review here? |
not likely, this PR is just a revert to the old implementation, the only thing that changed was that instead of overwriting our state token cookie with new requests, we create a unique state token cookie per request (short lived 1 minute) |
|
friendly ping @ryanclark |
|
Actually I will be updating the RFD to explain this flow to help make this whole flow more understandable |
|
|
||
| nonce, err := utils.CryptoRandomHex(auth.TokenLenBytes) | ||
| if err != nil { | ||
| h.log.WithError(err).Debugf("Failed to generate and encode random numbers.") |
There was a problem hiding this comment.
If I was running a Teleport cluster and I saw this in my logs I would have no idea what it means. Can we add a better message?
(Also, we use the exact same message above, so this adds addition burden for figuring out which line triggered the log message)
There was a problem hiding this comment.
@kimlisa I guess you didn't fix all the messages. This one is still cryptic.
| SetRedirectPageHeaders(w.Header(), nonce) | ||
|
|
||
| // Serving the HTML page. | ||
| fmt.Fprintf(w, appRedirectionJs, nonce) |
There was a problem hiding this comment.
nit: rename this constant
appRedirectPage or appRedirectHTML would work
| SetRedirectPageHeaders(w.Header(), nonce) | ||
|
|
||
| // Serving the HTML page. | ||
| fmt.Fprintf(w, appRedirectionJs, nonce) |
There was a problem hiding this comment.
This is probably fine (we control the input), but you do have to be very careful using printf on HTML (it's prone to injection). Using html/template instead would be better, since it's HTML-aware and not a generic string replacement.
|
|
||
| // completeAppAuthExchange completes the auth exchange flow started by "startAppAuthExchange" handler | ||
| // by validating the values passed in the request body, and upon success sets cookies required | ||
| // for the current app session. User should be able to interact with the app now. |
There was a problem hiding this comment.
| // for the current app session. User should be able to interact with the app now. | |
| // for the current app session. |
| httplib.SetNoCacheHeaders(w.Header()) | ||
| var req fragmentRequest | ||
| if err := json.NewDecoder(r.Body).Decode(&req); err != nil { | ||
| return trace.Wrap(err) |
There was a problem hiding this comment.
Since the primary reason this would fail is invalid JSON, what do you think about trace.BadParameter instead?
Or trace.AccessDenied, which is what you use below.
| h.router.POST("/x-teleport-auth", makeRouterHandler(h.withCustomCORS(h.handleAuth))) | ||
| h.router.OPTIONS("/x-teleport-auth", makeRouterHandler(h.withCustomCORS(nil))) | ||
| h.router.GET("/x-teleport-auth", makeRouterHandler(h.startAppAuthExchange)) | ||
| h.router.POST("/x-teleport-auth", makeRouterHandler(h.completeAppAuthExchange)) |
There was a problem hiding this comment.
Do we need to keep the OPTIONS call available for a bit?
As written, app access may break during upgrades when not all proxies are on the same version.
There was a problem hiding this comment.
how long will proxy versions differ though? (when ryan's changes went it, the app breakage wasn't taken into account)
if we were to handle preventing temporary breakage, I would need to make POST /x-teleport-auth differentiate between cors request with custom headers and non-cors request with state query param and auth cookie
There was a problem hiding this comment.
i added backwards compat starting from this commit: cc691d0
tested with v15 build using new and legacy AppLauncher.tsx component with https://grafana-test-127-0-0-1.nip.io:3080/alerting/list?search=state:inactive%20type:alerting%20health:nodata
| u := url.URL{ | ||
| Scheme: "https", | ||
| Host: proxyPublicAddr, | ||
| Path: fmt.Sprintf("/web/launch/%s", hostname), |
There was a problem hiding this comment.
Use url.JoinPath instead of sprintf here?
There was a problem hiding this comment.
i left it as is, b/c i don't think we need the extra functionality of url.JoinPath (cleaning paths). the url.JoinPath kinda complicates things by returning an error. if you feel strongly on it, i'll make it happen.
There was a problem hiding this comment.
Just wanted to point out that url.JoinPath performs URL path encoding and path normalization (i.e. resolution, removal of redundant slashes) while Sprintf takes everything as-is.
On a different note, what happens if the path query parameter does not start with a /?
| RawQuery: query, | ||
| // Presence of a stateToken means we are beginning an app auth exchange. | ||
| if req.stateToken != "" { | ||
| u.RawQuery = fmt.Sprintf("state=%s&path=%s", url.QueryEscape(req.stateToken), url.QueryEscape(req.path)) |
There was a problem hiding this comment.
Similar comment - what you have is fine (you were careful to properly escape the values), but why not use url.Values to encode the string rather than sprintf?
var v url.Values
v.Add("state", req.stateToken)
v.Add("path", req.path)
u.RawQuery = v.Encode()
|
The PR changelog entry failed validation: Changelog entry not found in the PR body. Please add a "no-changelog" label to the PR, or changelog lines starting with |
| @@ -210,31 +210,6 @@ func SetIndexContentSecurityPolicy(h http.Header, cfg proto.Features, urlPath st | |||
| h.Set("Content-Security-Policy", cspString) | |||
There was a problem hiding this comment.
Not be strictly related to this PR, but you should consider implementing a report-uri.
| } | ||
| if subtle.ConstantTimeCompare([]byte(secretToken), []byte(stateCookie.Value)) != 1 { | ||
| h.log.Warn("Request failed: state token does not match.") | ||
| return trace.AccessDenied("access denied") |
There was a problem hiding this comment.
See #31103 (comment), should this incident be audit logged?
There was a problem hiding this comment.
emitted errors and attempt deleting app session starting with this commit: d24ddbb
Reverted parts of: #17592 Kept json field renames
- Create unique cookie for each state token created - Preserve path and queries
89371e8 to
de6bf60
Compare
ibeckermayer
left a comment
There was a problem hiding this comment.
I would need to spend a lot of time understanding context to give this a useful review. I'm happy to do that if necessary, but it seems better suited to somebody more well acquainted with app access and/or your #31103 RFD.
| return trace.Wrap(metaRedirectTemplate.Execute(w, redirectURL)) | ||
| } | ||
|
|
||
| const appRedirectHTML = ` |
There was a problem hiding this comment.
Consider something similar to the line above
teleport/lib/web/app/redirect.go
Line 43 in de6bf60
such that this template doesn't need to be re-parsed each time the respective endpoint is hit.
|
|
||
| nonce, err := utils.CryptoRandomHex(auth.TokenLenBytes) | ||
| if err != nil { | ||
| h.log.WithError(err).Debugf("Failed to generate and encode random numbers.") |
There was a problem hiding this comment.
@kimlisa I guess you didn't fix all the messages. This one is still cryptic.
| Path: "/", | ||
| HttpOnly: true, | ||
| Secure: true, | ||
| SameSite: http.SameSiteNoneMode, |
There was a problem hiding this comment.
Do we really want the "none" mode here, and not Strict?
There was a problem hiding this comment.
i think it is required because of the same reason stated here: https://github.com/gravitational/teleport/pull/30321/files#diff-379535a3878805eca5813cd9aa07db7a254cb885df354203105c4984eabafd83R181
| Path: "/", | ||
| HttpOnly: true, | ||
| Secure: true, | ||
| SameSite: http.SameSiteNoneMode, |
| func (h *Handler) emitErrorEventAndDeleteAppSession(r *http.Request, f emitErrorEventFields) { | ||
| // Attempt to delete app session. | ||
| if f.sessionID != "" { | ||
| _ = h.c.AuthClient.DeleteAppSession(r.Context(), types.DeleteAppSessionRequest{ |
There was a problem hiding this comment.
Perhaps we should log the error, even if we skip it?
| // Cookies). Given this, we should only redirect to it when this format is | ||
| // already in use. | ||
| if !HasSession(r) { | ||
| if p.stateToken == "" && !HasSession(r) { |
There was a problem hiding this comment.
I don't quite get what this code path means. Could you double-check whether the error message below is still appropriate? What does "this format is already in use" mean in the comment above?
There was a problem hiding this comment.
i think the error message, is missing a comma, does it make more sense if written like this:
"redirecting to launcher when using client certificate, is not valid"
you can access application through the CLI like this: https://goteleport.com/docs/application-access/cloud-apis/aws-console/#step-89-access-aws-cli
so i think from the CLI when you run into a forwaring error, it wouldn't make sense to redirect the user to the launcher from the cli, so we just return an error instead
but if the request came from the web, there'd be a cookie attached to it (HasSession), and if there is a cookie, then we can redirect
i'll add a clarifying comment
| const url = getXTeleportAuthUrl({ fqdn, port }); | ||
| url.searchParams.set('state', stateToken); | ||
| url.searchParams.set('subject', session.subjectCookieValue); | ||
| url.hash = `#value=${session.cookieValue}`; |
There was a problem hiding this comment.
Are we sure that whatever there is in the cookieValue variable doesn't need to be URL-escaped?
There was a problem hiding this comment.
it should be safe since the cookieValue is the session id, which is a hex string
|
@kimlisa One general comment: I'm really impressed by this PR, it's really well-thought and VERY well commented. Though it's a very complex subject, the code wasn't at all difficult to understand. Just a handful of small issues, and you'll have my approval. |
@bl-nero don't be impressed. this PR is mostly a |
…okie block (#30321) * Copy pasta revert back to previous app redirection logic Reverted parts of: #17592 Kept json field renames * Remove unused fields (were renamed while back) * Refactor previous implementation - Create unique cookie for each state token created - Preserve path and queries * Split handlers * Fix tests and lint * Address CR * Add backwards compatability * Emit errs with invalid vals and attempt to delete session * Update test * Address CRs * Fix lint
…rty cookie block (#37692) * Fix app redirection loop on browser's incognito mode and 3rd party cookie block (#30321) * Copy pasta revert back to previous app redirection logic Reverted parts of: #17592 Kept json field renames * Remove unused fields (were renamed while back) * Refactor previous implementation - Create unique cookie for each state token created - Preserve path and queries * Split handlers * Fix tests and lint * Address CR * Add backwards compatability * Emit errs with invalid vals and attempt to delete session * Update test * Address CRs * Fix lint * Fix import
resolves #26009, #15935
Updated RFD that explains the flow: #31103
The cookie failure issue, is not a problem if an app is accessed by the default teleport subdomain. It becomes a problem if user sets a
public _addrfor the app where the domain is not teleports.Given SiteA (teleport) and SiteB (domain of app), the current implementation, responded with app cookies from SiteB, when the current site context was still on SiteA. To the browser this is considered 3rd party cookie and is blocked by default for the following major browsers (reproduced it too):
Our previous implementation that used the oauth state exchange flow was not affected by this problem because we set the app cookies while we are in the app domain.
This PR reverts some parts of the current implementation with the previous implementation with some small refactors, notably the state token race condition which was resolved by creating unique cookies for each state token created.
Tested
Tested the following by updating teleport versions (from my reproduction steps) that includes this PR's changes:
Local testing to test parameters are getting saved:
With Browsers:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --test-third-party-cookies-phaseoutReproduction
Running version:
Teleport Enterprise v15.0.0-dev git:pull-4093-g68d8647cd6 go1.21.3(before this PR)proxy_service.https_keypairs(note: need to add inbound rule for port 80)kimlisa.app) to this cluster's ip address (eg:3.83.9.149):ec2 instance 1 (main cluster) cfg:
ec2 instance 2 for app.
http://localhost:4000)./tctl tokens addin ec2 instance 1public-addrflag to custom domainChangelog: Fix app redirection loop on browser's incognito mode and 3rd party cookie block