Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

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

Open
jgrandja opened this issue May 20, 2021 · 42 comments
Open

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

jgrandja opened this issue May 20, 2021 · 42 comments
Labels
type: documentation A documentation update

Comments

@jgrandja
Copy link
Collaborator

jgrandja commented May 20, 2021

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:

  • MUST use the OAuth 2.0 Authorization Code flow with the PKCE extension when obtaining an access token
  • MUST Protect themselves against CSRF attacks by either:
    • ensuring the authorization server supports PKCE, or
    • by using the OAuth 2.0 "state" parameter or the OpenID Connect 1.0 "nonce" parameter to carry one-time use CSRF tokens
  • MUST Register one or more redirect URIs, and use only exact registered redirect URIs in authorization requests

OAuth 2.0 Authorization Servers supporting browser-based applications:

  • MUST Require exact matching of registered redirect URIs
  • MUST Support the PKCE extension
  • MUST NOT issue access tokens in the authorization response

Application Architecture Patterns

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

  • a JavaScript application that has methods of sharing data with resource servers, such as using common-domain cookies
  • a JavaScript application with a backend
  • a JavaScript application with no backend, accessing resource servers directly

These three architectures have different use cases and considerations.

Common-domain cookies

  • Used in simple system architectures, where the client application, authorization server and resource server(s) share the same domain and can share HTTP-only secure cookies that store the access token and refresh token.
  • This architecture pattern is not widely used, as OAuth was originally created for third-party or federated access to APIs.

Backend component

  • This architecture pattern is commonly referred to as "backend for frontend" or "BFF".
  • The JavaScript code (frontend) is loaded from the server that also has a backend component deployed alongside it. This enables the ability to perform OAuth flows in the backend component and store the access token and refresh token in the "secure" server environment instead of the "insecure" frontend (browser) environment.
  • The backend component uses a confidential client when performing the OAuth 2.0 Authorization Code flow with PKCE.
  • The server establishes a session between the frontend and backend using a session cookie.
  • A request to a resource server is initiated from the frontend to the backend component and then to the resource server. The backend component acts as a mediator.

Frontend-only component

  • The JavaScript code (frontend) is loaded from a static web host into the browser, and the application then runs in the browser. This application is considered a public client, since there is no way to issue it a client secret and there is no other secure client authentication mechanism available in the browser.
  • The JavaScript application is responsible for storing the access token (and optional refresh token), however, there is no standard browser API that allows to store tokens in a completely secure way.
  • The JavaScript application directly interacts with the Authorization Server and Resource Server.
  • The Authorization Server and Resource Server MUST support the necessary CORS headers to enable the JavaScript application to make requests from the domain on which the JavaScript is executing.

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:

  • MUST either rotate refresh tokens on each use OR use sender-constrained refresh tokens
  • MUST either set a maximum lifetime on refresh tokens OR expire if the refresh token has not been used within some amount of time
  • MUST NOT extend the lifetime of the new refresh token beyond the lifetime of the initial refresh token

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

  • OAuth 2.0 Authorization Code flow with PKCE
  • OAuth 2.0 "state" parameter
  • OpenID Connect 1.0 Authorization Code flow with PKCE
  • OpenID Connect 1.0 "nonce" parameter
  • Exact matching of registered redirect URIs

Unsupported

  • Refresh Tokens for Public Clients

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
Copy link

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
Copy link

nickmelis commented Jan 20, 2022

@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
Copy link
Collaborator Author

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

@jgrandja jgrandja changed the title Implement security features for Browser-Based Apps Implementation guidelines for Browser-Based Apps (SPA) Feb 10, 2022
@jgrandja jgrandja added type: task and removed type: enhancement A general enhancement status: on-hold We can't start working on this issue yet labels Feb 10, 2022
@rayman245
Copy link

@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
Copy link
Collaborator Author

@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
Copy link

gnom7 commented May 13, 2022

@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
Copy link

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
Copy link
Member

sjohnr commented Jul 11, 2022

@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
Copy link

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
Copy link
Member

sjohnr commented Sep 26, 2022

@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
Copy link

wrsulliv commented Sep 26, 2022

@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
Copy link

everflux commented Sep 27, 2022

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
Copy link

pstorch commented Jan 4, 2023

Came here from #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?

@jgrandja jgrandja added type: documentation A documentation update and removed type: task labels May 27, 2023
@davidmarne-wf
Copy link

@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 #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
Copy link

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
Copy link
Collaborator Author

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

@Toerktumlare
Copy link

Toerktumlare commented Sep 20, 2023

@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
Copy link

davidmarne-wf commented Sep 22, 2023

@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
Copy link

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

@mberwanger
Copy link

mberwanger commented Oct 27, 2023

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
Copy link
Collaborator Author

@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
Copy link
Collaborator Author

jgrandja commented Nov 2, 2023

@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
Copy link

@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
Copy link
Collaborator Author

jgrandja commented Nov 2, 2023

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
Copy link

stefanocke commented Dec 2, 2023

@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 #1430 (comment) , where it fits better.

@Mehdi-HAFID
Copy link

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
Copy link
Member

sjohnr commented Jan 22, 2024

@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
Copy link

tr-nhan commented Apr 4, 2024

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
Copy link

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
Copy link

everflux commented May 1, 2024

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
Copy link

ekek54 commented May 2, 2024

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
Copy link

tr-nhan commented Aug 18, 2024

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

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
type: documentation A documentation update
Projects
None yet
Development

No branches or pull requests