Skip to content

OAuth 2.0


OAuth 2.0 Overview

OAuth 2.0 & OIDC Overview

What is OAuth 2.0? - Authorization framework (NOT authentication) - Allows third-party limited access to resources - Without sharing credentials

Key Concepts: - Delegated authorization - Access tokens (not identity) - Scopes define permissions

Example Use Case: "Allow Trello to access my Google Calendar" - Trello doesn't need my Google password - Trello only gets calendar access (scoped) - I can revoke Trello's access anytime


OAuth 2.0 Roles

Resource Owner - The user who owns the data - Grants permission to access resources - Example: You (granting Trello access to calendar)

Client - Application requesting access - Has client_id and client_secret - Example: Trello application

Authorization Server - Issues tokens after authentication - Validates credentials and consent - Example: Google OAuth server

Resource Server - Hosts protected resources - Accepts and validates access tokens - Example: Google Calendar API


OAuth 2.0 Grant Types

OAuth 2.0 Grant Types


Authorization Code Flow (with PKCE)

Authorization Code Flow

Flow Steps: 1. User clicks Login 2. Client generates code_verifier and code_challenge (PKCE) 3. Redirect to Auth Server 4. Auth request (client_id, redirect_uri, scope, state, code_challenge) 5. Auth Server shows Login page 6. User logs in and grants consent 7. Auth Server redirects with authorization_code 8. Callback to Client 9. Client exchanges code for tokens (+ code_verifier) 10. Auth Server returns Access token and Refresh token 11. User is authenticated

PKCE Implementation

// Client-side: Generate PKCE values
function generatePKCE() {
    // Generate random code_verifier (43-128 characters)
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    const codeVerifier = base64URLEncode(array);

    // Generate code_challenge = SHA256(code_verifier)
    const encoder = new TextEncoder();
    const data = encoder.encode(codeVerifier);
    const digest = await crypto.subtle.digest('SHA-256', data);
    const codeChallenge = base64URLEncode(new Uint8Array(digest));

    return { codeVerifier, codeChallenge };
}

// Step 1: Authorization request
const { codeVerifier, codeChallenge } = await generatePKCE();
const state = generateRandomString();

// Store for later
sessionStorage.setItem('code_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);

const authUrl = `https://auth.example.com/authorize?` +
    `client_id=${CLIENT_ID}&` +
    `redirect_uri=${REDIRECT_URI}&` +
    `response_type=code&` +
    `scope=openid profile email&` +
    `state=${state}&` +
    `code_challenge=${codeChallenge}&` +
    `code_challenge_method=S256`;

window.location.href = authUrl;
// Server-side: Token exchange
@PostMapping("/oauth/callback")
public TokenResponse handleCallback(@RequestParam String code,
                                    @RequestParam String state,
                                    HttpSession session) {
    // Verify state
    String savedState = (String) session.getAttribute("oauth_state");
    if (!state.equals(savedState)) {
        throw new SecurityException("Invalid state parameter");
    }

    String codeVerifier = (String) session.getAttribute("code_verifier");

    // Exchange code for tokens
    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("grant_type", "authorization_code");
    params.add("code", code);
    params.add("redirect_uri", redirectUri);
    params.add("client_id", clientId);
    params.add("client_secret", clientSecret);
    params.add("code_verifier", codeVerifier);

    ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
        "https://auth.example.com/token",
        new HttpEntity<>(params, headers),
        TokenResponse.class
    );

    return response.getBody();
}

Client Credentials Flow

Client Credentials Flow

@Service
public class OAuth2ClientService {

    private final WebClient webClient;
    private final String tokenEndpoint;
    private final String clientId;
    private final String clientSecret;

    private String accessToken;
    private Instant tokenExpiry;

    public String getAccessToken() {
        if (accessToken == null || Instant.now().isAfter(tokenExpiry)) {
            refreshToken();
        }
        return accessToken;
    }

