Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For many first-party applications, the safer default is a Backend-for-Frontend (BFF): Spring handles OAuth login, keeps provider tokens on the server, and gives the SPA a same-origin session cookie. Keep CSRF protection enabled for cookie-authenticated changes. If the browser must call APIs with OAuth tokens directly, register the SPA as a public client and use Authorization Code with PKCE—never a client secret in JavaScript.

Should I use a BFF or let the SPA handle OAuth?

These are different trust and deployment choices, not interchangeable ways to hide the same secret. In a BFF, the browser authenticates to your Spring application with a session cookie; Spring acts as the OAuth client and holds its access and refresh tokens server-side. In a direct SPA design, browser JavaScript obtains tokens and presents them to APIs. The browser application is a public client because anything shipped to it can be inspected or extracted.

Pattern Where OAuth tokens live Browser authenticates with Main trade-off
BFF / server-side OAuth client On the server A session cookie to the BFF Requires server-side session and API/proxy work; cookie-authenticated changes need CSRF defenses.
Direct SPA public client Available to page JavaScript OAuth tokens presented to APIs Avoids the BFF proxy role, but tokens are exposed to script compromise and the provider/API must support browser flows and CORS.

A BFF reduces exposure of OAuth tokens to browser JavaScript; it does not eliminate XSS, session theft, CSRF, or deployment risks. A direct SPA is appropriate when direct token-based access is a deliberate requirement, not as a shortcut to avoid configuring a secure server session.

How do I secure a React SPA with Spring Boot and OAuth?

The example below uses Spring Security 7.1.1, an OpenID Connect (OIDC) provider, and a same-origin deployment: the React build and Spring application are served through one HTTPS origin, with the Spring application handling the BFF endpoints. No particular provider or matching Spring Boot release is prescribed here; use the Spring Boot dependency management for the Boot version you select and verify the resulting Spring Security version. The Spring Security 7.1 OAuth reference is the version-specific starting point: Spring Security OAuth support.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

OAuth is an authorization framework; it does not, by itself, define who the user is. For interactive user login, OIDC adds an identity layer. Spring’s OAuth2 Login uses OAuth2 Client support to perform the login flow. Separately, an OAuth2 Resource Server validates bearer access tokens on APIs, and an OAuth2 Client can obtain tokens when the server calls a protected third-party API. Choose the Spring role that matches the job: login, inbound token validation, and outbound API access are not the same configuration.

1. Register an OIDC client with the provider

Create a confidential server-side client in the identity provider’s console. Supply its issuer or provider endpoints, client ID, client secret, requested scopes, and an exact redirect URI. With a registration named app and Spring’s standard callback path, the URI is https://your-application-host/login/oauth2/code/app. Configure that exact URI at the provider; production callbacks must use HTTPS. Provider setup, endpoint values, scope names, and supported flows vary, so use the provider’s own documentation rather than assuming Google’s or another provider’s settings are universal. Spring’s login configuration reference includes provider-specific examples: Spring Boot OAuth2 Login configuration.

2. Configure the server-side registration

For an issuer-discovery OIDC provider, a minimal registration can be expressed in application.properties. Supply these environment variables through your deployment’s secret/configuration system, not a committed file:

spring.security.oauth2.client.registration.app.provider=app-provider
spring.security.oauth2.client.registration.app.client-id=${OAUTH_CLIENT_ID}
spring.security.oauth2.client.registration.app.client-secret=${OAUTH_CLIENT_SECRET}
spring.security.oauth2.client.registration.app.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.app.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.app.scope=openid,profile,email
spring.security.oauth2.client.provider.app-provider.issuer-uri=${OIDC_ISSUER_URI}

app-provider, the issuer URI, credentials, and scopes are deployment-specific values, not universal provider constants. The openid scope signals an OIDC login; request only profile information the application needs. Use a client secret here only because this registration belongs to the confidential server-side client. Behind a reverse proxy, configure trusted forwarded headers and the external base URL correctly so Spring generates the public HTTPS callback rather than an internal hostname or HTTP URL.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

3. Enable OAuth2 Login and require authentication

A compact servlet security configuration for the BFF can enable the login flow and require authentication for application routes:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .requestMatchers("/", "/assets/**", "/favicon.ico").permitAll()
            .anyRequest().authenticated()
        )
        .oauth2Login(Customizer.withDefaults());

    return http.build();
}

Import the Spring Security DSL and Customizer as usual for your application. The authorization-code login flow is handled by Spring; the provider redirects back to the registered callback. This configuration does not create your application’s API, user-profile endpoint, or React routing fallback. Add those deliberately, and return only user data the SPA actually needs. Spring’s reference documents the flow and configuration: OAuth2 Login.

4. Connect the SPA to the session

Serve the SPA and BFF on the same origin where practical. The browser follows Spring’s login redirect, then sends the session cookie on same-origin requests. Provide a small authenticated endpoint such as /api/me for the SPA to discover the signed-in user’s application profile, and return a clear unauthenticated response or initiate the login redirect according to the route. Do not return provider access or refresh tokens to React merely to make the UI display a user’s identity.

