@capgo/capacitor-social-login 8.1.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,575 @@
1
+ import Foundation
2
+ import AuthenticationServices
3
+ import CryptoKit
4
+ import UIKit
5
+
6
+ struct OAuth2TokenResponse: Codable {
7
+ let token_type: String
8
+ let expires_in: Int?
9
+ let access_token: String
10
+ let scope: String?
11
+ let refresh_token: String?
12
+ let id_token: String?
13
+ }
14
+
15
+ struct OAuth2StoredTokens: Codable {
16
+ let accessToken: String
17
+ let refreshToken: String?
18
+ let idToken: String?
19
+ let expiresAt: Date
20
+ let scope: [String]
21
+ let tokenType: String
22
+ }
23
+
24
+ struct OAuth2LoginResponse {
25
+ let providerId: String
26
+ let accessToken: OAuth2AccessToken
27
+ let idToken: String?
28
+ let refreshToken: String?
29
+ let resourceData: [String: Any]?
30
+ let scope: [String]
31
+ let tokenType: String
32
+ let expiresIn: Int?
33
+ }
34
+
35
+ struct OAuth2AccessToken {
36
+ let token: String
37
+ let tokenType: String
38
+ let expires: String?
39
+ let refreshToken: String?
40
+ }
41
+
42
+ struct OAuth2ProviderConfig {
43
+ let appId: String
44
+ let authorizationBaseUrl: String
45
+ let accessTokenEndpoint: String?
46
+ let redirectUrl: String
47
+ let resourceUrl: String?
48
+ let responseType: String
49
+ let pkceEnabled: Bool
50
+ let scope: String
51
+ let additionalParameters: [String: String]?
52
+ let additionalResourceHeaders: [String: String]?
53
+ let logoutUrl: String?
54
+ let logsEnabled: Bool
55
+ }
56
+
57
+ class OAuth2Provider: NSObject {
58
+ private var providers: [String: OAuth2ProviderConfig] = [:]
59
+ private var currentSession: ASWebAuthenticationSession?
60
+ private var currentState: String?
61
+ private var currentCodeVerifier: String?
62
+ private var currentProviderId: String?
63
+ private let tokenStorageKeyPrefix = "CapgoOAuth2ProviderTokens_"
64
+
65
+ func initializeProviders(configs: [String: [String: Any]]) -> [String] {
66
+ var errors: [String] = []
67
+
68
+ for (providerId, config) in configs {
69
+ guard let appId = config["appId"] as? String, !appId.isEmpty else {
70
+ errors.append("oauth2.\(providerId).appId is required")
71
+ continue
72
+ }
73
+ guard let authorizationBaseUrl = config["authorizationBaseUrl"] as? String, !authorizationBaseUrl.isEmpty else {
74
+ errors.append("oauth2.\(providerId).authorizationBaseUrl is required")
75
+ continue
76
+ }
77
+ guard let redirectUrl = config["redirectUrl"] as? String, !redirectUrl.isEmpty else {
78
+ errors.append("oauth2.\(providerId).redirectUrl is required")
79
+ continue
80
+ }
81
+
82
+ let providerConfig = OAuth2ProviderConfig(
83
+ appId: appId,
84
+ authorizationBaseUrl: authorizationBaseUrl,
85
+ accessTokenEndpoint: config["accessTokenEndpoint"] as? String,
86
+ redirectUrl: redirectUrl,
87
+ resourceUrl: config["resourceUrl"] as? String,
88
+ responseType: config["responseType"] as? String ?? "code",
89
+ pkceEnabled: config["pkceEnabled"] as? Bool ?? true,
90
+ scope: config["scope"] as? String ?? "",
91
+ additionalParameters: config["additionalParameters"] as? [String: String],
92
+ additionalResourceHeaders: config["additionalResourceHeaders"] as? [String: String],
93
+ logoutUrl: config["logoutUrl"] as? String,
94
+ logsEnabled: config["logsEnabled"] as? Bool ?? false
95
+ )
96
+
97
+ providers[providerId] = providerConfig
98
+
99
+ if providerConfig.logsEnabled {
100
+ print("[OAuth2Provider] Initialized provider '\(providerId)' with appId: \(appId), authorizationBaseUrl: \(authorizationBaseUrl)")
101
+ }
102
+ }
103
+
104
+ return errors
105
+ }
106
+
107
+ private func getProvider(_ providerId: String) -> OAuth2ProviderConfig? {
108
+ return providers[providerId]
109
+ }
110
+
111
+ private func tokenStorageKey(for providerId: String) -> String {
112
+ return "\(tokenStorageKeyPrefix)\(providerId)"
113
+ }
114
+
115
+ func login(providerId: String, payload: [String: Any], completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void) {
116
+ guard let config = getProvider(providerId) else {
117
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -1, userInfo: [NSLocalizedDescriptionKey: "OAuth2 provider '\(providerId)' not configured. Call initialize()."])))
118
+ return
119
+ }
120
+
121
+ let loginScope = payload["scope"] as? String ?? config.scope
122
+ let state = payload["state"] as? String ?? UUID().uuidString
123
+ let codeVerifier = payload["codeVerifier"] as? String ?? generateCodeVerifier()
124
+ let redirect = payload["redirectUrl"] as? String ?? config.redirectUrl
125
+ let additionalLoginParams = payload["additionalParameters"] as? [String: String]
126
+
127
+ currentState = state
128
+ currentCodeVerifier = codeVerifier
129
+ currentProviderId = providerId
130
+
131
+ var components = URLComponents(string: config.authorizationBaseUrl)
132
+ var queryItems: [URLQueryItem] = [
133
+ URLQueryItem(name: "response_type", value: config.responseType),
134
+ URLQueryItem(name: "client_id", value: config.appId),
135
+ URLQueryItem(name: "redirect_uri", value: redirect),
136
+ URLQueryItem(name: "state", value: state)
137
+ ]
138
+
139
+ if !loginScope.isEmpty {
140
+ queryItems.append(URLQueryItem(name: "scope", value: loginScope))
141
+ }
142
+
143
+ // Add PKCE for code flow
144
+ if config.responseType == "code" && config.pkceEnabled {
145
+ let codeChallenge = generateCodeChallenge(from: codeVerifier)
146
+ queryItems.append(URLQueryItem(name: "code_challenge", value: codeChallenge))
147
+ queryItems.append(URLQueryItem(name: "code_challenge_method", value: "S256"))
148
+ }
149
+
150
+ // Add additional parameters from config
151
+ if let additionalParams = config.additionalParameters {
152
+ for (key, value) in additionalParams {
153
+ queryItems.append(URLQueryItem(name: key, value: value))
154
+ }
155
+ }
156
+
157
+ // Add additional parameters from login options
158
+ if let loginParams = additionalLoginParams {
159
+ for (key, value) in loginParams {
160
+ queryItems.append(URLQueryItem(name: key, value: value))
161
+ }
162
+ }
163
+
164
+ components?.queryItems = queryItems
165
+
166
+ guard let authUrl = components?.url, let callbackScheme = URL(string: redirect)?.scheme else {
167
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -2, userInfo: [NSLocalizedDescriptionKey: "Invalid redirect URL configuration."])))
168
+ return
169
+ }
170
+
171
+ if config.logsEnabled {
172
+ print("[OAuth2Provider] Opening authorization URL: \(authUrl.absoluteString)")
173
+ }
174
+
175
+ let session = ASWebAuthenticationSession(url: authUrl, callbackURLScheme: callbackScheme) { [weak self] callbackURL, error in
176
+ guard let self = self else { return }
177
+ if let error = error {
178
+ if (error as NSError).code == ASWebAuthenticationSessionError.canceledLogin.rawValue {
179
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -3, userInfo: [NSLocalizedDescriptionKey: "User cancelled login."])))
180
+ } else {
181
+ completion(.failure(error))
182
+ }
183
+ return
184
+ }
185
+
186
+ guard let callbackURL = callbackURL else {
187
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -4, userInfo: [NSLocalizedDescriptionKey: "Invalid callback URL."])))
188
+ return
189
+ }
190
+
191
+ self.handleCallback(providerId: providerId, config: config, callbackURL: callbackURL, redirectUri: redirect, completion: completion)
192
+ }
193
+
194
+ if #available(iOS 13.0, *) {
195
+ session.presentationContextProvider = self
196
+ }
197
+
198
+ session.prefersEphemeralWebBrowserSession = true
199
+ currentSession = session
200
+ session.start()
201
+ }
202
+
203
+ private func handleCallback(providerId: String, config: OAuth2ProviderConfig, callbackURL: URL, redirectUri: String, completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void) {
204
+ // Parse both query params and fragment
205
+ var params: [String: String] = [:]
206
+
207
+ if let queryItems = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false)?.queryItems {
208
+ for item in queryItems {
209
+ params[item.name] = item.value
210
+ }
211
+ }
212
+
213
+ // Parse fragment for implicit flow
214
+ if let fragment = callbackURL.fragment {
215
+ let fragmentParams = fragment.components(separatedBy: "&")
216
+ for param in fragmentParams {
217
+ let parts = param.components(separatedBy: "=")
218
+ if parts.count == 2 {
219
+ params[parts[0]] = parts[1].removingPercentEncoding
220
+ }
221
+ }
222
+ }
223
+
224
+ // Validate state
225
+ if let returnedState = params["state"], returnedState != currentState {
226
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -5, userInfo: [NSLocalizedDescriptionKey: "State mismatch detected during OAuth2 login."])))
227
+ return
228
+ }
229
+
230
+ // Check for error
231
+ if let errorCode = params["error"] {
232
+ let errorDescription = params["error_description"] ?? errorCode
233
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -6, userInfo: [NSLocalizedDescriptionKey: errorDescription])))
234
+ return
235
+ }
236
+
237
+ // Handle code flow
238
+ if let code = params["code"] {
239
+ guard let codeVerifier = currentCodeVerifier else {
240
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -7, userInfo: [NSLocalizedDescriptionKey: "Missing code verifier."])))
241
+ return
242
+ }
243
+ exchangeCode(providerId: providerId, config: config, code: code, redirectUri: redirectUri, codeVerifier: codeVerifier, completion: completion)
244
+ return
245
+ }
246
+
247
+ // Handle implicit flow
248
+ if let accessToken = params["access_token"] {
249
+ let tokenResponse = OAuth2TokenResponse(
250
+ token_type: params["token_type"] ?? "bearer",
251
+ expires_in: params["expires_in"].flatMap { Int($0) },
252
+ access_token: accessToken,
253
+ scope: params["scope"],
254
+ refresh_token: nil,
255
+ id_token: params["id_token"]
256
+ )
257
+ handleTokenSuccess(providerId: providerId, config: config, tokenResponse: tokenResponse, completion: completion)
258
+ return
259
+ }
260
+
261
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -8, userInfo: [NSLocalizedDescriptionKey: "No authorization code or access token in callback."])))
262
+ }
263
+
264
+ func logout(providerId: String, completion: @escaping (Result<Void, Error>) -> Void) {
265
+ UserDefaults.standard.removeObject(forKey: tokenStorageKey(for: providerId))
266
+
267
+ if let config = getProvider(providerId), let logoutUrl = config.logoutUrl, let url = URL(string: logoutUrl) {
268
+ DispatchQueue.main.async {
269
+ UIApplication.shared.open(url)
270
+ }
271
+ }
272
+
273
+ completion(.success(()))
274
+ }
275
+
276
+ func isLoggedIn(providerId: String, completion: @escaping (Result<Bool, Error>) -> Void) {
277
+ if let tokens = loadTokens(for: providerId) {
278
+ completion(.success(tokens.expiresAt > Date()))
279
+ } else {
280
+ completion(.success(false))
281
+ }
282
+ }
283
+
284
+ func getAuthorizationCode(providerId: String, completion: @escaping (Result<OAuth2AccessToken, Error>) -> Void) {
285
+ guard let tokens = loadTokens(for: providerId) else {
286
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -9, userInfo: [NSLocalizedDescriptionKey: "User not logged in."])))
287
+ return
288
+ }
289
+ completion(.success(OAuth2AccessToken(
290
+ token: tokens.accessToken,
291
+ tokenType: tokens.tokenType,
292
+ expires: ISO8601DateFormatter().string(from: tokens.expiresAt),
293
+ refreshToken: tokens.refreshToken
294
+ )))
295
+ }
296
+
297
+ func refresh(providerId: String, completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void) {
298
+ guard let config = getProvider(providerId) else {
299
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -1, userInfo: [NSLocalizedDescriptionKey: "OAuth2 provider '\(providerId)' not configured."])))
300
+ return
301
+ }
302
+
303
+ guard let tokens = loadTokens(for: providerId), let refreshToken = tokens.refreshToken else {
304
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -10, userInfo: [NSLocalizedDescriptionKey: "No refresh token available. Include offline_access scope."])))
305
+ return
306
+ }
307
+
308
+ refreshTokens(providerId: providerId, config: config, refreshToken: refreshToken, completion: completion)
309
+ }
310
+
311
+ private func exchangeCode(providerId: String, config: OAuth2ProviderConfig, code: String, redirectUri: String, codeVerifier: String, completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void) {
312
+ guard let accessTokenEndpoint = config.accessTokenEndpoint else {
313
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -11, userInfo: [NSLocalizedDescriptionKey: "Missing accessTokenEndpoint for code exchange."])))
314
+ return
315
+ }
316
+
317
+ var body: [String: String] = [
318
+ "grant_type": "authorization_code",
319
+ "client_id": config.appId,
320
+ "code": code,
321
+ "redirect_uri": redirectUri
322
+ ]
323
+
324
+ if config.pkceEnabled {
325
+ body["code_verifier"] = codeVerifier
326
+ }
327
+
328
+ if config.logsEnabled {
329
+ print("[OAuth2Provider] Exchanging code at: \(accessTokenEndpoint)")
330
+ }
331
+
332
+ performTokenRequest(endpoint: accessTokenEndpoint, body: body) { [weak self] result in
333
+ switch result {
334
+ case .success(let tokenResponse):
335
+ self?.handleTokenSuccess(providerId: providerId, config: config, tokenResponse: tokenResponse, completion: completion)
336
+ case .failure(let error):
337
+ completion(.failure(error))
338
+ }
339
+ }
340
+ }
341
+
342
+ private func refreshTokens(providerId: String, config: OAuth2ProviderConfig, refreshToken: String, completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void) {
343
+ guard let accessTokenEndpoint = config.accessTokenEndpoint else {
344
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -12, userInfo: [NSLocalizedDescriptionKey: "Missing accessTokenEndpoint for token refresh."])))
345
+ return
346
+ }
347
+
348
+ let body: [String: String] = [
349
+ "grant_type": "refresh_token",
350
+ "refresh_token": refreshToken,
351
+ "client_id": config.appId
352
+ ]
353
+
354
+ performTokenRequest(endpoint: accessTokenEndpoint, body: body) { [weak self] result in
355
+ switch result {
356
+ case .success(let tokenResponse):
357
+ self?.handleTokenSuccess(providerId: providerId, config: config, tokenResponse: tokenResponse, completion: completion)
358
+ case .failure(let error):
359
+ completion(.failure(error))
360
+ }
361
+ }
362
+ }
363
+
364
+ private func performTokenRequest(endpoint: String, body: [String: String], completion: @escaping (Result<OAuth2TokenResponse, Error>) -> Void) {
365
+ guard let url = URL(string: endpoint) else {
366
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -13, userInfo: [NSLocalizedDescriptionKey: "Invalid token endpoint."])))
367
+ return
368
+ }
369
+
370
+ var request = URLRequest(url: url)
371
+ request.httpMethod = "POST"
372
+ request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
373
+ request.httpBody = body.percentEncoded()
374
+
375
+ URLSession.shared.dataTask(with: request) { data, response, error in
376
+ if let error = error {
377
+ completion(.failure(error))
378
+ return
379
+ }
380
+ guard let httpResponse = response as? HTTPURLResponse else {
381
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -14, userInfo: [NSLocalizedDescriptionKey: "Invalid response."])))
382
+ return
383
+ }
384
+ guard let data = data, httpResponse.statusCode == 200 else {
385
+ let message = String(data: data ?? Data(), encoding: .utf8) ?? "Unknown error"
386
+ completion(.failure(NSError(domain: "OAuth2Provider", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: message])))
387
+ return
388
+ }
389
+
390
+ do {
391
+ let decoded = try JSONDecoder().decode(OAuth2TokenResponse.self, from: data)
392
+ completion(.success(decoded))
393
+ } catch {
394
+ completion(.failure(error))
395
+ }
396
+ }
397
+ .resume()
398
+ }
399
+
400
+ private func handleTokenSuccess(providerId: String, config: OAuth2ProviderConfig, tokenResponse: OAuth2TokenResponse, completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void) {
401
+ let expiresAt = tokenResponse.expires_in.map { Date().addingTimeInterval(TimeInterval($0)) } ?? Date().addingTimeInterval(3600)
402
+ let scopeArray = tokenResponse.scope?.split(separator: " ").map { String($0) } ?? []
403
+
404
+ // Fetch resource data if configured
405
+ if let resourceUrl = config.resourceUrl {
406
+ fetchResource(config: config, accessToken: tokenResponse.access_token) { [weak self] resourceResult in
407
+ guard let self = self else { return }
408
+
409
+ let resourceData: [String: Any]?
410
+ switch resourceResult {
411
+ case .success(let data):
412
+ resourceData = data
413
+ case .failure(let error):
414
+ if config.logsEnabled {
415
+ print("[OAuth2Provider] Failed to fetch resource: \(error.localizedDescription)")
416
+ }
417
+ resourceData = nil
418
+ }
419
+
420
+ self.completeLogin(
421
+ providerId: providerId,
422
+ tokenResponse: tokenResponse,
423
+ expiresAt: expiresAt,
424
+ scopeArray: scopeArray,
425
+ resourceData: resourceData,
426
+ completion: completion
427
+ )
428
+ }
429
+ } else {
430
+ completeLogin(
431
+ providerId: providerId,
432
+ tokenResponse: tokenResponse,
433
+ expiresAt: expiresAt,
434
+ scopeArray: scopeArray,
435
+ resourceData: nil,
436
+ completion: completion
437
+ )
438
+ }
439
+ }
440
+
441
+ private func completeLogin(
442
+ providerId: String,
443
+ tokenResponse: OAuth2TokenResponse,
444
+ expiresAt: Date,
445
+ scopeArray: [String],
446
+ resourceData: [String: Any]?,
447
+ completion: @escaping (Result<OAuth2LoginResponse, Error>) -> Void
448
+ ) {
449
+ let stored = OAuth2StoredTokens(
450
+ accessToken: tokenResponse.access_token,
451
+ refreshToken: tokenResponse.refresh_token,
452
+ idToken: tokenResponse.id_token,
453
+ expiresAt: expiresAt,
454
+ scope: scopeArray,
455
+ tokenType: tokenResponse.token_type
456
+ )
457
+ persistTokens(tokens: stored, for: providerId)
458
+
459
+ let response = OAuth2LoginResponse(
460
+ providerId: providerId,
461
+ accessToken: OAuth2AccessToken(
462
+ token: tokenResponse.access_token,
463
+ tokenType: tokenResponse.token_type,
464
+ expires: ISO8601DateFormatter().string(from: expiresAt),
465
+ refreshToken: tokenResponse.refresh_token
466
+ ),
467
+ idToken: tokenResponse.id_token,
468
+ refreshToken: tokenResponse.refresh_token,
469
+ resourceData: resourceData,
470
+ scope: scopeArray,
471
+ tokenType: tokenResponse.token_type,
472
+ expiresIn: tokenResponse.expires_in
473
+ )
474
+ completion(.success(response))
475
+ }
476
+
477
+ private func fetchResource(config: OAuth2ProviderConfig, accessToken: String, completion: @escaping (Result<[String: Any], Error>) -> Void) {
478
+ guard let resourceUrl = config.resourceUrl, let url = URL(string: resourceUrl) else {
479
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -15, userInfo: [NSLocalizedDescriptionKey: "Invalid resource URL."])))
480
+ return
481
+ }
482
+
483
+ var request = URLRequest(url: url)
484
+ request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
485
+
486
+ if let additionalHeaders = config.additionalResourceHeaders {
487
+ for (key, value) in additionalHeaders {
488
+ request.addValue(value, forHTTPHeaderField: key)
489
+ }
490
+ }
491
+
492
+ URLSession.shared.dataTask(with: request) { data, response, error in
493
+ if let error = error {
494
+ completion(.failure(error))
495
+ return
496
+ }
497
+ guard let httpResponse = response as? HTTPURLResponse else {
498
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -16, userInfo: [NSLocalizedDescriptionKey: "Invalid response."])))
499
+ return
500
+ }
501
+ guard let data = data, httpResponse.statusCode == 200 else {
502
+ let message = String(data: data ?? Data(), encoding: .utf8) ?? "Unknown error"
503
+ completion(.failure(NSError(domain: "OAuth2Provider", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: message])))
504
+ return
505
+ }
506
+
507
+ do {
508
+ if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
509
+ completion(.success(json))
510
+ } else {
511
+ completion(.failure(NSError(domain: "OAuth2Provider", code: -17, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON response."])))
512
+ }
513
+ } catch {
514
+ completion(.failure(error))
515
+ }
516
+ }
517
+ .resume()
518
+ }
519
+
520
+ private func persistTokens(tokens: OAuth2StoredTokens, for providerId: String) {
521
+ do {
522
+ let data = try JSONEncoder().encode(tokens)
523
+ UserDefaults.standard.set(data, forKey: tokenStorageKey(for: providerId))
524
+ } catch {
525
+ print("OAuth2Provider persistTokens error: \(error.localizedDescription)")
526
+ }
527
+ }
528
+
529
+ private func loadTokens(for providerId: String) -> OAuth2StoredTokens? {
530
+ guard let data = UserDefaults.standard.data(forKey: tokenStorageKey(for: providerId)) else {
531
+ return nil
532
+ }
533
+ do {
534
+ return try JSONDecoder().decode(OAuth2StoredTokens.self, from: data)
535
+ } catch {
536
+ print("OAuth2Provider loadTokens error: \(error.localizedDescription)")
537
+ return nil
538
+ }
539
+ }
540
+
541
+ private func generateCodeVerifier() -> String {
542
+ var data = Data(count: 64)
543
+ _ = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 64, $0.baseAddress!) }
544
+ return data.base64EncodedString()
545
+ .replacingOccurrences(of: "=", with: "")
546
+ .replacingOccurrences(of: "+", with: "-")
547
+ .replacingOccurrences(of: "/", with: "_")
548
+ }
549
+
550
+ private func generateCodeChallenge(from verifier: String) -> String {
551
+ let data = verifier.data(using: .utf8) ?? Data()
552
+ let digest = SHA256.hash(data: data)
553
+ return Data(digest).base64EncodedString()
554
+ .replacingOccurrences(of: "+", with: "-")
555
+ .replacingOccurrences(of: "/", with: "_")
556
+ .replacingOccurrences(of: "=", with: "")
557
+ }
558
+ }
559
+
560
+ extension OAuth2Provider: ASWebAuthenticationPresentationContextProviding {
561
+ @available(iOS 13.0, *)
562
+ func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
563
+ return UIApplication.shared.windows.first { $0.isKeyWindow } ?? ASPresentationAnchor()
564
+ }
565
+ }
566
+
567
+ private extension Dictionary where Key == String, Value == String {
568
+ func percentEncoded() -> Data? {
569
+ map { key, value in
570
+ "\(key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? key)=\(value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value)"
571
+ }
572
+ .joined(separator: "&")
573
+ .data(using: .utf8)
574
+ }
575
+ }