    private synchronized void refreshToken() {
        TokenResponse response = webClient.post()
            .uri(tokenEndpoint)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .body(BodyInserters.fromFormData("grant_type", "client_credentials")
                .with("client_id", clientId)
                .with("client_secret", clientSecret)
                .with("scope", "api.read api.write"))
            .retrieve()
            .bodyToMono(TokenResponse.class)
            .block();

        this.accessToken = response.getAccessToken();
        // Refresh slightly before actual expiry
        this.tokenExpiry = Instant.now()
            .plusSeconds(response.getExpiresIn() - 60);
    }
}

Device Code Flow

Device Code Flow


OpenID Connect (OIDC)

OpenID Connect Overview

ID Token Claims

{
  "iss": "https://auth.example.com",     // Issuer
  "sub": "user123",                       // Subject (user ID)
  "aud": "my-client-id",                  // Audience
  "exp": 1704067200,                      // Expiration
  "iat": 1704063600,                      // Issued at
  "auth_time": 1704063500,                // When user authenticated
  "nonce": "abc123",                      // Replay protection
  "name": "John Doe",                     // User's full name
  "email": "[email protected]",            // User's email
  "email_verified": true,                 // Email verification status
  "picture": "https://example.com/john.jpg"
}

OIDC Scopes

Scope Claims
openid sub (required for OIDC)
profile name, family_name, given_name, nickname, preferred_username, picture, updated_at
email email, email_verified
address address (JSON object)
phone phone_number, phone_number_verified

OAuth 2.0 Security Best Practices

OAuth 2.0 Security Best Practices


Spring Security OAuth 2.0

OAuth 2.0 Client (Login with OAuth)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
            );
        return http.build();
    }

    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

// application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}
            scope: openid, profile, email
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: user:email

Resource Server (API Protection)

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        return http.build();
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter =
            new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthoritiesClaimName("roles");
        authoritiesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
}

// application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          # or
          jwk-set-uri: https://auth.example.com/.well-known/jwks.json

Authorization Server (Spring Authorization Server)

@Configuration
public class AuthorizationServerConfig {

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
            throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .oidc(Customizer.withDefaults());

        http.exceptionHandling(exceptions -> exceptions
            .defaultAuthenticationEntryPointFor(
                new LoginUrlAuthenticationEntryPoint("/login"),
                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
            )
        );

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient webClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("web-client")
            .clientSecret("{noop}secret")
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://localhost:8080/callback")
            .scope(OidcScopes.OPENID)
            .scope(OidcScopes.PROFILE)
            .scope("api.read")
            .clientSettings(ClientSettings.builder()
                .requireProofKey(true)  // PKCE required
                .build())
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(15))
                .refreshTokenTimeToLive(Duration.ofDays(7))
                .build())
            .build();

        return new InMemoryRegisteredClientRepository(webClient);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        RSAKey rsaKey = generateRsaKey();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }
}

Token Introspection vs JWT Validation

Aspect Introspection Local JWT Validation
Method Call auth server to validate Validate signature locally using public key
Revocation Real-time revocation check Can't detect revocation until expiry
Performance Network latency per request Fast (no network call)
Token Format Works with opaque tokens Requires JWT format
Best For Sensitive operations High-throughput APIs

Common Interview Questions

  1. What is OAuth 2.0?
  2. Authorization framework for delegated access
  3. Allows third-party apps limited resource access
  4. Without sharing credentials

  5. OAuth 2.0 vs OIDC?

  6. OAuth 2.0: Authorization (access tokens)
  7. OIDC: Authentication layer (ID tokens with identity)
  8. OIDC builds on top of OAuth 2.0

  9. Why use PKCE?

  10. Protects authorization code from interception
  11. Required for public clients (SPAs, mobile)
  12. Recommended for all clients now

  13. How to handle token revocation?

  14. Short-lived access tokens
  15. Refresh token rotation
  16. Token introspection endpoint
  17. Revocation endpoint

  18. Where to store tokens in browser?

  19. Access token: Memory only (not localStorage)
  20. Refresh token: HttpOnly secure cookie
  21. Avoid localStorage (XSS vulnerable)

  • *