Store provider credentials outside source control, keep session state server-side, and configure the session cookie as Secure and HttpOnly, with an appropriate SameSite policy for your actual login and deployment flows. Spring Security does not itself directly control the servlet session cookie’s SameSite setting; configure it in the servlet container, framework, or proxy layer that owns that cookie.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I store OAuth tokens in localStorage?

For a BFF, no: the point is to keep OAuth tokens off the browser. For a direct SPA, JavaScript necessarily handles tokens, so putting them in localStorage makes them accessible to any script executing in the page, including injected script after an XSS flaw. Browser storage choices cannot make a public client confidential. Minimize token exposure and lifetime according to the provider’s supported design, avoid persistent storage unless the chosen client library and threat model specifically require it, and prioritize preventing script injection. A BFF reduces the impact of token exfiltration but does not make XSS harmless: injected code can still act through the user’s active session.

How do I use PKCE with Spring Security?

For a direct browser application, register a public client with the identity provider and use Authorization Code with Proof Key for Code Exchange (PKCE). PKCE binds the authorization-code exchange to a verifier held by the initiating client, mitigating authorization-code interception; it does not turn browser code into a confidential client or protect a token from malicious JavaScript already running in the page. Do not use the Implicit flow as a shortcut, and never embed a confidential client secret in React code.

Spring Security’s OAuth2 Client documentation says it automatically uses PKCE for an authorization-code registration with no client secret and client-authentication-method: none, or when requireProofKey is enabled. That setting applies to a Spring OAuth2 Client registration; it does not automatically configure an unrelated JavaScript OAuth library running in React. In a direct SPA architecture, configure the browser OAuth client and provider registration for PKCE, and confirm the provider supports the selected public-client arrangement. See Spring authorization grant support and the IETF browser-app guidance, RFC 10017.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Does CORS protect my API from CSRF?

No. CORS controls whether browser JavaScript from one origin can read or make certain cross-origin requests under the browser’s rules. It is not authentication, and it is not a CSRF defense. A browser attaches eligible cookies automatically, so a malicious site may be able to cause a victim’s browser to send a state-changing cookie-authenticated request even when that site cannot read the response. RFC 9700 states: “Clients MUST prevent Cross-Site Request Forgery (CSRF).”

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Keep CSRF defenses for cookie-authenticated changes

Keep Spring Security’s CSRF protection enabled for unsafe methods such as POST, PUT, PATCH, and DELETE when the browser authenticates with a session cookie. Make the CSRF token available to the SPA through a deliberate server mechanism, then require the client to send it in the expected request header or parameter. Align the token repository and request handler with the Spring Security version and the application’s delivery pattern; do not assume that merely setting a CSRF cookie proves the request was intentionally initiated by the application. Spring documents the token handling and SPA considerations in its CSRF protection reference.

Use CORS only for actual cross-origin browser access

If the SPA and API have different origins, allow only the required frontend origin, methods, and headers. Credentialed cookie requests need explicit origin handling; do not treat a wildcard origin as a safe way to allow credentials. CORS configuration must work with the security filter chain and preflight requests, but a permissive or restrictive CORS policy does not replace CSRF tokens for cookie-authenticated state changes. OAuth authorization endpoints are normally redirect destinations in the login flow, not APIs that should be made CORS-readable. RFC 9700 distinguishes browser-accessed endpoints from the authorization endpoint: OAuth 2.0 Security Best Current Practice.

What changes if the SPA calls APIs directly?

With a direct public SPA client, React obtains the authorization code using PKCE, exchanges it through the provider-supported browser flow, and sends the resulting access token to an API. The API must validate that token; configuring OAuth2 Login on Spring does not validate bearer tokens on unrelated API requests. For Spring APIs, configure OAuth2 Resource Server with the trusted issuer or verification keys appropriate to the token format, and validate the expected issuer, audience, expiry, and other relevant claims rather than accepting any well-formed JWT. Opaque tokens require the configured introspection approach instead of local JWT signature validation.

The identity provider and API must support the browser’s cross-origin requests where origins differ. Apply a narrow CORS policy at the API. Token-bearing requests still require an XSS-conscious design because page JavaScript can use or steal tokens while it runs; PKCE protects the authorization-code exchange, not a compromised page. This alternative is not a reason to place a confidential client secret in the browser.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Production checklist: callback, cookies, and deployment

  • Use HTTPS end to end for deployed authorization responses and application traffic; RFC 9700 says authorization responses must not use unencrypted network connections.
  • Register exact redirect URIs and ensure the externally visible callback matches Spring’s generated callback, especially behind a proxy.
  • Keep confidential client secrets on the server and out of logs, source control, and browser bundles.
  • Set secure session-cookie attributes in the component that controls the servlet cookie, and verify the SameSite behavior required by the provider’s redirect flow.
  • Retain CSRF protection for unsafe cookie-authenticated operations; configure token exposure and validation to match the selected Spring Security version.
  • Configure CORS only for real cross-origin browser use, with the minimum origins, methods, headers, and credential access needed.
  • Keep the roles separate: OAuth2 Login for sign-in, OAuth2 Client for outbound protected calls, and OAuth2 Resource Server for validating inbound access tokens.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.