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

OAuth 2.0: Authorization Code y Access Token

Aprende OAuth 2.0 con Authorization Code Flow, implementación de Access Token y mejores prácticas de seguridad.

S

schutzgeist

12 min read
OAuth 2.0: Authorization Code y Access Token

Fundamentos de OAuth 2.0: Authorization Code y Access Token

OAuth 2.0 es el estándar de la industria para delegar autorizaciones. Permite que las aplicaciones accedan a recursos en nombre de un usuario sin exponer sus credenciales.

¿Qué es OAuth 2.0?

OAuth 2.0 es un framework de autorización que permite a aplicaciones de terceros obtener acceso limitado a recursos protegidos sin intercambiar credenciales de usuario.

Conceptos clave de OAuth 2.0

  • Delegación: Los usuarios delegan acceso a aplicaciones
  • Basado en tokens: Se usan tokens en lugar de contraseñas
  • Basado en alcances: Acceso a áreas definidas (scopes)
  • Seguro: Reduce la superficie de ataque mediante tokens con vida útil limitada

Roles en OAuth 2.0

Los cuatro roles principales

// Roles de OAuth 2.0 como clases Java
public class OAuthRoles {
    
    // Resource Owner: El usuario que posee el recurso
    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);
        }
        
        // Aprueba o rechaza el acceso
        public boolean authorizeAccess(String clientId, List<String> scopes) {
            // Lógica de negocio para la aprobación
            return true; // Simplificado
        }
    }
    
    // Resource Server: Aloja los recursos protegidos
    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: Autentica al usuario y emite 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) // válido por 10 minutos
            );
            
            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");
            }
            
            // Crear Access Token y 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) {
            // Implementación para validar el Authorization Code
            return new AuthorizationCode(code, clientId, "user123", 
                Arrays.asList("read", "write"), Instant.now());
        }
    }
    
    // Client: La aplicación que solicita acceso
    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) {
            // Crear 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) {
            // Enviar Token Request al Authorization Server
            return tokenService.exchangeCode(code, clientId, clientSecret);
        }
    }
}

Authorization Code Flow

El flujo OAuth 2.0 más seguro

public class AuthorizationCodeFlow {
    
    // Paso 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"); // Protección CSRF
        
        return request.toString();
    }
    
    // Paso 2: User Authorization
    public void handleUserAuthorization(String userId, String clientId, List<String> scopes) {
        // El usuario es redirigido a la página de inicio de sesión
        // Tras la autenticación exitosa:
        
        if (userConsentsToScopes(userId, scopes)) {
            AuthorizationCode code = authorizationServer.generateAuthorizationCode(
                clientId, userId, scopes
            );
            
            // Redireccionar con Authorization Code
            String redirectUri = String.format(
                "https://client.example.com/callback?code=%s&state=xyz123",
                code.getValue()
            );
            
            redirectToClient(redirectUri);
        } else {
            // El usuario rechazó
            redirectToClient("https://client.example.com/callback?error=access_denied");
        }
    }
    
    // Paso 3: Token Exchange
    public TokenResponse exchangeCodeForTokens(String code, String state) {
        // Validar state (Protección CSRF)
        if (!isValidState(state)) {
            throw new SecurityException("Invalid state parameter");
        }
        
        // Enviar Token Request al 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");
        
        // Enviar HTTP POST Request
        TokenResponse response = httpClient.postForm(
            "https://auth.example.com/token", 
            tokenRequest
        );
        
        return response;
    }
    
    // Paso 4: Resource Access
    public ProtectedResource accessResource(String accessToken, String resourceId) {
        // Enviar Access Token en el header Authorization
        Map<String, String> headers = new HashMap<>();
        headers.put("Authorization", "Bearer " + accessToken);
        
        return httpClient.get(
            "https://api.example.com/resources/" + resourceId,
            headers
        );
    }
}

Implementación de Spring Security OAuth 2.0

@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);
        
        // Procesamos la información del usuario y la guardamos en la base de datos
        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) {
        // Lógica para buscar o crear el usuario
        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);
    }
}

Gestión de Tokens

Implementación de JWT

@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 {
            // Validamos el código de autorización
            AuthorizationCode code = validateAuthorizationCode(tokenRequest.getCode());
            
            // Creamos los 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) {
        // Añadimos el token a la lista negra
        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;
    }
}

Buenas prácticas de seguridad en OAuth 2.0

1. Parámetro State (Protección CSRF)

public class StateManager {
    
    public String generateState() {
        return UUID.randomUUID().toString();
    }
    
    public void storeState(String state, String sessionId) {
        // Almacenar state en sesión o 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 expirado o no encontrado
        }
        
        if (!storedSessionId.equals(sessionId)) {
            return false; // State no coincide
        }
        
