Introducción: por qué OAuth 2.0 y JWT son el estándar para aplicaciones iOS
\nLas aplicaciones móviles modernas rara vez funcionan de forma aislada. La mayoría interactúa con APIs remotas, requiriendo autenticación y autorización segura del usuario. En el ecosistema de Apple (iOS, macOS, watchOS), el estándar de facto es la combinación de OAuth 2.0 (protocolo de delegación de acceso) y JWT (JSON Web Token — formato compacto de token). Esta combinación proporciona flexibilidad, seguridad y facilidad de integración.
\nEn este artículo no profundizaremos en teoría abstracta. En su lugar, analizaremos una implementación práctica en Swift. Aprenderá cómo obtener un token de acceso mediante OAuth 2.0, cómo almacenarlo de forma segura en Keychain, cómo decodificar JWT en el cliente y cómo renovar tokens sin intervención del usuario. Todos los ejemplos de código están listos para usar en un proyecto real.
\n\n1. Arquitectura de OAuth 2.0 en Swift: desde la solicitud hasta el token
\nPara empezar, definamos el flujo. En aplicaciones móviles, el más utilizado es el Authorization Code Flow with PKCE (Proof Key for Code Exchange). Esta es la opción más segura para clientes públicos (las aplicaciones móviles no pueden almacenar un secreto).
\nCrearemos una estructura para la configuración de OAuth:
\nstruct OAuthConfig {\n let clientID: String\n let redirectURI: String\n let authorizationEndpoint: String\n let tokenEndpoint: String\n let scope: String\n}\nEl primer paso es construir la URL de autorización. Para PKCE necesitamos generar un codeVerifier y su hash SHA256 — codeChallenge:
import Foundation\nimport CommonCrypto\n\nclass PKCEGenerator {\n static func generateCodeVerifier() -> String {\n var buffer = [UInt8](repeating: 0, count: 32)\n _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)\n return Data(buffer).base64URLEncodedString()\n }\n \n static func generateCodeChallenge(from verifier: String) -> String {\n guard let data = verifier.data(using: .utf8) else { return "" }\n var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))\n data.withUnsafeBytes { \n _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)\n }\n return Data(hash).base64URLEncodedString()\n }\n}\n\nextension Data {\n func base64URLEncodedString() -> String {\n return self.base64EncodedString()\n .replacingOccurrences(of: "+", with: "-")\n .replacingOccurrences(of: "/", with: "_")\n .replacingOccurrences(of: "=", with: "")\n }\n}\nAhora construimos la URL para abrir en el navegador (a través de ASWebAuthenticationSession):
func buildAuthorizationURL(config: OAuthConfig, codeChallenge: String) -> URL? {\n var components = URLComponents(string: config.authorizationEndpoint)\n components?.queryItems = [\n URLQueryItem(name: "response_type", value: "code"),\n URLQueryItem(name: "client_id", value: config.clientID),\n URLQueryItem(name: "redirect_uri", value: config.redirectURI),\n URLQueryItem(name: "scope", value: config.scope),\n URLQueryItem(name: "code_challenge", value: codeChallenge),\n URLQueryItem(name: "code_challenge_method", value: "S256")\n ]\n return components?.url\n}\nDespués de una autorización exitosa, el usuario será redirigido a redirectURI con el parámetro code. Este código se intercambia por tokens:
func exchangeCodeForToken(config: OAuthConfig, code: String, codeVerifier: String) async throws -> TokenResponse {\n let url = URL(string: config.tokenEndpoint)!\n var request = URLRequest(url: url)\n request.httpMethod = "POST"\n request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")\n \n let body = "grant_type=authorization_code" +\n "&co