spring-projects / spring-authorization-server

Spring Authorization Server
https://spring.io/projects/spring-authorization-server
Apache License 2.0
4.89k stars 1.3k forks source link

Implementation guidelines for Browser-Based Apps (SPA) #297

Open jgrandja opened 3 years ago

jgrandja commented 3 years ago

The OAuth 2.0 for Browser-Based Apps specification details the security considerations and best practices when developing browser-based applications that use OAuth 2.0.

The purpose of this issue is to:

  1. Provide a summary of the key points detailed in the specification.
  2. List the currently supported and unsupported features from the specification.

Overview

The current best practice for browser-based applications is to use the OAuth 2.0 Authorization Code flow with PKCE.

Browser-based applications:

OAuth 2.0 Authorization Servers supporting browser-based applications:

Application Architecture Patterns

There are three primary architectural patterns available when building browser-based applications:

These three architectures have different use cases and considerations.

Common-domain cookies

Backend component

Frontend-only component

Refresh Tokens

With public clients, the risk of a leaked refresh token is greater than leaked access tokens, since an attacker may be able to continue using the stolen refresh token to obtain new access tokens potentially without being detectable by the Authorization Server.

Browser-based applications provide an attacker with several opportunities by which a refresh token can be leaked, just as with access tokens. As such, these applications are considered a higher risk for handling refresh tokens.

Authorization Servers may choose whether or not to issue refresh tokens to browser-based applications. If refresh tokens are issued, Authorization Servers MUST conform to the following:

Refresh Token Rotation

The Authorization Server issues a new refresh token with every access token refresh response. The previous refresh token is invalidated but information about the relationship is retained by the Authorization Server. If a refresh token is compromised and subsequently used by both the attacker and the legitimate client, one of them will present an invalidated refresh token, which will inform the Authorization Server of the breach. The Authorization Server cannot determine which party submitted the invalid refresh token, but it will revoke the active refresh token. This stops the attack at the cost of forcing the legitimate client to obtain a fresh authorization grant.

Sender-Constrained Refresh Tokens

Sender-constrained refresh tokens scope the applicability of a refresh token to a certain sender. This sender is obliged to demonstrate knowledge of a certain secret as prerequisite for the acceptance of the refresh token at the Authorization Server.

A typical flow looks like this:

  1. The Authorization Server associates data with the refresh token that binds this particular token to a certain client. The binding can utilize the client identity, but in most cases the Authorization Server utilizes key material (or data derived from the key material) known to the client.
  2. This key material must be distributed somehow. Either the key material already exists before the Authorization Server creates the binding or the Authorization Server creates ephemeral keys. The way pre-existing key material is distributed varies among the different approaches. For example, X.509 Certificates can be used in which case the distribution happens explicitly during the enrolment process. Or the key material is created and distributed at the TLS layer, in which case it might automatically happen during the setup of a TLS connection.
  3. The Authorization Server must implement the actual proof of possession check during a refresh access token request. This is typically done on the application level, often tied to specific material provided by transport layer (e.g., TLS). The Authorization Server must also ensure that replay of the proof of possession is not possible.

There exists several proposals to demonstrate the proof of possession in the scope of the OAuth working group:

OAuth 2.0 Mutual-TLS is the most widely implemented and the only standardized sender-constraining method. The use of OAuth 2.0 Mutual-TLS is RECOMMENDED.

Current Feature Support for Browser-Based Apps

Supported

Unsupported


Refresh Tokens for Public Clients

There are no plans to implement refresh tokens for Public Clients, as there are no browser APIs that allow refresh tokens to be stored in a secure way, which would result in an increased attack surface.

Rotating refresh tokens would help reduce the attack surface, however, it cannot eliminate it. For example, a leaked refresh token can be used by an attacker (before the legitimate client) to refresh an access token and ultimately access the protected resources. The exposure would last until the refreshed access token is expired or invalidated.

A sender-constrained refresh token prevents Token Replay and therefore is RECOMMENDED. However, this requires key material (Public-Private key pair) to be configured in the browser agent and used by the browser-based application, resulting in a complicated setup.

Final Recommendation

After a detailed review of OAuth 2.0 for Browser-Based Apps, OAuth 2.0 Security Best Current Practice and OAuth 2.0 Threat Model and Security Considerations, our recommendation when developing browser-based applications is to use the OAuth 2.0 Authorization Code flow with PKCE and a confidential client.

