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

[1.13] New OAuth2 middleware with token persistence #2967

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

ItalyPaleAle
Copy link
Contributor

@ItalyPaleAle ItalyPaleAle commented Jul 5, 2023

Fixes #2635
Replaces #2963 and #2964

Currently the OAuth 2 middleware suffers from scalability issues as reported in #2635. Tokens obtained from the authorization server are stored in-memory in the Dapr runtime (a session ID is stored in a cookie on the client), so they do not persist if the runtime is restarted, and make it impossible to scale the application horizontally.

The most common solution to this problem is to store the tokens in the clients, as self-contained tokens such as JWTs; because tokens are sensitive, they are encrypted (using encrypted JWTs).

This PR rewrites the OAuth2 middleware to allow for 2 different behaviors based on the new mode metadata property:

  • cookie (default)
    • Just like today's component, stores the JWT in a cookie that is passed to the client.
    • After successfully authenticating, users are automatically redirected to the endpoint they were visiting at the beginning
  • header:
    • Requires the token (issued by Dapr and not by the IdP!) to be present in the Authorization header
    • When users authenticate successfully, the client sees a response with a JSON body containing a single key Authorization and the value of the Authorization header that clients must pass
    • Note that there's no automatic redirect here

Why 2 modes?

The reason why we have two modes is due to the issue described in #2963. TLDR: tokens returned by Azure AD can be too large to be stored in a cookie.

However, using headers is not a perfect 1:1 replacement:

  • Cookies can set in a 302 response while the client is redirected to the original endpoint that was invoked. We cannot do that with headers
  • Cookies are handled automatically by browsers and other clients, while headers must always be handled manually

Token encryption key

Because cookies now store the token encrypted, users need to provide an encryption key via the new metadata property tokenEncryptionKey:

  • This property is required.
  • For backwards-compatibility, in Dapr 1.12 this property is optional and, if missing, a key will be randomly-generated by the runtime. When this happens, the key is ephemeral so tokens do not survive a restart of the Dapr runtime and horizontal scaling is not supported - this is pretty much what happens in Dapr 1.11 today! I have opted for allowing this behavior in Dapr 1.12 to offer a smoother transition period (even if the component is in alpha stage).
  • Technically speaking, tokenEncryptionKey is not the actual encryption key used: the Dapr runtime deterministically derives both a signing key and an encryption key from that

Included in this PR

  • Unit tests with a mocked IdP
  • metadata.yaml

Future work

  • When Allow (middleware) components to define internal actors dapr#6538 is implemented, we can use actors (if enabled) to store tokens when they're too large for a cookie. In this case, if the cookie is small enough, it's sent to the client as-is. Otherwise, Dapr stores the token in an actor's state, and then sends a session ID as cookie to the client; this is done transparently only for tokens that are too large to be included in cookies.
  • Add certification tests so the component can be made stable. See Certification test for middleware.http.oauth2 #2621 .
    • Note that because this PR is essentially a complete rewrite, the components cannot be made stable until Dapr 1.13 at the earliest, even with certification tests
  • The middleware middleware.http.oauth2clientcredentials has the same issues with scalability as the OAuth2, and needs to be updated as well (and perhaps we can re-use the shared implementation of this middleware).

Includes validation

Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
@ItalyPaleAle ItalyPaleAle added this to the v1.12 milestone Jul 7, 2023
Copy link
Contributor

@DeepanshuA DeepanshuA left a comment

Choose a reason for hiding this comment

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

Doing the very first pass. Will try to review it more thoroughly.

m.setTokenFn = m.cookieModeSetTokenInResponse
m.claimsForAuthFn = m.cookieModeClaimsForAuth
case modeHeader:
m.getTokenFn = m.headerModeGetClaimsFromHeader
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we change the name headerModeGetClaimsFromHeader to GetClaimsFromHeader, so that it is in sync with GetClaimsFromCookie ?

m.claimsForAuthFn = m.cookieModeClaimsForAuth
case modeHeader:
m.getTokenFn = m.headerModeGetClaimsFromHeader
m.setTokenFn = m.headerModeSetTokenResponse
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
m.setTokenFn = m.headerModeSetTokenResponse
m.setTokenFn = m.headerModeSetTokenInResponse

or cookieModeSetTokenInResponse to cookieModeSetTokenResponse

state := r.URL.Query().Get("state")
if code != "" && state != "" {
// Always get the claims from the cookies in this case
claims, err := m.GetClaimsFromCookie(r)
Copy link
Contributor

Choose a reason for hiding this comment

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

Trying to understand: Why claims are always fetched from cookie and not from header, if code and state are provided?


url := conf.AuthCodeURL(idStr, oauth2.AccessTypeOffline)
httputils.RespondWithRedirect(w, http.StatusFound, url)
m.exchangeAccessCode(w, r, claims, code, state)
Copy link
Contributor

Choose a reason for hiding this comment

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

Here, we rely on code being not blank to exchange it to get access token. Should we also check response_type somehow before doing so?
Ref: https://darutk.medium.com/diagrams-and-movies-of-all-the-oauth-2-0-flows-194f3c3ade85 and https://stackoverflow.com/a/74547318/18848251

@ItalyPaleAle
Copy link
Contributor Author

I think we should push this to 1.13 for when internal actors for components (dapr/dapr#6538) is available. At this stage, it's highly unlikely internal actors for components are available in 1.12.

The reason is that in the current case, it fails when tokens are large, such as with Azure AD.

Although this PR does fix a very important issue, it may be a lesser issue than the alternative.

Thoughts? @dapr/maintainers-components-contrib

@berndverst
Copy link
Member

I'm fine postponing this to 1.13. If so, do you want to mark it as a draft PR and add 1.13 to the title for now @ItalyPaleAle ?

@ItalyPaleAle ItalyPaleAle modified the milestones: v1.12, v1.13 Jul 24, 2023
@ItalyPaleAle ItalyPaleAle marked this pull request as draft July 24, 2023 21:35
@ItalyPaleAle ItalyPaleAle changed the title New OAuth2 middleware with token persistence [1.13] New OAuth2 middleware with token persistence Jul 24, 2023
@github-actions
Copy link

github-actions bot commented Sep 1, 2023

This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions!

…o oauth2-mw-persistence3

Signed-off-by: ItalyPaleAle <43508+ItalyPaleAle@users.noreply.github.com>
@ItalyPaleAle ItalyPaleAle modified the milestones: v1.13, v1.14 Feb 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pinned Issue does not get stale
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Oauth2 Middleware scalability limit and resilience issues with code grant flow and token storage
3 participants