Introduction : pourquoi OAuth 2.0 et JWT sont la norme pour les applications iOS
\nLes applications mobiles modernes fonctionnent rarement de manière isolée. La plupart interagissent avec des API distantes, nécessitant une authentification et une autorisation sécurisées de l'utilisateur. Dans l'écosystème Apple (iOS, macOS, watchOS), la combinaison OAuth 2.0 (protocole de délégation d'accès) et JWT (JSON Web Token — format de token compact) est devenue le standard de facto. Cette combinaison offre flexibilité, sécurité et facilité d'intégration.
\nDans cet article, nous n'allons pas approfondir la théorie sèche. Au lieu de cela, nous allons aborder une implémentation pratique en Swift. Vous apprendrez comment obtenir un token d'accès via OAuth 2.0, comment le stocker en toute sécurité dans Keychain, comment décoder un JWT côté client et comment renouveler les tokens sans intervention de l'utilisateur. Tous les exemples de code sont prêts à être utilisés dans un projet réel.
\n\n1. Architecture OAuth 2.0 en Swift : de la requête au token
\nPour commencer, définissons le flux (flow). Dans les applications mobiles, on utilise le plus souvent Authorization Code Flow with PKCE (Proof Key for Code Exchange). C'est l'option la plus sécurisée pour les clients publics (les applications mobiles ne peuvent pas stocker de secret).
\nCréons une structure pour la configuration OAuth :
\nstruct OAuthConfig {\n let clientID: String\n let redirectURI: String\n let authorizationEndpoint: String\n let tokenEndpoint: String\n let scope: String\n}\nLa première étape consiste à former l'URL d'autorisation. Pour PKCE, nous devons générer un codeVerifier et son 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}\nMaintenant, formons l'URL à ouvrir dans le navigateur (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}\nAprès une autorisation réussie, l'utilisateur sera redirigé vers redirectURI avec le paramètre code. Ce code est échangé contre des 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