What is OAuth 2.0?
OAuth 2.0 is an open standard that lets one application access a user's data in another application without ever seeing the user's password. Instead of a password, the application receives a short-lived access token that allows only the actions the user agreed to.
Every time you click "Sign in with Google" or allow an app to read your GitHub repositories, OAuth 2.0 is working in the background. In this tutorial you will learn how it works in plain language, which flow to use when, and how to use it in a Spring Boot 3 application.
1. What Is OAuth 2.0?
OAuth 2.0 is an authorization framework. It answers the question: "May this application do this action on this user's behalf?"
The key idea is delegated access. The user stays in control and gives an application a limited, temporary permission, instead of handing over their username and password. The permission can be narrow (for example, "read my calendar" but not "delete events") and can be taken back at any time.
2. A Simple Real-Life Example
Think of a hotel key card. At check-in, the front desk confirms who you are and gives you a card. The card opens only your room and the gym, and only until your checkout date. You never get the master key, and the hotel can cancel your card at any moment.
OAuth 2.0 works the same way. Here is what happens when you click "Sign in with Google" on a photo printing website:
- The website sends you to Google's sign-in page.
- You sign in on Google's page, so the website never sees your password.
- Google asks: "Allow PhotoPrint to see your photos?" You click Allow.
- Google sends the website a token that can only read your photos, for a short time.
- The website uses that token to fetch your photos and print them.
3. Why Do We Need OAuth 2.0?
Before OAuth, if an app wanted your data from another service, it simply asked for your password. This caused serious problems:
| Sharing Your Password | Using OAuth 2.0 |
|---|---|
| The app gets full access to your account | The app gets only the permissions you approve |
| Access lasts until you change your password | Tokens expire automatically |
| To block one app, you must change your password everywhere | You can revoke one app without affecting others |
| Every app stores your password, so one leak exposes it | Your password stays with the service you trust |
| Two-factor login is hard to support | Login, including two-factor checks, happens on the provider's page |
4. The Four Roles in OAuth 2.0
Every OAuth 2.0 exchange involves four parties:
| Role | What It Does | In Our Example |
|---|---|---|
| Resource Owner | The user who owns the data and gives permission | You |
| Client | The application that wants access to the data | The photo printing website |
| Authorization Server | Checks the user's identity, asks for consent and issues tokens | Google's sign-in service |
| Resource Server | Holds the protected data and accepts valid tokens | The Google Photos API |
In small systems, the authorization server and resource server can be part of the same application. In larger systems they are usually separate, and popular authorization servers include Keycloak, Okta, Auth0, Microsoft Entra ID and Spring Authorization Server.
5. Important Terms
| Term | Meaning |
|---|---|
| Client ID | A public name for the application, given when you register it with the authorization server. |
| Client Secret | A password for the application itself. Only server-side apps can keep it safe. |
| Redirect URI | The address where the authorization server sends the user back after they approve. |
| Scope | A named permission the client asks for, such as photos.read or email. |
| Consent | The screen where the user sees the requested scopes and clicks Allow or Deny. |
| Authorization Code | A one-time, short-lived code that the client swaps for tokens. |
| State | A random value the client sends and checks on return, to block forged requests. |
| Access Token | The key the client sends to the resource server to access data. |
| Refresh Token | A longer-lived token used to get new access tokens without a new login. |
6. Access Tokens and Refresh Tokens
An access token is like a cinema ticket: it is valid for one show and then expires. The client sends it with every API request in the Authorization header:
GET /api/photos HTTP/1.1
Host: photos.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
The word Bearer means "whoever holds this token may use it", so tokens must be protected like passwords and only sent over HTTPS.
| Access Token | Refresh Token | |
|---|---|---|
| Purpose | Call protected APIs | Get a new access token |
| Sent to | The resource server (API) | Only the authorization server |
| Typical lifetime | Minutes (often 5 to 60) | Hours, days or weeks |
| If stolen | Useful only until it expires | Dangerous, so it must be stored very carefully |
An access token can be a JWT (a signed token the API can verify by itself) or an opaque random string (the API asks the authorization server whether it is valid). OAuth 2.0 allows both.
7. How OAuth 2.0 Works Step by Step
The most common and secure flow is the Authorization Code flow. Here it is with real HTTP messages, using a made-up photo app.
Step 1: The client sends the user to the authorization server
GET https://auth.example.com/oauth2/authorize
?response_type=code
&client_id=photo-app
&redirect_uri=https://photo.example.com/callback
&scope=photos.read
&state=k8Df2xQ
&code_challenge=RrazcE-fHKlTxaTi-HpO391O8vR7koePATMUSrv_eBE
&code_challenge_method=S256
(The URL is split across lines only for readability.)
Step 2: The user signs in and approves
The user signs in on the authorization server's page and sees a consent screen listing the requested scopes. They click Allow.
Step 3: The authorization server returns a code
GET https://photo.example.com/callback?code=Qx7mT2pL9a&state=k8Df2xQ
The client first checks that state matches the value it sent. If not, it rejects the response.
Step 4: The client swaps the code for tokens
This call goes directly from the client's server to the authorization server, not through the browser:
POST https://auth.example.com/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic cGhvdG8tYXBwOnNlY3JldA==
grant_type=authorization_code
&code=Qx7mT2pL9a
&redirect_uri=https://photo.example.com/callback
&code_verifier=RNAU_64bPW8ibaFwHjE5cvsggYrXH92FRtzHnvaGbvg
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "r2Lk9vXp4QmZ...",
"scope": "photos.read"
}
Step 5: The client calls the API with the access token
The client sends Authorization: Bearer <access_token> to the resource server. The resource server checks the token's signature, expiry and scope, and then returns the photos.
8. PKCE: Protecting the Code
The authorization code travels through the browser, so an attacker might try to steal it. PKCE (Proof Key for Code Exchange, pronounced "pixy") makes a stolen code useless:
- Before starting, the client creates a random secret called the code verifier.
- It sends only a hash of it, the code challenge, in step 1.
- In step 4, it sends the original verifier. The authorization server hashes it and checks that it matches.
An attacker who steals the code does not have the verifier, so they cannot exchange it for tokens. PKCE is now recommended for every client, including server-side apps. Here is how the two values are created in plain Java:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
public class PkceGenerator {
private static final Base64.Encoder URL_ENCODER = Base64.getUrlEncoder().withoutPadding();
// A random, high-entropy secret that stays with the client
public static String createCodeVerifier() {
byte[] randomBytes = new byte[32];
new SecureRandom().nextBytes(randomBytes);
return URL_ENCODER.encodeToString(randomBytes);
}
// The SHA-256 hash of the verifier, sent in the authorization request
public static String createCodeChallenge(String codeVerifier) throws NoSuchAlgorithmException {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] hash = sha256.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
return URL_ENCODER.encodeToString(hash);
}
public static void main(String[] args) throws NoSuchAlgorithmException {
String verifier = createCodeVerifier();
String challenge = createCodeChallenge(verifier);
System.out.println("code_verifier = " + verifier);
System.out.println("code_challenge = " + challenge);
}
}
In Spring Boot you rarely write this yourself, because Spring Security's OAuth2 client can add PKCE for you. It is still useful to know what happens behind the scenes.
9. OAuth 2.0 Grant Types
A grant type is the method a client uses to get an access token. Choose the one that matches your type of application:
| Grant Type | Is a User Involved? | Use It For | Status |
|---|---|---|---|
| Authorization Code + PKCE | Yes | Web apps, single-page apps, mobile and desktop apps | Recommended |
| Client Credentials | No | Backend services and scheduled jobs calling other APIs | Recommended |
| Refresh Token | No new login | Getting a fresh access token when the old one expires | Recommended |
| Device Code | Yes, on a second device | Smart TVs, consoles and command-line tools without a browser | Recommended |
| Implicit | Yes | Old single-page apps | Not recommended |
| Password (Resource Owner Password) | Yes | Old first-party apps | Not recommended |
The implicit grant returned tokens directly in the browser URL, where they could leak. The password grant made the app collect the user's password, which is exactly what OAuth was designed to avoid. Use Authorization Code with PKCE instead of both.
10. OAuth 2.0 vs OpenID Connect
OAuth 2.0 is about authorization ("what may this app do?"). It does not define a standard way to tell the app who the user is. OpenID Connect (OIDC) is a thin layer on top of OAuth 2.0 that adds exactly that.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| Main question | What can this app access? | Who is the user? |
| Main token | Access token | ID token (always a JWT), plus the access token |
| Token is meant for | The API (resource server) | The client application |
| How to enable | Request API scopes | Add the openid scope |
| Typical use | Securing APIs | "Sign in with Google" style login |
A simple rule: use the ID token to learn who logged in, and the access token to call APIs. Never send the ID token to an API as if it were an access token.
11. Spring Boot: Login with Google
Spring Boot makes social login very simple with the OAuth2 Client starter. First, create OAuth credentials in the Google Cloud Console and add this redirect URI to them: http://localhost:8080/login/oauth2/code/google.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Add the credentials as environment variables, never directly in the file:
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
Spring Boot already knows Google's authorization and token addresses, so no other settings are needed. Next, turn on OAuth2 login:
package com.javabytechie.photos.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(Customizer.withDefaults())
.logout(logout -> logout.logoutSuccessUrl("/"));
return http.build();
}
}
Finally, read the logged-in user's details. Because we requested the openid scope, Spring gives us an OidcUser built from the ID token:
package com.javabytechie.photos.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class ProfileController {
@GetMapping("/profile")
public Map<String, Object> profile(@AuthenticationPrincipal OidcUser user) {
return Map.of(
"name", user.getFullName(),
"email", user.getEmail(),
"subject", user.getSubject()
);
}
}
Start the app and open http://localhost:8080/profile. You are sent to Google, you sign in, and you return to see your name and email. Spring handles the redirect, the state check, the code exchange and the token validation for you.
12. Spring Boot: Protecting an API
An API that accepts access tokens is a resource server. With the resource server starter, Spring Security validates every incoming JWT for you.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
Tell Spring where the authorization server is. On startup, Spring reads the server's public keys from this address and uses them to check token signatures:
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com
Spring turns each scope in the token into an authority with the prefix SCOPE_. Use these to protect your endpoints:
package com.javabytechie.orders.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_orders.read")
.requestMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("SCOPE_orders.write")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
Inside a controller, you can read claims from the validated token:
package com.javabytechie.orders.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public Map<String, Object> myOrders(@AuthenticationPrincipal Jwt jwt) {
String userId = jwt.getSubject(); // "sub" claim
String scopes = jwt.getClaimAsString("scope"); // e.g. "orders.read orders.write"
return Map.of(
"user", userId,
"grantedScopes", scopes,
"orders", List.of("ORD-1001", "ORD-1002")
);
}
}
If the token is missing, expired or has a bad signature, Spring returns 401 Unauthorized. If the token is valid but lacks the required scope, it returns 403 Forbidden.
13. Spring Boot: Service-to-Service Calls
When an Order Service needs data from an Inventory Service with no user involved, it uses the Client Credentials grant. The service logs in as itself with its own client ID and secret.
Add spring-boot-starter-oauth2-client to the calling service and register the client:
spring.security.oauth2.client.registration.inventory-client.client-id=order-service
spring.security.oauth2.client.registration.inventory-client.client-secret=${INVENTORY_CLIENT_SECRET}
spring.security.oauth2.client.registration.inventory-client.authorization-grant-type=client_credentials
spring.security.oauth2.client.registration.inventory-client.scope=inventory.read
spring.security.oauth2.client.registration.inventory-client.provider=my-auth-server
spring.security.oauth2.client.provider.my-auth-server.issuer-uri=https://auth.example.com
Next, create a RestClient that fetches a token, caches it, renews it when it expires, and adds it to every request. This uses OAuth2ClientHttpRequestInterceptor, available from Spring Boot 3.4 (Spring Security 6.4):
package com.javabytechie.orders.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor;
import org.springframework.web.client.RestClient;
@Configuration
public class RestClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository registrations,
OAuth2AuthorizedClientService clientService) {
AuthorizedClientServiceOAuth2AuthorizedClientManager manager =
new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrations, clientService);
manager.setAuthorizedClientProvider(
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build());
return manager;
}
@Bean
public RestClient inventoryRestClient(RestClient.Builder builder,
OAuth2AuthorizedClientManager manager) {
return builder
.baseUrl("http://localhost:8081")
.requestInterceptor(new OAuth2ClientHttpRequestInterceptor(manager))
.build();
}
}
Now each call only needs to name the client registration to use:
package com.javabytechie.orders.client;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import static org.springframework.security.oauth2.client.web.client.RequestAttributeClientRegistrationIdResolver.clientRegistrationId;
@Service
public class InventoryClient {
private final RestClient inventoryRestClient;
public InventoryClient(RestClient inventoryRestClient) {
this.inventoryRestClient = inventoryRestClient;
}
public StockLevel getStock(String productCode) {
return inventoryRestClient.get()
.uri("/api/stock/{code}", productCode)
.attributes(clientRegistrationId("inventory-client"))
.retrieve()
.body(StockLevel.class);
}
public record StockLevel(String productCode, int quantity) {
}
}
The Inventory Service is a normal resource server (as in section 12) that requires SCOPE_inventory.read.
14. Testing with curl
You can try the Client Credentials flow by hand. First, ask the authorization server for a token:
curl -X POST https://auth.example.com/oauth2/token \
-u order-service:my-secret \
-d grant_type=client_credentials \
-d scope=orders.read
{
"access_token": "eyJraWQiOiJhYmMxMjMiLCJhbGciOiJSUzI1NiJ9...",
"token_type": "Bearer",
"expires_in": 300,
"scope": "orders.read"
}
Then call the protected API with that token:
# 200 OK: valid token with the orders.read scope
curl -H "Authorization: Bearer <access_token>" http://localhost:8080/api/orders
# 401 Unauthorized: no token at all
curl http://localhost:8080/api/orders
Notice there is no refresh_token in the response. In the Client Credentials flow the service can simply ask for a new token when the old one expires.
15. Where Is OAuth 2.0 Used?
- Social login: "Sign in with Google, GitHub, Microsoft or Facebook" (usually with OpenID Connect).
- Third-party integrations: a project tool posting messages to your team chat, or an accounting app reading bank data.
- Public APIs: most large platforms protect their developer APIs with OAuth 2.0.
- Microservices: services check access tokens and call each other with client credentials.
- Mobile and single-page apps: secure login without storing passwords on the device.
- Single sign-on (SSO): one company login for many internal applications.
16. Benefits of OAuth 2.0
- Passwords stay private: only the authorization server ever sees them.
- Limited access: scopes give each app only the permissions it needs.
- Short-lived access: tokens expire, which limits the damage if one leaks.
- Easy to revoke: users can remove one app's access without changing their password.
- Central security: login rules, two-factor checks and auditing live in one place.
- Widely supported: every major platform and framework, including Spring Security, supports it.
17. Security Best Practices
- Use Authorization Code with PKCE for every app where a user logs in.
- Always use HTTPS. Bearer tokens sent over plain HTTP can be stolen.
- Register exact redirect URIs and avoid wildcards.
- Always check the
stateparameter (Spring Security does this for you). - Ask for the smallest scopes you need, such as
photos.readinstead ofphotos.write. - Keep access tokens short-lived and protect refresh tokens carefully. Rotate refresh tokens when possible.
- Keep client secrets out of code. Use environment variables or a secrets manager.
- Validate every token on the API: signature, issuer, audience, expiry and scope.
- Avoid storing tokens in browser
localStorage. For browser apps, prefer a backend (often called a BFF, Backend for Frontend) that keeps tokens on the server.
18. Common Mistakes to Avoid
- Treating OAuth 2.0 as a login protocol. Use OpenID Connect when you need to know who the user is.
- Sending the ID token to APIs. APIs should receive access tokens only.
- Using the implicit or password grants in new applications.
- Putting a client secret in a mobile or JavaScript app. Anyone can extract it. These apps are public clients and must use PKCE instead.
- Skipping the audience check, so a token meant for one API is accepted by another.
- Logging tokens in application logs or error messages.
- Asking for every scope "just in case". Users trust apps less when the consent screen asks for too much.
19. Conclusion
OAuth 2.0 lets applications work with a user's data without ever handling their password. The user approves a limited set of scopes, the authorization server issues a short-lived access token, and the API checks that token on every request.
For new projects, keep it simple: use Authorization Code with PKCE when a user is involved, Client Credentials between services, and OpenID Connect when you need login. In Spring Boot, the OAuth2 Client and Resource Server starters handle most of the hard work with just a few lines of configuration.
If you have ever clicked "Sign in with Google", you have already used OAuth 2.0, and now you know exactly what happens behind that button.
Frequently Asked Questions (FAQ)
Q1. What is OAuth 2.0 in simple words?
OAuth 2.0 is a standard way for a user to let one app access their data in another app without sharing their password. The app receives a temporary access token with limited permissions instead.
Q2. Is OAuth 2.0 used for authentication or authorization?
OAuth 2.0 is for authorization: deciding what an app may access. It does not tell the app who the user is in a standard way.
For login (authentication), use OpenID Connect, which is built on top of OAuth 2.0.
Q3. What is the difference between OAuth 2.0 and OpenID Connect?
OAuth 2.0 issues access tokens so apps can call APIs. OpenID Connect adds an ID token that tells the app who the user is, such as their ID, name and email.
You turn on OpenID Connect by adding the openid scope to the request.
Q4. What is an access token?
An access token is a string the client sends to an API to prove it has permission. It usually lasts a few minutes, allows only certain scopes, and is sent in the HTTP header:
Authorization: Bearer <access_token>
Q5. What is a refresh token, and why do we need it?
Access tokens expire quickly for safety. A refresh token lets the client get a new access token without asking the user to log in again.
It is sent only to the authorization server, never to APIs, and must be stored very carefully because it lives much longer.
Q6. What are scopes in OAuth 2.0?
Scopes are named permissions, such as photos.read, email or orders.write. The client asks for scopes, the user approves them, and the access token carries only the approved ones.
In Spring Security, each scope becomes an authority like SCOPE_photos.read.
Q7. Which grant type should I use?
- A user logs in (web, mobile, single-page or desktop app): Authorization Code with PKCE.
- A service calls another service with no user: Client Credentials.
- A device without a browser (smart TV, CLI tool): Device Code.
- The access token expired: Refresh Token.
Q8. Why is the implicit grant no longer recommended?
The implicit grant put the access token directly in the browser URL. From there it could leak through browser history, logs or other scripts on the page.
The Authorization Code flow with PKCE is safer and now works well in browsers, so it replaces the implicit grant.
Q9. What is PKCE, and do I need it?
PKCE (Proof Key for Code Exchange) makes a stolen authorization code useless. The client sends a hash of a random secret at the start, and the secret itself when exchanging the code. Only the real client knows the secret.
Yes, use it. It is required for mobile and single-page apps and recommended for all clients.
Q10. What is the state parameter used for?
state is a random value the client sends to the authorization server and checks when the user returns. If the values do not match, the response is rejected.
This stops attackers from tricking a user's browser into finishing a login the user never started (a CSRF attack). Spring Security creates and checks it automatically.
Q11. What is a redirect URI, and why must it match exactly?
The redirect URI is where the authorization server sends the user, along with the code, after they approve. It must match one of the addresses registered for the client.
An exact match prevents attackers from redirecting codes to a site they control. In Spring Boot the default is:
{baseUrl}/login/oauth2/code/{registrationId}
Q12. What is the difference between a client ID and a client secret?
The client ID is a public name for your app. The client secret is a password for your app.
Only confidential clients (apps running on a server) can keep a secret safe. Public clients (mobile and browser apps) cannot, so they use PKCE instead of a secret.
Q13. Is an access token always a JWT?
No. OAuth 2.0 does not fix the token format. Many servers issue JWTs, which the API can verify on its own using the server's public keys. Others issue opaque tokens, which the API checks by calling the authorization server's introspection endpoint.
Spring Security supports both: .jwt(...) and .opaqueToken(...).
Q14. How long should an access token be valid?
Keep it short: usually between 5 and 60 minutes. A short life limits the damage if a token is stolen. Use refresh tokens, or the Client Credentials flow between services, to get new tokens without bothering the user.
Q15. Where should a browser app store tokens?
Avoid localStorage, because any script running on the page can read it. The safest approach is a Backend for Frontend (BFF): your own server-side app handles OAuth, keeps the tokens on the server, and gives the browser only a secure, HTTP-only session cookie.
Q16. How does a Spring Boot API validate a JWT access token?
Set the issuer address, and Spring Security does the rest:
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://auth.example.com
Spring downloads the server's public keys, then checks each token's signature, issuer and expiry. Invalid tokens receive a 401 response.
Q17. How do I add "Sign in with Google" to a Spring Boot app?
Add spring-boot-starter-oauth2-client, set your Google credentials, and call oauth2Login() in your security configuration:
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=openid,profile,email
See section 11 for the full example.
Q18. How do microservices call each other securely with OAuth 2.0?
The calling service uses the Client Credentials grant to get an access token for itself, then sends it to the other service. The receiving service validates the token and checks its scope, for example inventory.read.
In Spring Boot 3.4+, OAuth2ClientHttpRequestInterceptor adds and renews the token on every RestClient call (see section 13).
Q19. Can a user take back the access they gave to an app?
Yes. Most providers have a "connected apps" page in the account settings where users can remove an app. The authorization server then rejects the app's refresh token, so the app loses access once its current access token expires.
Apps can also cancel tokens themselves, for example on logout, by calling the server's revocation endpoint.
Q20. What is OAuth 2.1?
OAuth 2.1 is an update to OAuth 2.0 that is still being finalized. It does not add a new design. Instead, it collects the best practices learned over the years into one document:
- PKCE is required for the Authorization Code flow.
- The implicit and password grants are removed.
- Redirect URIs must match exactly.
- Bearer tokens must not be sent in URL query strings.
If you follow the best practices in this tutorial, your application is already close to OAuth 2.1.