Skip to content

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:

RoleResponsibility
Resource OwnerThe user who grants access to their protected data
ClientThe application requesting access on behalf of the resource owner
Authorization ServerIssues tokens after authenticating the resource owner and obtaining consent
Resource ServerHosts the protected resources; validates access tokens on each request

OAuth 2.0 Flows

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-configuration

Spring 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

  1. Always use PKCE for public clients — Authorization Code + PKCE (RFC 7636) prevents authorization code interception in mobile and SPA contexts. The code_verifier is never transmitted until the token exchange.

  2. 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.

  3. 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.

  4. 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.

  5. Validate the state parameter — Always generate a cryptographically random state value and verify it on return. This protects against CSRF attacks on the authorization endpoint.

  6. Store tokens securely — In browsers, prefer HttpOnly + Secure cookies over localStorage for refresh tokens. Access tokens can be held in memory to reduce XSS exposure.

  7. 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.

  8. Deprecate Implicit and ROPC grants — Migrate any existing Implicit or Resource Owner Password grants to Authorization Code + PKCE per OAuth 2.1 recommendations.