مقدمة: لماذا OAuth 2.0 و JWT هما المعيار لتطبيقات iOS
\nتطبيقات الهواتف المحمولة الحديثة نادراً ما تعمل بشكل منعزل. معظمها يتفاعل مع واجهات برمجة التطبيقات (API) البعيدة، مما يتطلب مصادقة وتفويضاً آمناً للمستخدم. في نظام Apple البيئي (iOS، macOS، watchOS)، أصبح الجمع بين OAuth 2.0 (بروتوكول تفويض الوصول) و JWT (رمز JSON للويب - تنسيق مضغوط للرمز المميز) هو المعيار الفعلي. يوفر هذا المزيج المرونة والأمان وسهولة التكامل.
\nفي هذه المقالة، لن نتعمق في النظرية الجافة. بدلاً من ذلك، سنقوم بتحليل التنفيذ العملي على Swift. ستتعلم كيفية الحصول على رمز الوصول عبر OAuth 2.0، وكيفية تخزينه بأمان في Keychain، وكيفية فك تشفير JWT على العميل، وكيفية تحديث الرموز المميزة دون تدخل المستخدم. جميع أمثلة الكود جاهزة للاستخدام في مشروع حقيقي.
\n\n1. بنية OAuth 2.0 على Swift: من الطلب إلى الرمز المميز
\nأولاً، دعنا نحدد التدفق (flow). في تطبيقات الهواتف المحمولة، يُستخدم غالباً Authorization Code Flow with PKCE (إثبات المفتاح لتبادل الكود). هذا هو الخيار الأكثر أماناً للعملاء العموميين (تطبيقات الهواتف المحمولة لا يمكنها تخزين السر).
\nلنقم بإنشاء هيكل لتكوين OAuth:
\nstruct OAuthConfig {\n let clientID: String\n let redirectURI: String\n let authorizationEndpoint: String\n let tokenEndpoint: String\n let scope: String\n}\nالخطوة الأولى هي تشكيل عنوان URL للتفويض. بالنسبة لـ PKCE، سنحتاج إلى توليد codeVerifier وتجزئته 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}\nالآن نقوم بتشكيل عنوان URL لفتحه في المتصفح (عبر 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}\nبعد التفويض الناجح، سيتم إعادة توجيه المستخدم إلى redirectURI مع معامل code. يتم استبدال هذا الكود بالرموز المميزة:
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