Skip to content
IRC-CodingIRC-Coding
OAuth 2.0Authorization CodeAccess TokenRefresh TokenSecuritySpring SecurityJWTOIDC

OAuth 2.0 Basics: Authorization Code & Access Token

OAuth 2.0 fundamentals with Authorization Code flow, Access Token implementation, and security best practices using Spring Security.

S

schutzgeist

11 min read
OAuth 2.0 Basics: Authorization Code & Access Token

OAuth 2.0 Fundamentals: Authorization Code & Access Token

OAuth 2.0 is the industry standard for delegated authorization. It lets applications access resources on behalf of a user without exposing user credentials.

What is OAuth 2.0?

OAuth 2.0 is an authorization framework that allows third-party applications to obtain limited access to protected resources without exchanging user credentials directly.

Core Concepts of OAuth 2.0

  • Delegation: Users grant applications access to their resources
  • Token-based: Tokens are used instead of passwords
  • Scope-based: Access is confined to defined scopes
  • Secure: Reduces attack surface by using short-lived tokens with limited permissions

OAuth 2.0 Roles

The Four Key Players

// OAuth 2.0 Roles as Java Classes
public class OAuthRoles {
    
    // Resource Owner: The user who owns the resource
    public static class ResourceOwner {
        private String userId;
        private String username;
        private List<String> ownedResources;
        
        public ResourceOwner(String userId, String username) {
            this.userId = userId;
            this.username = username;
            this.ownedResources = new ArrayList<>();
        }
        
        public boolean ownsResource(String resourceId) {
            return ownedResources.contains(resourceId);
        }
        
        // Grants or denies access
        public boolean authorizeAccess(String clientId, List<String> scopes) {
            // Business logic for authorization
            return true; // Simplified
        }
    }
    
    // Resource Server: Hosts the protected resources
    public static class ResourceServer {
        private Map<String, ProtectedResource> resources;
        private TokenValidator tokenValidator;
        
        public ResourceServer() {
            this.resources = new HashMap<>();
            this.tokenValidator = new JWTTokenValidator();
        }
        
        public ProtectedResource getResource(String resourceId, String accessToken) {
            if (!tokenValidator.isValid(accessToken)) {
                throw new UnauthorizedException("Invalid token");
            }
            
            TokenInfo tokenInfo = tokenValidator.getTokenInfo(accessToken);
            if (!tokenInfo.hasScope("read")) {
                throw new ForbiddenException("Insufficient scope");
            }
            
            ProtectedResource resource = resources.get(resourceId);
            if (resource == null) {
                throw new NotFoundException("Resource not found");
            }
            
            return resource;
        }
    }
    
    // Authorization Server: Authenticates the user and issues tokens
    public static class AuthorizationServer {
        private ClientRegistry clientRegistry;
        private UserRegistry userRegistry;
        private TokenService tokenService;
        
        public AuthorizationServer() {
            this.clientRegistry = new ClientRegistry();
            this.userRegistry = new UserRegistry();
            this.tokenService = new JWTTokenService();
        }
        
        public AuthorizationCode generateAuthorizationCode(
                String clientId, String userId, List<String> scopes) {
            
            if (!clientRegistry.isValidClient(clientId)) {
                throw new InvalidClientException("Unknown client");
            }
            
            AuthorizationCode code = new AuthorizationCode(
                UUID.randomUUID().toString(),
                clientId,
                userId,
                scopes,
                Instant.now().plusSeconds(600) // Valid for 10 minutes
            );
            
            return code;
        }
        
        public TokenResponse exchangeCodeForTokens(String code, String clientId, String clientSecret) {
            AuthorizationCode authCode = validateAuthorizationCode(code, clientId);
            
            if (!clientRegistry.authenticateClient(clientId, clientSecret)) {
                throw new InvalidClientException("Authentication failed");
            }
            
            // Create Access Token and Refresh Token
            String accessToken = tokenService.createAccessToken(
                authCode.getUserId(), 
                authCode.getScopes()
            );
            
            String refreshToken = tokenService.createRefreshToken(
                authCode.getUserId()
            );
            
            return new TokenResponse(accessToken, refreshToken, 3600, "Bearer");
        }
        