        // Eliminar state después de su uso
        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. Almacenamiento seguro de tokens

@Component
public class SecureTokenStorage {
    
    @Value("${token.encryption.key}")
    private String encryptionKey;
    
    public void storeTokens(String sessionId, TokenResponse tokens) {
        // Almacenar tokens cifrados
        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) {
        // Implementación con AES
        return AESTextEncryption.encrypt(data, encryptionKey);
    }
    
    private String decrypt(String encryptedData) {
        // Implementación con AES
        return AESTextEncryption.decrypt(encryptedData, encryptionKey);
    }
}

OpenID Connect (OIDC)

Integración OIDC

@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;
    }
}

// Servicio de información de usuario OIDC
@Service
public class OIDCUserInfoService {
    
    public OIDCUserInfo getUserInfo(String accessToken) {
        // Invocar el endpoint de información de usuario
        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();
    }
}

// DTO de información de usuario OIDC
public class OIDCUserInfo {
    private String sub;
    private String name;
    private String email;
    private String picture;
    private List<String> roles;
    
    // Getters y 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; }
}

Implementación de Cliente OAuth 2.0

Ejemplo de Cliente JavaScript

class OAuth2Client {
    constructor(config) {
        this.clientId = config.clientId;
        this.redirectUri = config.redirectUri;
        this.authUrl = config.authUrl;
        this.tokenUrl = config.tokenUrl;
        this.scopes = config.scopes;
    }
    
    // Iniciar Authorization Request
    initiateAuthorization() {
        const state = this.generateState();
        const codeVerifier = this.generateCodeVerifier();
        const codeChallenge = this.generateCodeChallenge(codeVerifier);
        
        // Guardar state y 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');
        
        // Redirigir al servidor de autorización
        window.location.href = authUrl.toString();
    }
    
    // Procesar 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');
        
        // Validar state
        if (state !== storedState) {
            throw new Error('Invalid state parameter');
        }
        
        // Intercambiar código por tokens
        const codeVerifier = sessionStorage.getItem('oauth2_code_verifier');
        const tokenResponse = await this.exchangeCodeForTokens(code, codeVerifier);
        
        // Almacenar tokens
        localStorage.setItem('access_token', tokenResponse.access_token);
        localStorage.setItem('refresh_token', tokenResponse.refresh_token);
        
        // Limpiar datos de sesión
        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();
    }
    
    // Renovar 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;
    }
    
    // API request con access token
    async makeAuthenticatedRequest(url, options = {}) {
        let accessToken = localStorage.getItem('access_token');
        
        // Verificar token y renovar si es necesario
        if (this.isTokenExpired(accessToken)) {
            await this.refreshAccessToken();
            accessToken = localStorage.getItem('access_token');
        }
        
        const headers = {
            'Authorization': `Bearer ${accessToken}`,
            ...options.headers
        };
        
        return fetch(url, {
            ...options,
            headers
        });
    }
    
    // Métodos auxiliares
    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;
        }
    }
}

// Uso
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']
});

// Iniciar 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);
    }
});

Conceptos Relevantes para Examen

Flujos Importantes de OAuth 2.0

  1. Authorization Code Flow: El método más seguro para aplicaciones web
  2. Implicit Flow: Obsoleto, ya no se recomienda
  3. Client Credentials Flow: Para comunicación máquina a máquina
  4. Resource Owner Password Credentials: Solo para aplicaciones de confianza

Aspectos de Seguridad

  • State Parameter: Protección contra CSRF
  • PKCE: Protección contra interceptación de código
  • Token Storage: Almacenamiento seguro de tokens
  • HTTPS: Comunicación cifrada requerida
  • Scope Limitation: Solicitar permisos mínimos

Tareas Típicas de Examen

  1. Explique el Authorization Code Flow
  2. Compare OAuth 2.0 con OpenID Connect
  3. Implemente un cliente OAuth 2.0 simple
  4. Describa los riesgos de seguridad y las contramedidas

Resumen

OAuth 2.0 es un framework potente para delegar autorización:

  • Seguro: Autenticación basada en tokens en lugar de contraseñas
  • Flexible: Diferentes flujos para distintos casos de uso
  • Estándar: Ampliamente adoptado y bien soportado
  • Extensible: OpenID Connect para gestión de identidad

Una implementación correcta requiere atención cuidadosa a aspectos de seguridad como parámetros State, PKCE y almacenamiento seguro de tokens.

Continúa en la ruta de aprendizaje de API

El siguiente artículo en la ruta de aprendizaje de API cubre OAuth2 y OpenID Connect para APIs — cómo combinar OAuth2 con OpenID Connect e integrar autenticación y autorización en tus APIs.

Volver al blog
Share:

Entradas relacionadas