Introdução: por que OAuth 2.0 e JWT são o padrão para aplicativos iOS
\nOs aplicativos móveis modernos raramente funcionam isoladamente. A maioria deles interage com APIs remotas, exigindo autenticação e autorização seguras do usuário. No ecossistema Apple (iOS, macOS, watchOS), a combinação de OAuth 2.0 (protocolo de delegação de acesso) e JWT (JSON Web Token — formato compacto de token) tornou-se o padrão de fato. Essa combinação oferece flexibilidade, segurança e facilidade de integração.
\nNeste artigo, não vamos nos aprofundar em teoria seca. Em vez disso, vamos abordar a implementação prática em Swift. Você aprenderá como obter um token de acesso via OAuth 2.0, como armazená-lo com segurança no Keychain, como decodificar JWT no cliente e como renovar tokens sem a intervenção do usuário. Todos os exemplos de código estão prontos para uso em um projeto real.
\n\n1. Arquitetura OAuth 2.0 em Swift: da solicitação ao token
\nPrimeiro, vamos definir o fluxo (flow). Em aplicativos móveis, o mais utilizado é o Authorization Code Flow with PKCE (Proof Key for Code Exchange). Esta é a opção mais segura para clientes públicos (aplicativos móveis não podem armazenar segredos).
\nVamos criar uma estrutura para a configuração OAuth:
\nstruct OAuthConfig {\n let clientID: String\n let redirectURI: String\n let authorizationEndpoint: String\n let tokenEndpoint: String\n let scope: String\n}\nO primeiro passo é formar a URL para autorização. Para PKCE, precisamos gerar um codeVerifier e seu 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}\nAgora, formamos a URL para abrir no navegador (via 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}\nApós a autorização bem-sucedida, o usuário será redirecionado para a redirectURI com o parâmetro code. Esse código é trocado 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