This can be implemented using either of the two architectural patterns:

  1. Backends For Frontends
  2. Token Mediating and Session Information Backend For Frontend
asterisk360-admin commented 3 years ago

Add a "response_type=jwtCookie" in the authorize endpoind can be helpfull for javascript applications with common domain (currently very used)

This cookie can be added using HTTP-only, SameSite and Secure options.

nickmelis commented 2 years ago

@jgrandja May I ask if there's a rough timeline for when refresh_token capability for public clients will be implemented? We just stumbled upon the same issue and can't find any way around without compromising on user experience (i.e. asking users to log back in when access token expires). Thanks!

jgrandja commented 2 years ago

@nickmelis We don't have a timeline yet. See this comment for more info.

rayman245 commented 2 years ago

@jgrandja isn't it possible to mitigate the misuse of the rotating "leaked" refresh token, by passing the code_verifier from access token grant flow for the refresh token grant flow?

FYI I am not sure how flexible is the OAuth2 architecture on this.

jgrandja commented 2 years ago

@rayman245 PKCE is specified for the authorization_code grant only. It's not specified for the refresh_token grant.

See Refresh Token Protection for the recommended mitigations.

gnom7 commented 2 years ago

@jgrandja is there support (existing or planned) to handle cases when SPA sends multiple concurrent requests with expired access token to BFF, so BFF is required to update token via refresh flow and does this for every request causing all refresh token requests except one to fail OR maybe there was ongoing request propagated downstream at the time new request arrives and causes refresh? maybe you can suggest some mitigation to this (given distributed BFF setup), some sort of synching maybe

mahdiraddadi commented 2 years ago

my spring authorization server works perfectly with my angular but I have only one single issue, On my first login it works correctly, but when I do a logout which is a token revocation, the token is successfully revoked. But when I re-login, the login page directly redirects me with another code to get a new access token without putting the user credentials. After some verifications, I think that the JSESSIONID is used when it's not expired to provide a new code and a new access token I tried to put a max-age to cookie and a timeout to the session using but it's not working how can I solve this issue please?

sjohnr commented 2 years ago

@mahdiraddadi, you may be interested in following progress on gh-266 which I believe is related to your experience. If the context on that issue doesn't clear up your question, please feel free to open a stackoverflow question and post the link here so others can find it, and I can take a look.

everflux commented 2 years ago