        private AuthorizationCode validateAuthorizationCode(String code, String clientId) {
            // Implementation to validate the authorization code
            return new AuthorizationCode(code, clientId, "user123", 
                Arrays.asList("read", "write"), Instant.now());
        }
    }
    
    // Client: The application requesting access
    public static class Client {
        private String clientId;
        private String clientSecret;
        private List<String> redirectUris;
        private List<String> allowedScopes;
        
        public Client(String clientId, String clientSecret) {
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.redirectUris = new ArrayList<>();
            this.allowedScopes = Arrays.asList("read", "write", "profile");
        }
        
        public String initiateAuthorizationFlow(List<String> requestedScopes) {
            // Create an authorization request
            String authUrl = String.format(
                "https://auth.example.com/authorize?" +
                "response_type=code&" +
                "client_id=%s&" +
                "redirect_uri=%s&" +
                "scope=%s&" +
                "state=%s",
                clientId,
                "https://client.example.com/callback",
                String.join(" ", requestedScopes),
                UUID.randomUUID().toString()
            );
            
            return authUrl;
        }
        
        public TokenResponse exchangeCodeForTokens(String code) {
            // Send token request to authorization server
            return tokenService.exchangeCode(code, clientId, clientSecret);
        }
    }
}

Authorization Code Flow

The Most Secure OAuth 2.0 Flow

public class AuthorizationCodeFlow {
    
    // Step 1: Authorization Request
    public String buildAuthorizationRequest() {
        StringBuilder request = new StringBuilder("https://auth.example.com/authorize?");
        request.append("response_type=code");
        request.append("&client_id=client123");
        request.append("&redirect_uri=https://client.example.com/callback");
        request.append("&scope=read%20write%20profile");
        request.append("&state=xyz123"); // CSRF Protection
        
        return request.toString();
    }
    
    // Step 2: User Authorization
    public void handleUserAuthorization(String userId, String clientId, List<String> scopes) {
        // User is redirected to the login page
        // After successful authentication:
        
        if (userConsentsToScopes(userId, scopes)) {
            AuthorizationCode code = authorizationServer.generateAuthorizationCode(
                clientId, userId, scopes
            );
            
            // Redirect with authorization code
            String redirectUri = String.format(
                "https://client.example.com/callback?code=%s&state=xyz123",
                code.getValue()
            );
            
            redirectToClient(redirectUri);
        } else {
            // User denied access
            redirectToClient("https://client.example.com/callback?error=access_denied");
        }
    }
    
    // Step 3: Token Exchange
    public TokenResponse exchangeCodeForTokens(String code, String state) {
        // Validate state (CSRF Protection)
        if (!isValidState(state)) {
            throw new SecurityException("Invalid state parameter");
        }
        
        // Send token request to authorization server
        Map<String, String> tokenRequest = new HashMap<>();
        tokenRequest.put("grant_type", "authorization_code");
        tokenRequest.put("code", code);
        tokenRequest.put("redirect_uri", "https://client.example.com/callback");
        tokenRequest.put("client_id", "client123");
        tokenRequest.put("client_secret", "secret123");
        
        // Send HTTP POST request
        TokenResponse response = httpClient.postForm(
            "https://auth.example.com/token", 
            tokenRequest
        );
        
        return response;
    }
    
    // Step 4: Resource Access
    public ProtectedResource accessResource(String accessToken, String resourceId) {
        // Send access token in Authorization header
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", "Bearer " + accessToken);
        
        return httpClient.get(
            "https://api.example.com/resources/" + resourceId,
            headers
        );
    }
}

Spring Security OAuth 2.0 Implementation

