Appearance
OAuth 2.0 Authorization Delegation
Introduction
OAuth 2.0 (RFC 6749) is an industry-standard authorization framework that enables a third-party application to obtain limited access to an HTTP service on behalf of a resource owner, without exposing the owner's credentials. It separates the role of the client from that of the resource owner and introduces a dedicated authorization server to issue scoped access tokens. OAuth 2.0 is the foundation of delegated authorization across modern APIs, mobile applications, and federated identity systems.
OAuth 2.0 Roles
OAuth 2.0 defines four distinct roles:
| Role | Responsibility |
|---|---|
| Resource Owner | The user who grants access to their protected data |
| Client | The application requesting access on behalf of the resource owner |
| Authorization Server | Issues tokens after authenticating the resource owner and obtaining consent |
| Resource Server | Hosts the protected resources; validates access tokens on each request |
OAuth 2.0 Flows
Authorization Code Flow (Recommended for Web Apps)
The Authorization Code flow is the most secure grant type. The authorization code is short-lived and exchanged for tokens on the back channel, keeping tokens out of the browser.
Client Credentials Flow (Machine-to-Machine)
Used when the client is also the resource owner, typically for server-to-server API calls.
Resource Owner Password Credentials (Deprecated)
This grant passes the user's credentials directly to the client — use only for highly trusted first-party applications migrating away from basic auth. RFC 9700 (OAuth 2.1) removes this grant entirely.
Implicit Flow (Deprecated)
The Implicit flow returned tokens directly in the fragment of the redirect URI. It is deprecated due to token leakage risks via referrer headers and browser history. Use Authorization Code + PKCE instead.
OpenID Connect and Identity Federation
OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. It adds an id_token (a signed JWT) containing claims about the authenticated user.
Token Management and Scopes
Token Lifecycle
Scope Hierarchy
Spring Boot OAuth2 Client Configuration
application.yml
yaml
spring:
security:
oauth2:
client:
registration:
my-provider:
client-id: ${OAUTH2_CLIENT_ID}
client-secret: ${OAUTH2_CLIENT_SECRET}
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
scope:
- openid
- profile
- email
provider:
my-provider:
issuer-uri: https://auth.example.com
# Discovered automatically from /.well-known/openid-configurationSpring Security OAuth2 Configuration Class
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.web.SecurityFilterChain;
/**
* Spring Security OAuth2 / OIDC configuration.
* Secures all endpoints and delegates login to the configured OIDC provider.
*/
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService())
)
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error")
)
.oauth2ResourceServer(rs -> rs
.jwt(jwt -> jwt
.jwkSetUri("https://auth.example.com/.well-known/jwks.json")
)
);
return http.build();
}
@Bean
public OidcUserService oidcUserService() {
OidcUserService delegate = new OidcUserService();
return delegate; // Extend to map custom claims to GrantedAuthority
}
}Custom OAuth2 Server Implementation
Authorization + Token Endpoints
java
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Minimal custom OAuth2 authorization server.
* For production use Spring Authorization Server instead.
*/
@RestController
@RequestMapping("/oauth2")
public class AuthorizationServerController {
private final TokenService tokenService;
// In-memory code store: code -> clientId
private final Map<String, String> authCodes = new ConcurrentHashMap<>();
public AuthorizationServerController(TokenService tokenService) {
this.tokenService = tokenService;
}
/**
* Authorization endpoint — issues an authorization code.
*/
@GetMapping("/authorize")
public ResponseEntity<Void> authorize(
@RequestParam String response_type,
@RequestParam String client_id,
@RequestParam String redirect_uri,
@RequestParam String scope,
@RequestParam String state) {
if (!"code".equals(response_type)) {
return ResponseEntity.badRequest().build();
}
String code = UUID.randomUUID().toString();
authCodes.put(code, client_id);
String location = redirect_uri
+ "?code=" + code
+ "&state=" + state;
return ResponseEntity.status(HttpStatus.FOUND)
.header("Location", location)
.build();
}
/**
* Token endpoint — exchanges authorization code or refresh token.
*/
@PostMapping("/token")
public ResponseEntity<TokenResponse> token(
@RequestParam String grant_type,
@RequestParam(required = false) String code,
@RequestParam(required = false) String refresh_token,
@RequestParam String client_id,
@RequestParam String client_secret) {
return switch (grant_type) {
case "authorization_code" -> {
if (!authCodes.containsKey(code)) {
yield ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(TokenResponse.error("invalid_grant"));
}
authCodes.remove(code);
yield ResponseEntity.ok(tokenService.issueTokens(client_id));
}
case "client_credentials" ->
ResponseEntity.ok(tokenService.issueClientToken(client_id, client_secret));
case "refresh_token" ->
ResponseEntity.ok(tokenService.refreshTokens(refresh_token));
default ->
ResponseEntity.badRequest()
.body(TokenResponse.error("unsupported_grant_type"));
};
}
}TokenResponse Model
java
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* OAuth 2.0 token response (RFC 6749 §5.1).
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record TokenResponse(
@JsonProperty("access_token") String accessToken,
@JsonProperty("token_type") String tokenType,
@JsonProperty("expires_in") Integer expiresIn,
@JsonProperty("refresh_token") String refreshToken,
@JsonProperty("scope") String scope,
@JsonProperty("id_token") String idToken,
@JsonProperty("error") String error,
@JsonProperty("error_description") String errorDescription
) {
public static TokenResponse error(String error) {
return new TokenResponse(null, null, null,
null, null, null, error, null);
}
}JWT Token Generation with JJWT
java
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import java.util.UUID;
/**
* Issues and validates JWT access tokens using JJWT.
* Dependency: io.jsonwebtoken:jjwt-api:0.12.x
*/
@Service
public class TokenService {
// In production: load from secure secret store (e.g., AWS Secrets Manager)
private final Key signingKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final int ACCESS_TTL_SECONDS = 3600; // 1 hour
private static final int REFRESH_TTL_SECONDS = 86_400 * 30; // 30 days
public TokenResponse issueTokens(String subject) {
String accessToken = buildJwt(subject, "access", ACCESS_TTL_SECONDS);
String refreshToken = buildJwt(subject, "refresh", REFRESH_TTL_SECONDS);
return new TokenResponse(accessToken, "Bearer",
ACCESS_TTL_SECONDS, refreshToken,
"openid profile email", null, null, null);
}
public TokenResponse issueClientToken(String clientId, String secret) {
// Validate client secret against stored hash (omitted for brevity)
String accessToken = buildJwt(clientId, "access", ACCESS_TTL_SECONDS);
return new TokenResponse(accessToken, "Bearer",
ACCESS_TTL_SECONDS, null,
"api:read api:write", null, null, null);
}
public TokenResponse refreshTokens(String refreshToken) {
var claims = Jwts.parserBuilder()
.setSigningKey(signingKey).build()
.parseClaimsJws(refreshToken)
.getBody();
return issueTokens(claims.getSubject());
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(signingKey)
.build().parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
private String buildJwt(String subject, String tokenUse, int ttlSeconds) {
Instant now = Instant.now();
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plusSeconds(ttlSeconds)))
.setId(UUID.randomUUID().toString())
.claim("token_use", tokenUse)
.signWith(signingKey)
.compact();
}
}Best Practices
Always use PKCE for public clients — Authorization Code + PKCE (RFC 7636) prevents authorization code interception in mobile and SPA contexts. The
code_verifieris never transmitted until the token exchange.Short-lived access tokens — Set access token expiry to 15–60 minutes. Use refresh tokens for session longevity; never extend access token lifetimes as a convenience shortcut.
Rotate refresh tokens — Implement refresh token rotation (each use issues a new refresh token and revokes the old one) to detect token theft via replay detection.
Scope minimization — Request only the scopes required for the current operation. Avoid
admin-level scopes in user-facing clients; reserve them for back-end services.Validate the
stateparameter — Always generate a cryptographically randomstatevalue and verify it on return. This protects against CSRF attacks on the authorization endpoint.Store tokens securely — In browsers, prefer
HttpOnly+Securecookies overlocalStoragefor refresh tokens. Access tokens can be held in memory to reduce XSS exposure.Token introspection for opaque tokens — If using opaque (non-JWT) tokens, implement RFC 7662 introspection on the resource server to validate tokens and retrieve claims.
Deprecate Implicit and ROPC grants — Migrate any existing Implicit or Resource Owner Password grants to Authorization Code + PKCE per OAuth 2.1 recommendations.