Providing no option for refresh tokens for public clients makes the authorization server a really tough choice compared to other approaches: Taking the user experience into account it is either logging in again every 5 minutes or having long lived access tokens with no way to become aware of misuse compared to token rotation. The browser application could store the refresh token just in memory without using local storage or other means of storage that might hypothetical leak the data. (Or even use isolation like a webworker shown here https://auth0.com/blog/secure-browser-storage-the-facts/ )

I would appreciate if this could be re-evaluated.

sjohnr commented 2 years ago

@everflux, thanks for the discussion and link.

(Or even use isolation like a webworker shown here https://auth0.com/blog/secure-browser-storage-the-facts/ )

I believe the summary from that article (see "Using Browser Storage Best Practices Helps Keep Data Secure") comes to the same conclusion as the guidelines recommended by this issue. Specifically:

Finding the right solution depends on your application requirements but always consider moving away from a browser storage design to a Backend-For-Frontend (BFF) one, where the secret is stored in the backend.

wrsulliv commented 2 years ago

@sjohnr I understand the concern saving a refresh token on a public client, but I don't understand how the BFF approach solves this.

Even if the refresh token is stored in the server context, there must be some way for the public client to request a new token when the current token expires.

Per the proposal draft:

As such, the backend MUST verify the the call is occurring in the context of a secure session (e.g., by mandating the presence of a valid session cookie received via HTTPS).

Further, the draft does not indicate how this is to be implemented:

This document does not mandate any specific mechanism to establish and maintain that session. In other words: the user must have signed in the backend, using whatever web sign on mechanism the developer chooses. For example, the user might have signed in the backend using OpenID Connect [OIDC], resulting in a session cookie bound to the backend application domain that will be included in every future requests from the user agent. The choice of web sign on technology is completely arbitrary, with the only requirement of resulting in an authenticated session.

In this case, the client is still storing a piece of data (e.g. a cookie) that effectively gives it the ability to refresh the access token.

I'm still reading the linked info, but at this point, other than the additional control point, I don't understand how this is safer than storing the refresh token directly in the public client.

everflux commented 2 years ago

I think there are different types of applications to be considered: SPAs in the desktop-replacement class and traditional web sites, possibly augmented with jquery or some other progressive enhancements, possibly even hosted in an environment not considered as an application but a website. For the later I would concur that it is hard to secure correctly.

Using a modern framework XSS protection must be turned off forcefully, so the developer should be knowing, what he does. It is really hard to follow the reasoning that a backend or framework developer tries to solve an potential issue (refresh token leakage) for the frontend application by forcing them to use a less secure approach (long lived access token). I don't believe that a technical approach is the right one compared to education, code review etc.

I would really appreciate to be able to build desktop-class SPAs with spring in the background using the authorization server. And yes, I might consider a BFF approach, but depending on the requirements it is not always feasible to do so. I would like to see that Spring gives me a choice, as that is - in my opinion - the spirit of Spring.

pstorch commented 1 year ago

Came here from https://github.com/spring-projects/spring-authorization-server/pull/335 I want to implement a native mobile app with authorization code grant + PKCE, because they are also considered public clients according to https://datatracker.ietf.org/doc/html/rfc6749#section-2.1 Then I was wondering why there is no refresh_token in the response. This issue only discusses webpages (mainly SPAs), but what about native mobile apps? They can't store client_secrets securely and need to protect the redirect_uri, so that other malicious apps can't intercept. That's the code_verifier for, right? Is it considered best practice to have long lived access tokens for native mobile apps instead of refresh tokens?

ccaspanello commented 1 year ago

I'm dying for some good documentation on CORS configuration :) . I saw this ticket https://github.com/spring-projects/spring-authorization-server/issues/110; but the suggested approach is 2 years old and no longer works because the WebSecurityConfigurerAdapter class has been deprecated. I tried other approaches but nothing seems to work. Would anyone be so kind to offer a solution so I can get passed my implementation hurdle?

For more context; I'm using the samples/default-authorizationserver to hook up a ReactJS (w/ react-oidc-context) . Any helpful would be grateful!

jgrandja commented 1 year ago

@ccaspanello Take a look at the sample in this branch. It's using Angular and here is the CORS config.

ccaspanello commented 1 year ago

Thank you @jgrandja for the very quick reply - Checking it out! Report back shortly.

ccaspanello commented 1 year ago

@jgrandja - You rock! That branch helped greatly. I'm now unblocked and authentication against the server is working perfectly. Thank you so much. You and your team are amazing. Cheers!

ccaspanello commented 1 year ago

@jgrandja - I may have celebrated too soon. When I hooked up my React app to the branch server it worked fine. However trying to move to using "real" dependencies I discovered the branch is out of date. This branch is 13 commits ahead, 179 commits behind spring-projects:main.

For example the ProviderSettings class is missing entirely. Looking at past issues it looks like this was renamed to AuthorizationServerSettings however after doing this and reimporting moved packages it doesn't look like the CorsConfig Bean is being used. I am currently trying to use the latest release 1.0.0

jgrandja commented 1 year ago

@ccaspanello CORS is a feature provided by Spring Framework. See reference documentation for details on how to configure. Also, see Spring Security CORS integration reference for additional details.

ccaspanello commented 1 year ago

@jgrandja - I get that but even disabling CORS according to the documentation you shared doesn't work:

@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
    http
        // ...
        .cors(cors -> cors.disable());
    return http.build();
}

What should I do? Think I should file a bug somewhere else?

jgrandja commented 1 year ago

@ccaspanello FYI, questions are better suited to Stack Overflow. We prefer to use GitHub issues only for bugs and enhancements. If you feel this is a genuine bug, please log a new issue and provide details along with a minimal sample that reproduces the issue.

The CORS related comments is unrelated to the main issue and should be avoided as we do not want to pollute the issue with unrelated comments.

sjohnr commented 1 year ago

@ccaspanello I have updated the bff-demo branch.

Note: Make sure you visit the Angular app via http://127.0.0.1:4200 as the CORS allowed origin uses that URL specifically. I ran into CORS issues when upgrading, and realized (remembered) that visiting localhost:4200 does not work with my configuration.

mrowley-hundred10 commented 1 year ago

IMO - if rotating refresh tokens are enabled then public clients should be able to use them. Yes, there is an increased attack surface as a bad actor could steal the refresh token and use it before the legit client uses it. However, this is also true of the access token being stolen so the risk is similar.

In my case, I have an existing public SPA that we are trying to change over to use OAuth2 (instead of self-generated JOTS) and this is a bit of a blocker.

Instead of protecting developers the risks should be identified but the option should be made available - many other OAuth2/OIDC providers allow rotating refresh tokens with public clients.

For Auth0 if an invalided refresh token is used all refresh tokens are automatically invalidated in order to stop further use: https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation

davidmarne-wf commented 1 year ago

@jgrandja thank you so much for all of the discovery you've done here, and for all info you've provided on this subject!

while i agree with your takes for the SPA/browser based use case, i have similar questions to @pstorch regrading mobile/native clients that use ACF + PKCE with a public client.

These native platforms provide methods for secure storage (e.g. apple's secure enclave system), which i understand are typically used to store sensitive refresh tokens. I'm also aware these native flows do still use the browser during the oauth2 flow, but my understanding there is that the embedded browser apis are sandboxed and more secure.

It seems https://github.com/spring-projects/spring-authorization-server/issues/296 and this issue may be conflating "refresh tokens with public clients" with "secure SPA/browser based apps", and decisions regarding security for browser based applications are inhibiting this refresh token feature for native clients.

Are there still vulnerabilities with the native/mobile story for ACF + PKCE with a public client that i'm unaware of? are the concerns with mobile/native still the same as SPA since technically a browser is used during the oauth2 flow?

Toerktumlare commented 1 year ago

Great post, i very much agree on everything written in this issue @jgrandja

I have just one question, will the text in this issue ever be added to any official spring documentation, so that it is possible to link to properly. For instance in the authorization server docs, or in spring security docs?

jgrandja commented 1 year ago

Great idea @Tandolf. We'll look at adding it to the reference doc.

Toerktumlare commented 1 year ago

@davidmarne-wf thanks for the reply, you wrote:

I'm also aware these native flows do still use the browser during the oauth2 flow, but my understanding there is that the embedded browser apis are sandboxed and more secure.

Im not well versed in the apple eco system when it comes to development and apis, but I fail to see how a sandboxed environment can prevent for instance a XSS attack in a mobile application that stores a tokens that running code can access.

davidmarne-wf commented 1 year ago

@Tandolf

I fail to see how a sandboxed environment can prevent for instance a XSS attack in a mobile application that stores a tokens that running code can access.

Yea I'm no expert on apple APIs either and there isn't a whole lotta info in those docs for ASWebAuthenticationSession. All it says is "the browser is a secure, embedded web view" lol, not exactly sure what all that means. But I agree, that alone surely isn't gonna rule out all XSS attacks for example.

The main thing i was trying to call out with my comment is native apps and SPAs are not always susceptible to the same threats/attacks, and have entirely different ecosystems of security tooling and apis to leverage.

Take XSS as an example, that is generally browser based attack. Once the oauth2 flow is complete, a native ios application or a jvm based android app is going to be far less susceptible to being tricked into running malicious javascript than a browser based app. For many mobile app use cases the only time javascript is ever executed is to perform the oauth2 flow.

So my core question is this:

Is the spring teams stance of "no refresh token for public clients" still true for native applications because native apps will still need to use a browser to perform the oauth2 flow? if this is the stance then cool, i get it.

or does there need to be another conversation about supporting refresh tokens for public clients, but considering the threat model for native apps rather than browser based apps?

sidsamant commented 1 year ago

Thanks all for your inputs! Where can I find a custom implementation for refresh_token for public clients?

mberwanger commented 1 year ago

I am developing a command-line utility that leverages the OAuth 2.0 Authorization Code flow with PKCE to obtain an access token, which is used for authenticating with an API server. A notable example of this approach can be seen in the Google Cloud CLI.

The gcloud utility temporarily creates a local web server to manage the response from the Authorization Server and facilitates the exchange of the received code for tokens. It's essential to emphasize that these tokens are neither stored nor exposed within the user's browser. While there are valid concerns about securely managing tokens in Single Page Applications (SPAs), it's worth noting that this scenario represents a public client that should not be held to the same restriction of not being granted a refresh token.

I could opt for issuing access tokens with extended lifespans (in the span of hours). However, this approach doesn't appear right to me, especially considering that the risk of inadvertently disclosing the refresh token remains consistent with the BFF pattern or other methodologies.

I am a big fan of this project but this is definitely a pain point for me.

jgrandja commented 1 year ago

@mberwanger

this scenario represents a public client that should not be held to the same restriction of not being granted a refresh token

Agreed and thanks for providing this use case.

this is definitely a pain point for me

We hear you and it's been a paint point for a few others as well. This is not our intention. Our goal is to make the framework easy to use and configure. And to provide enough flexibility to customize to meet your requirements.

Given this, I opened gh-1430 to address this.

cc/ @nickmelis @everflux @mrowley-hundred10 @davidmarne-wf

jgrandja commented 1 year ago

@mberwanger @nickmelis @everflux @mrowley-hundred10 @davidmarne-wf

Please take a look at gh-1432, specifically this test and let me know if this enhancement will provide the customization you need.

mberwanger commented 1 year ago

@jgrandja That works for me! I really appreciate the quick response and the hard work you and the team have put into this project.

jgrandja commented 1 year ago

Excellent @mberwanger ! I'm glad this will solve your use case. We also appreciate the feedback you have given and glad that this project is working well for you 👍

stefanocke commented 1 year ago

@jgrandja , I was able to get refresh tokens for a public client as shown in your test case. However, I was not able to use them afterwards: I call the token endpoint with the refresh token and wiht grant_type "refresh_token", but I get an InsufficientAuthenticationException ("Full authentication is required to access this resource").

I did some further analysis and described a solution at https://github.com/spring-projects/spring-authorization-server/issues/1430#issuecomment-1839359930 , where it fits better.

Mehdi-HAFID commented 10 months ago

Having a spring authorization server, resource server, oauth client. how does react application connect with this OAuth system ? I've read this whole issue. and cannot find one example of this BFF pattern. the two urls in the end are just abstract discussion of a pattern. does anyone here have a link to an article to how an SPA connect to a Spring OAuth backend. I asked this on stackoverflow with no answer so far: question

sjohnr commented 10 months ago

@Mehdi-HAFID, please do not cross-post questions on issues, as the team (and community) regularly review Stack Overflow for questions. We prefer to use GitHub issues only for bugs and enhancements.

tr-nhan commented 8 months ago

Hey just to be sure, although I know it is not recommended but I can assign a "placeholder" client secret for my public client i.e SPA website so that I can force this spring authorization server to generate refresh token, right?

Mehdi-HAFID commented 7 months ago

For everyone struggling with the implementation of a Spring OAuth 2 system with React SPA as the front as I was. I made Nidam just for that. It is open source at https://nidam.derbyware.com. It is a collection of Spring microservices and a React SPA. It includes:

  1. Spring OAuth 2 Authorization Server. Spring OAuth 2 Resource Server.
  2. BFF (Backend For Frontend). With a Reverse Proxy.
  3. User Registration Backend connected to a MySQL DB.
  4. a React SPA that offers a Registration page with Google Recaptcha support, login page, logout, and access to private resources when logged in.

Every single component is well documented on the website above. Also, all is open source.

everflux commented 7 months ago

Hey just to be sure, although I know it is not recommended but I can assign a "placeholder" client secret for my public client i.e SPA website so that I can force this spring authorization server to generate refresh token, right?

That sounds like a terrible work-around, but should indeed "solve" the issue. The real fix would be to support this scenario by the spring authorization server. I read the OAuth 2.1 RFC in that manner that this is indeed now officially defined to provide refresh tokens for public clients.

ekek54 commented 7 months ago

The RFC document describes three structures. I believe this framework should support these three structures to function well. Especially in the third structure (Frontend-only component), it is restrictive to only use access tokens. If the resource server is stateless and cannot use CSRF tokens, access tokens will typically be managed in local storage. This is because cookies are vulnerable to CSRF, and in-memory variables cannot maintain sessions across refreshes or other tabs. However, if there is a refresh token, it can be managed in an http-only cookie, and the access token can be managed in a more secure in-memory variable or web worker, allowing sessions to be maintained across tabs or refreshes.

In summary, excluding refresh tokens increases the validity period of access tokens, increasing security threats.I believe that managing refresh tokens in http-only cookies and access tokens in in-memory variables is a more secure method and can maintain UX, compared to managing long-lived access tokens in local storage.

tr-nhan commented 3 months ago

Hey why don't we just use remember-me cookies from vanilla Spring Security, instead of using refresh token for public client?

Basically, the Spring Authorization Server requires the user (resources owner) to be authenticated. However, it does not care which method the user would be authenticated by. On the other hand, the regular Spring Security will automatically authenticate user after validating the remember-me token, forward to Authorization Server's filter chain, issues an access token with no additional authentication is required

Moreover, Spring Security has already provided mechanisms to invalidate, persist remember-me cookies or use non-persistent, hash-based token