@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/private/**").authenticated()
                .anyRequest().denyAll()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/oauth2/authorization/my-client")
                .authorizationEndpoint(authorization -> authorization
                    .baseUri("/oauth2/authorize")
                )
                .redirectionEndpoint(redirection -> redirection
                    .baseUri("/oauth2/callback/*")
                )
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .decoder(jwtDecoder())
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
            .build();
    }
    
    @Bean
    public Converter<Jwt, UsernamePasswordAuthenticationToken> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthorityPrefix("ROLE_");
        authoritiesConverter.setAuthoritiesClaimName("roles");
        
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

// Custom OAuth2 User Service
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = 
            new DefaultOAuth2UserService();
        
        OAuth2User oAuth2User = delegate.loadUser(userRequest);
        
        // Process user information and persist to database
        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oAuth2User.getAttribute("id");
        
        User user = findOrCreateUser(provider, providerId, oAuth2User);
        
        return new CustomOAuth2User(user, oAuth2User.getAttributes());
    }
    
    private User findOrCreateUser(String provider, String providerId, OAuth2User oAuth2User) {
        // Logic to find or create a user
        return userRepository.findByProviderAndProviderId(provider, providerId)
            .orElseGet(() -> createUser(provider, providerId, oAuth2User));
    }
    
    private User createUser(String provider, String providerId, OAuth2User oAuth2User) {
        User user = new User();
        user.setProvider(provider);
        user.setProviderId(providerId);
        user.setEmail(oAuth2User.getAttribute("email"));
        user.setName(oAuth2User.getAttribute("name"));
        user.setRoles(Arrays.asList("USER"));
        
        return userRepository.save(user);
    }
}

Token Management

JWT Token Implementation

@Component
public class JWTTokenService {
    
    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration}")
    private int jwtExpiration;
    
    @Value("${jwt.refresh-expiration}")
    private int refreshExpiration;
    
    public String createAccessToken(String userId, List<String> scopes) {
        return createToken(userId, scopes, jwtExpiration);
    }
    
    public String createRefreshToken(String userId) {
        return createToken(userId, Arrays.asList("refresh"), refreshExpiration);
    }
    
    private String createToken(String userId, List<String> scopes, int expiration) {
        Instant now = Instant.now();
        Instant expiry = now.plusSeconds(expiration);
        
        return Jwts.builder()
            .setSubject(userId)
            .claim("scopes", scopes)
            .claim("type", scopes.contains("refresh") ? "refresh" : "access")
            .setIssuedAt(Date.from(now))
            .setExpiration(Date.from(expiry))
            .signWith(SignatureAlgorithm.HS256, jwtSecret)
            .compact();
    }
    
    public Claims validateToken(String token) {
        try {
            return Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();
        } catch (ExpiredJwtException e) {
            throw new TokenExpiredException("Token expired");
        } catch (JwtException e) {
            throw new InvalidTokenException("Invalid token");
        }
    }
    
    public String refreshToken(String refreshToken) {
        Claims claims = validateToken(refreshToken);
        
        if (!"refresh".equals(claims.get("type"))) {
            throw new InvalidTokenException("Not a refresh token");
        }
        
        String userId = claims.getSubject();
        List<String> scopes = Arrays.asList("read", "write"); // Default scopes
        
        return createAccessToken(userId, scopes);
    }
}

// Token Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private JWTTokenService tokenService;
    
    @PostMapping("/token")
    public ResponseEntity<TokenResponse> exchangeCodeForToken(
            @RequestBody TokenRequest tokenRequest) {
        
        try {
            // Validate authorization code
            AuthorizationCode code = validateAuthorizationCode(tokenRequest.getCode());
            
            // Generate tokens
            String accessToken = tokenService.createAccessToken(
                code.getUserId(), 
                code.getScopes()
            );
            
            String refreshToken = tokenService.createRefreshToken(code.getUserId());
            
            TokenResponse response = new TokenResponse(
                accessToken, 
                refreshToken, 
                3600, 
                "Bearer"
            );
            
            return ResponseEntity.ok(response);
            
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(new ErrorResponse("invalid_grant", e.getMessage()));
        }
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<TokenResponse> refreshToken(
            @RequestBody RefreshTokenRequest request) {
        
        try {
            String newAccessToken = tokenService.refreshToken(request.getRefreshToken());
            
            TokenResponse response = new TokenResponse(
                newAccessToken, 
                request.getRefreshToken(), 
                3600, 
                "Bearer"
            );
            
            return ResponseEntity.ok(response);
            
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(new ErrorResponse("invalid_grant", e.getMessage()));
        }
    }
    
    @PostMapping("/revoke")
    public ResponseEntity<Void> revokeToken(@RequestBody RevokeTokenRequest request) {
        // Add token to blacklist
        tokenBlacklist.addToBlacklist(request.getToken());
        return ResponseEntity.ok().build();
    }
}

Token Blacklist

@Service
public class TokenBlacklist {
    
    private final RedisTemplate<String, String> redisTemplate;
    private static final String BLACKLIST_PREFIX = "blacklist:";
    
    public TokenBlacklist(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    
    public void addToBlacklist(String token) {
        String jti = extractJti(token);
        long expiration = extractExpiration(token);
        
        redisTemplate.opsForValue().set(
            BLACKLIST_PREFIX + jti, 
            "revoked", 
            Duration.ofSeconds(expiration - System.currentTimeMillis() / 1000)
        );
    }
    
    public boolean isBlacklisted(String token) {
        String jti = extractJti(token);
        return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + jti));
    }
    
    private String extractJti(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();
        
        return claims.getId();
    }
    
    private long extractExpiration(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();
        
        return claims.getExpiration().getTime() / 1000;
    }
}

OAuth 2.0 Security Best Practices

1. State Parameter (CSRF Protection)

public class StateManager {
    
    public String generateState() {
        return UUID.randomUUID().toString();
    }
    
    public void storeState(String state, String sessionId) {
        // Store state in session or Redis
        redisTemplate.opsForValue().set(
            "oauth2_state:" + state, 
            sessionId, 
            Duration.ofMinutes(10)
        );
    }
    
    public boolean validateState(String state, String sessionId) {
        String storedSessionId = redisTemplate.opsForValue()
            .get("oauth2_state:" + state);
        
        if (storedSessionId == null) {
            return false; // State expired or not found
        }
        
        if (!storedSessionId.equals(sessionId)) {
            return false; // State mismatch
        }
        
        // Delete state after use
        redisTemplate.delete("oauth2_state:" + state);
        return true;
    }
}

2. PKCE (Proof Key for Code Exchange)

public class PKCEManager {
    
    public PKCEPair generatePKCEPair() {
        String codeVerifier = generateCodeVerifier();
        String codeChallenge = generateCodeChallenge(codeVerifier);
        
        return new PKCEPair(codeVerifier, codeChallenge);
    }
    
    private String generateCodeVerifier() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[32];
        random.nextBytes(bytes);
        
        return Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(bytes);
    }
    
    private String generateCodeChallenge(String codeVerifier) {
        byte[] bytes = codeVerifier.getBytes(StandardCharsets.US_ASCII);
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digest = md.digest(bytes);
        
        return Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(digest);
    }
    
    public boolean verifyCodeChallenge(String codeVerifier, String codeChallenge) {
        String computedChallenge = generateCodeChallenge(codeVerifier);
        return computedChallenge.equals(codeChallenge);
    }
}

3. Secure Token Storage

@Component
public class SecureTokenStorage {
    
    @Value("${token.encryption.key}")
    private String encryptionKey;
    
    public void storeTokens(String sessionId, TokenResponse tokens) {
        // Store tokens encrypted
        String encryptedAccessToken = encrypt(tokens.getAccessToken());
        String encryptedRefreshToken = encrypt(tokens.getRefreshToken());
        
        TokenStorage storage = new TokenStorage(
            encryptedAccessToken,
            encryptedRefreshToken,
            Instant.now().plusSeconds(tokens.getExpiresIn())
        );
        
        redisTemplate.opsForValue().set(
            "tokens:" + sessionId,
            storage,
            Duration.ofDays(30)
        );
    }
    
    public TokenResponse getTokens(String sessionId) {
        TokenStorage storage = redisTemplate.opsForValue()
            .get("tokens:" + sessionId);
        
        if (storage == null) {
            return null;
        }
        
        String accessToken = decrypt(storage.getAccessToken());
        String refreshToken = decrypt(storage.getRefreshToken());
        
        return new TokenResponse(
            accessToken,
            refreshToken,
            storage.getExpiresIn(),
            "Bearer"
        );
    }
    
    private String encrypt(String data) {
        // AES implementation
        return AESTextEncryption.encrypt(data, encryptionKey);
    }
    
    private String decrypt(String encryptedData) {
        // AES implementation
        return AESTextEncryption.decrypt(encryptedData, encryptionKey);
    }
}

OpenID Connect (OIDC)

OIDC Integration

@Configuration
public class OIDCConfig {
    
    @Bean
    public OAuth2AuthorizedClientManager authorizedClientManager(
            ClientRegistrationRepository clientRegistrationRepository,
            OAuth2AuthorizedClientRepository authorizedClientRepository) {
        
        OAuth2AuthorizedClientProvider authorizedClientProvider = 
            OAuth2AuthorizedClientProviderBuilder.builder()
                .authorizationCode()
                .refreshToken()
                .build();
        
        DefaultOAuth2AuthorizedClientManager authorizedClientManager = 
            new DefaultOAuth2AuthorizedClientManager(
                clientRegistrationRepository, 
                authorizedClientRepository);
        
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
        
        return authorizedClientManager;
    }
}

// OIDC User Info Service
@Service
public class OIDCUserInfoService {
    
    public OIDCUserInfo getUserInfo(String accessToken) {
        // Call User Info endpoint
        String userInfoUrl = "https://auth.example.com/oauth2/userinfo";
        
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        
        HttpEntity<String> entity = new HttpEntity<>(headers);
        
        ResponseEntity<OIDCUserInfo> response = restTemplate.exchange(
            userInfoUrl,
            HttpMethod.GET,
            entity,
            OIDCUserInfo.class
        );
        
        return response.getBody();
    }
}

// OIDC User Info DTO
public class OIDCUserInfo {
    private String sub;
    private String name;
    private String email;
    private String picture;
    private List<String> roles;
    
    // Getters and setters
    public String getSubject() { return sub; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public String getPicture() { return picture; }
    public List<String> getRoles() { return roles; }
}

OAuth 2.0 Client Implementation

JavaScript Client Example

class OAuth2Client {
    constructor(config) {
        this.clientId = config.clientId;
        this.redirectUri = config.redirectUri;
        this.authUrl = config.authUrl;
        this.tokenUrl = config.tokenUrl;
        this.scopes = config.scopes;
    }
    
    // Initiate authorization request
    initiateAuthorization() {
        const state = this.generateState();
        const codeVerifier = this.generateCodeVerifier();
        const codeChallenge = this.generateCodeChallenge(codeVerifier);
        
        // Store state and code verifier
        sessionStorage.setItem('oauth2_state', state);
        sessionStorage.setItem('oauth2_code_verifier', codeVerifier);
        
        const authUrl = new URL(this.authUrl);
        authUrl.searchParams.set('response_type', 'code');
        authUrl.searchParams.set('client_id', this.clientId);
        authUrl.searchParams.set('redirect_uri', this.redirectUri);
        authUrl.searchParams.set('scope', this.scopes.join(' '));
        authUrl.searchParams.set('state', state);
        authUrl.searchParams.set('code_challenge', codeChallenge);
        authUrl.searchParams.set('code_challenge_method', 'S256');
        
        // Redirect to authorization server
        window.location.href = authUrl.toString();
    }
    
    // Handle callback
    async handleCallback() {
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');
        const state = urlParams.get('state');
        const storedState = sessionStorage.getItem('oauth2_state');
        
        // Validate state
        if (state !== storedState) {
            throw new Error('Invalid state parameter');
        }
        
        // Exchange code for tokens
        const codeVerifier = sessionStorage.getItem('oauth2_code_verifier');
        const tokenResponse = await this.exchangeCodeForTokens(code, codeVerifier);
        
        // Store tokens
        localStorage.setItem('access_token', tokenResponse.access_token);
        localStorage.setItem('refresh_token', tokenResponse.refresh_token);
        
        // Clean up
        sessionStorage.removeItem('oauth2_state');
        sessionStorage.removeItem('oauth2_code_verifier');
        
        return tokenResponse;
    }
    
    // Token exchange
    async exchangeCodeForTokens(code, codeVerifier) {
        const response = await fetch(this.tokenUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
                grant_type: 'authorization_code',
                code: code,
                redirect_uri: this.redirectUri,
                client_id: this.clientId,
                code_verifier: codeVerifier
            })
        });
        
        if (!response.ok) {
            throw new Error('Token exchange failed');
        }
        
        return await response.json();
    }
    
    // Refresh access token
    async refreshAccessToken() {
        const refreshToken = localStorage.getItem('refresh_token');
        
        const response = await fetch(this.tokenUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
                grant_type: 'refresh_token',
                refresh_token: refreshToken,
                client_id: this.clientId
            })
        });
        
        if (!response.ok) {
            throw new Error('Token refresh failed');
        }
        
        const tokenResponse = await response.json();
        localStorage.setItem('access_token', tokenResponse.access_token);
        
        return tokenResponse;
    }
    
    // Make authenticated API request
    async makeAuthenticatedRequest(url, options = {}) {
        let accessToken = localStorage.getItem('access_token');
        
        // Check token and refresh if needed
        if (this.isTokenExpired(accessToken)) {
            await this.refreshAccessToken();
            accessToken = localStorage.getItem('access_token');
        }
        
        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            ...options.headers
        };
        
        return fetch(url, {
            ...options,
            headers
        });
    }
    
    // Helper methods
    generateState() {
        return Math.random().toString(36).substring(2, 15);
    }
    
    generateCodeVerifier() {
        const array = new Uint8Array(32);
        crypto.getRandomValues(array);
        return btoa(String.fromCharCode.apply(null, array))
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }
    
    async generateCodeChallenge(verifier) {
        const encoder = new TextEncoder();
        const data = encoder.encode(verifier);
        const digest = await crypto.subtle.digest('SHA-256', data);
        return btoa(String.fromCharCode.apply(null, new Uint8Array(digest)))
            .replace(/\+/g, '-')
            .replace(/\//g, '_')
            .replace(/=/g, '');
    }
    
    isTokenExpired(token) {
        try {
            const payload = JSON.parse(atob(token.split('.')[1]));
            return Date.now() >= payload.exp * 1000;
        } catch {
            return true;
        }
    }
}

// Usage
const oauth2Client = new OAuth2Client({
    clientId: 'your-client-id',
    redirectUri: 'http://localhost:3000/callback',
    authUrl: 'https://auth.example.com/oauth2/authorize',
    tokenUrl: 'https://auth.example.com/oauth2/token',
    scopes: ['read', 'write', 'profile']
});

// Initiate login
document.getElementById('login-btn').addEventListener('click', () => {
    oauth2Client.initiateAuthorization();
});

// API request
document.getElementById('fetch-data-btn').addEventListener('click', async () => {
    try {
        const response = await oauth2Client.makeAuthenticatedRequest(
            'https://api.example.com/user/profile'
        );
        const data = await response.json();
        console.log('User data:', data);
    } catch (error) {
        console.error('API request failed:', error);
    }
});

Key Exam Concepts

OAuth 2.0 Flow Types

  1. Authorization Code Flow: The most secure approach for web applications
  2. Implicit Flow: Deprecated and no longer recommended
  3. Client Credentials Flow: For server-to-server communication
  4. Resource Owner Password Credentials: Only for trusted applications

Security Considerations

  • State Parameter: CSRF protection
  • PKCE: Defense against authorization code interception
  • Token Storage: Secure token persistence
  • HTTPS: Required for encrypted communication
  • Scope Limitation: Request minimum necessary permissions

Typical Exam Questions

  1. Explain the Authorization Code Flow
  2. Compare OAuth 2.0 with OpenID Connect
  3. Implement a simple OAuth 2.0 Client
  4. Describe security risks and mitigation strategies

Summary

OAuth 2.0 is a powerful framework for delegating authorization:

  • Secure: Token-based authentication instead of passwords
  • Flexible: Different flows for different use cases
  • Standardized: Widely adopted and well-supported
  • Extensible: OpenID Connect for identity management

Proper implementation requires careful attention to security aspects such as state parameters, PKCE, and secure token storage.

Next in the API Learning Path

The next article in the API Learning Path covers OAuth2 and OpenID Connect for APIs — how to combine OAuth2 with OpenID Connect and integrate authentication and authorization in APIs.

Back to Blog
Share:

Related Posts