CVE-2020-15679: Session Fixation (CWE-384)
value: An OAuth session fixation vulnerability existed in the VPN login flow, where an attacker could craft a custom login URL, convince a VPN user to login via that URL, and obtain authenticated access as that user. This issue is limited to cases where attacker and victim are sharing the same source IP and could allow the ability to view session states and disconnect VPN sessions. This vulnerability affects Mozilla VPN iOS 1.0.7 < (929), Mozilla VPN Windows < 1.2.2, and Mozilla VPN Android 1.1.0 < (1360).
CWE description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
CVSS v3 base score: 7.6
Published: 2022-12-22T20:15Z
Repository: https://github.com/mozilla-mobile/guardian-vpn-ios
Language: Swift
Files changed: FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj, FirefoxPrivateNetworkVPN/AppDelegate.swift, FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift, FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift, FirefoxPrivateNetworkVPN/Networking/GuardianURLRequestPath.swift, FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h	, FirefoxPrivateNetworkVPN/Project Files/Info.plist	, FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift, FirefoxPrivateNetworkVPN/Utilities/PKCECodeGenerator.swift, FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift, FirefoxPrivateNetworkVPNTests/Mocks/MockGuardianAPI.swift
Commit date: 2020-09-24 16:52:48 +0800
Commit message: PGAND-410 Resolve oauth session fixation on iOS (#272)

* update login flow with PKCE

* fix MockGuardianAPI
Diff: diff --git a/FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj b/FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj
index 8404548..c8238ed 100644
--- a/FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj
+++ b/FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj
@@ -242,10 +242,12 @@
 		D54700E32491D90C00FA40A2 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D54700E22491D90C00FA40A2 /* UserNotifications.framework */; };
 		D54700E52491D90C00FA40A2 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D54700E42491D90C00FA40A2 /* UserNotificationsUI.framework */; };
 		D54700E82491D90C00FA40A2 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54700E72491D90C00FA40A2 /* NotificationViewController.swift */; };
 		D54700EB2491D90C00FA40A2 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D54700E92491D90C00FA40A2 /* MainInterface.storyboard */; };
 		D54700EF2491D90C00FA40A2 /* NotificationContentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = D54700E12491D90C00FA40A2 /* NotificationContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		D54D31DD2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */; };
+		D54D31DE2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */; };
 		D5D349B3249A73FB00DD9F50 /* error_noSignal.png in Resources */ = {isa = PBXBuildFile; fileRef = D5D349B1249A73FA00DD9F50 /* error_noSignal.png */; };
 		D5D349B4249A73FB00DD9F50 /* error_unstable.png in Resources */ = {isa = PBXBuildFile; fileRef = D5D349B2249A73FA00DD9F50 /* error_unstable.png */; };
 		D5D349B6249B703F00DD9F50 /* AppExtensionUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D349B5249B703F00DD9F50 /* AppExtensionUserDefaults.swift */; };
 		D5D349B7249B744C00DD9F50 /* AppExtensionUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D349B5249B703F00DD9F50 /* AppExtensionUserDefaults.swift */; };
 		D5F693A824976192009BDA0A /* WireGuardTunnelPacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F693A724976192009BDA0A /* WireGuardTunnelPacketTunnelProvider.swift */; };
@@ -591,10 +593,11 @@
 		D54700E22491D90C00FA40A2 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
 		D54700E42491D90C00FA40A2 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; };
 		D54700E72491D90C00FA40A2 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
 		D54700EA2491D90C00FA40A2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
 		D54700EC2491D90C00FA40A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCECodeGenerator.swift; sourceTree = "<group>"; };
 		D5D349B1249A73FA00DD9F50 /* error_noSignal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = error_noSignal.png; sourceTree = "<group>"; };
 		D5D349B2249A73FA00DD9F50 /* error_unstable.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = error_unstable.png; sourceTree = "<group>"; };
 		D5D349B5249B703F00DD9F50 /* AppExtensionUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionUserDefaults.swift; sourceTree = "<group>"; };
 		D5F693A724976192009BDA0A /* WireGuardTunnelPacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WireGuardTunnelPacketTunnelProvider.swift; sourceTree = "<group>"; };
 		F401097823FEE2D8006DFF96 /* HeartbeatMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeartbeatMonitor.swift; sourceTree = "<group>"; };
@@ -1132,10 +1135,11 @@
 				F4BE1E4F2433C4C10061FECD /* TunnelManagerUtilities.swift */,
 				F40831D3233D04B600A60FCA /* GuardianAPIDateFormatter.swift */,
 				F4056031239E9F31000A93CC /* KeychainStored.swift */,
 				F43DFC0A234D0B5C003EA64A /* TunnelConfigurationBuilder.swift */,
 				F4108050242CDAB300973421 /* NetworkingUtilities.swift */,
+				D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */,
 			);
 			path = Utilities;
 			sourceTree = "<group>";
 		};
 		F41CCA2123711C7A0018DC43 /* ViewControllers */ = {
@@ -2254,10 +2258,11 @@
 				F422FEF823980C1D009413E2 /* NSAttributedString+Extensions.swift in Sources */,
 				F4AED257235F835300F80ABF /* IPAddressRange.swift in Sources */,
 				F4E908D32371CAAD00EB9FE6 /* NavigationCoordinating.swift in Sources */,
 				F4E909092371CAAD00EB9FE6 /* AccountInformationCell.swift in Sources */,
 				F42DE7C82395A64A000B79C3 /* CarouselViewController.swift in Sources */,
+				D54D31DD2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */,
 				F45D1D1023873B8A0084B5DE /* CarouselPageViewController.swift in Sources */,
 				F4A7FA24237C5C960034AF6C /* Account.swift in Sources */,
 				F4E908D52371CAAD00EB9FE6 /* TunnelManaging.swift in Sources */,
 				F4E2C22B243262B200060B19 /* LoginError.swift in Sources */,
 				F4B42BFB238D88EE001F64F7 /* SimplePing.m in Sources */,
@@ -2402,10 +2407,11 @@
 				D541182D24C8358500628DF6 /* NSAttributedString+Extensions.swift in Sources */,
 				D541182E24C8358500628DF6 /* IPAddressRange.swift in Sources */,
 				D541182F24C8358500628DF6 /* NavigationCoordinating.swift in Sources */,
 				D541183024C8358500628DF6 /* AccountInformationCell.swift in Sources */,
 				D541183124C8358500628DF6 /* CarouselViewController.swift in Sources */,
+				D54D31DE2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */,
 				D541183224C8358500628DF6 /* CarouselPageViewController.swift in Sources */,
 				D541183324C8358500628DF6 /* Account.swift in Sources */,
 				D541183424C8358500628DF6 /* TunnelManaging.swift in Sources */,
 				D541183524C8358500628DF6 /* LoginError.swift in Sources */,
 				D541183624C8358500628DF6 /* SimplePing.m in Sources */,
diff --git a/FirefoxPrivateNetworkVPN/AppDelegate.swift b/FirefoxPrivateNetworkVPN/AppDelegate.swift
index d6fcc99..7deb95d 100644
--- a/FirefoxPrivateNetworkVPN/AppDelegate.swift
+++ b/FirefoxPrivateNetworkVPN/AppDelegate.swift
@@ -63,10 +63,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
         UIDevice.current.userInterfaceIdiom == .pad ? .all : [.portrait, .portraitUpsideDown]
     }
 
+    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+        if let sourceApplication = options[.sourceApplication] as? String,
+            sourceApplication == "com.apple.SafariViewService" {
+           NotificationCenter.default.post(name: .callbackURLNotification, object: nil,
+                                           userInfo: ["callbackURL": url])
+           return true
+        }
+        return false
+    }
+
     func applicationDidEnterBackground(_ application: UIApplication) {
         dependencyManager?.connectionHealthMonitor.stop()
         dependencyManager?.heartbeatMonitor.stop()
         dependencyManager?.releaseMonitor.stop()
         userDedaults.set(false, forKey: .isSwitchingInProgress)
diff --git a/FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift b/FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift
index 69a633f..b350812 100644
--- a/FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift
+++ b/FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift
@@ -18,30 +18,27 @@ class GuardianAPI: NetworkRequesting {
         self.networkLayer = networkLayer
         self.userAgentInfo = userAgentInfo
     }
 
     // MARK: -
-    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void) {
-        let urlRequest = GuardianURLRequest.urlRequest(request: .login, type: .POST)
-        networkLayer.fire(urlRequest: urlRequest) { result in
-            DispatchQueue.main.async {
-                completion(result.decode(to: LoginCheckpointModel.self))
-            }
-        }
-    }
-
     func accountInfo(token: String, completion: @escaping (Result<User, GuardianAPIError>) -> Void) {
         let urlRequest = GuardianURLRequest.urlRequest(request: .account, type: .GET, httpHeaderParams: headers(with: token))
         networkLayer.fire(urlRequest: urlRequest) { result in
             DispatchQueue.main.async {
                 completion(result.decode(to: User.self))
             }
         }
     }
 
-    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
-        let urlRequest = GuardianURLRequest.urlRequest(with: urlString, type: .GET)
+    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
+        let body: [String: Any] = ["code": code, "code_verifier": codeVerifier]
+        guard let data = try? JSONSerialization.data(withJSONObject: body) else {
+            completion(.failure(.couldNotEncodeData))
+            return
+        }
+
+        let urlRequest = GuardianURLRequest.urlRequest(request: .verify, type: .POST, httpHeaderParams: headers(), body: data)
         networkLayer.fire(urlRequest: urlRequest) { result in
             DispatchQueue.main.async {
                 completion(result.decode(to: VerifyResponse.self))
             }
         }
@@ -104,11 +101,14 @@ class GuardianAPI: NetworkRequesting {
         let urlRequest = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
         networkLayer.fire(urlRequest: urlRequest, completion: completion)
     }
 
     // MARK: - Utils
-    private func headers(with token: String) -> [String: String] {
-        return ["Authorization": "Bearer \(token)",
-            "Content-Type": "application/json",
-            "User-Agent": userAgentInfo]
+    private func headers(with token: String = "") -> [String: String] {
+        var headers = ["Content-Type": "application/json",
+                       "User-Agent": userAgentInfo]
+        if !token.isEmpty {
+            headers["Authorization"] = "Bearer \(token)"
+        }
+        return headers
     }
 }
diff --git a/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift b/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift
index f69a678..e6461d4 100644
--- a/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift
+++ b/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift
@@ -29,22 +29,17 @@ struct GuardianURLRequest {
         let urlString = "\(GuardianURLRequest.baseURL)\(request.endpoint)"
 
         return urlRequest(with: urlString, type: type, queryParameters: queryParameters, httpHeaderParams: httpHeaderParams, body: body)
     }
 
-    static func urlRequest(with urlString: String,
-                           type: HttpMethod,
-                           queryParameters: [String: String]? = nil,
-                           httpHeaderParams: [String: String]? = nil,
-                           body: Data? = nil) -> URLRequest {
-        var urlComponent = URLComponents(string: urlString)!
-        if let queryParameters = queryParameters {
-            let queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
-            urlComponent.queryItems = queryItems
-        }
-
-        var urlRequest = URLRequest(url: urlComponent.url!)
+    static private func urlRequest(with urlString: String,
+                                   type: HttpMethod,
+                                   queryParameters: [String: String]? = nil,
+                                   httpHeaderParams: [String: String]? = nil,
+                                   body: Data? = nil) -> URLRequest {
+        let url = generateURL(urlString: urlString, queryParameters: queryParameters)
+        var urlRequest = URLRequest(url: url!)
         if let httpHeaderParams = httpHeaderParams {
             httpHeaderParams.forEach {
                 urlRequest.setValue($0.value, forHTTPHeaderField: $0.key)
             }
         }
@@ -55,6 +50,22 @@ struct GuardianURLRequest {
             urlRequest.httpBody = body
         }
 
         return urlRequest
     }
+
+    static private func generateURL(urlString: String, queryParameters: [String: String]?) -> URL? {
+        var urlComponent = URLComponents(string: urlString)!
+        if let queryParameters = queryParameters {
+            let queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
+            urlComponent.queryItems = queryItems
+        }
+
+        return urlComponent.url
+    }
+
+    static func pkceLoginURL(codeChallenge: String) -> URL {
+        let urlString = "\(GuardianURLRequest.baseURL)\(GuardianURLRequestPath.login.endpoint)"
+        let queryParameters = ["code_challenge": codeChallenge, "code_challenge_method": "S256"]
+        return generateURL(urlString: urlString, queryParameters: queryParameters)!
+    }
 }
diff --git a/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequestPath.swift b/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequestPath.swift
index a720564..2977c4d 100644
--- a/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequestPath.swift
+++ b/FirefoxPrivateNetworkVPN/Networking/GuardianURLRequestPath.swift
@@ -9,24 +9,25 @@
 //  Copyright © 2020 Mozilla Corporation.
 //
 
 enum GuardianURLRequestPath {
     case login
-    case verify(String)
+    case verify
     case retrieveServers
     case account
     case addDevice
     case removeDevice(String)
     case versions
 
     var endpoint: String {
         let prefix = "api/v1/vpn/"
+        let v2Prefix = "api/v2/vpn/"
         switch self {
         case .login:
-            return prefix + "login"
-        case .verify(let token):
-            return prefix + "login/verify/" + token
+            return v2Prefix + "login/ios"
+        case .verify:
+            return v2Prefix + "login/verify"
         case .retrieveServers:
             return prefix + "servers"
         case .account:
             return prefix + "account"
         case .addDevice:
diff --git a/FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h b/FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h
index 205e89f..b28dfb9 100644
--- a/FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h	
+++ b/FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h	
@@ -16,7 +16,8 @@
 #include "x25519.h"
 #include "wireguard-go-version.h"
 #include "ringlogger.h"
 #include "key.h"
 #import "SimplePing.h"
+#import "CommonCrypto/CommonCrypto.h"
 
 #endif /* Guardian_Bridging_Header_h */
diff --git a/FirefoxPrivateNetworkVPN/Project Files/Info.plist b/FirefoxPrivateNetworkVPN/Project Files/Info.plist
index 34ee9e3..4c592a4 100644
--- a/FirefoxPrivateNetworkVPN/Project Files/Info.plist	
+++ b/FirefoxPrivateNetworkVPN/Project Files/Info.plist	
@@ -68,10 +68,23 @@
 	<string>${APP_DISPLAY_NAME}</string>
 	<key>CFBundlePackageType</key>
 	<string>APPL</string>
 	<key>CFBundleShortVersionString</key>
 	<string>$(MARKETING_VERSION)</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLName</key>
+			<string>org.mozilla.ios.FirefoxVPN</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>mozilla-vpn</string>
+			</array>
+		</dict>
+	</array>
 	<key>CFBundleVersion</key>
 	<string>$(CURRENT_PROJECT_VERSION)</string>
 	<key>ITSAppUsesNonExemptEncryption</key>
 	<false/>
 	<key>LSRequiresIPhoneOS</key>
diff --git a/FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift b/FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift
index ff88649..a6c7bb6 100644
--- a/FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift
+++ b/FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift
@@ -10,12 +10,11 @@
 //
 
 import Foundation
 
 protocol NetworkRequesting {
-    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void)
     func accountInfo(token: String, completion: @escaping (Result<User, GuardianAPIError>) -> Void)
-    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void)
+    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void)
     func availableServers(with token: String, completion: @escaping (Result<[VPNCountry], GuardianAPIError>) -> Void)
     func addDevice(with token: String, body: [String: Any], completion: @escaping (Result<Device, GuardianAPIError>) -> Void)
     func removeDevice(with token: String, deviceKey: String, completion: @escaping (Result<Void, GuardianAPIError>) -> Void)
 }
diff --git a/FirefoxPrivateNetworkVPN/Utilities/PKCECodeGenerator.swift b/FirefoxPrivateNetworkVPN/Utilities/PKCECodeGenerator.swift
new file mode 100644
index 0000000..3ed0de1
--- /dev/null
+++ b/FirefoxPrivateNetworkVPN/Utilities/PKCECodeGenerator.swift
@@ -0,0 +1,42 @@
+//
+//  PKCECodeGenerator
+//  FirefoxPrivateNetworkVPN
+//
+//  This Source Code Form is subject to the terms of the Mozilla Public
+//  License, v. 2.0. If a copy of the MPL was not distributed with this
+//  file, You can obtain one at https://mozilla.org/MPL/2.0/.
+//
+//  Copyright © 2020 Mozilla Corporation.
+//
+
+import Foundation
+
+struct PKCECodeGenerator {
+    static var generateCode: (codeChallenge: String, codeVerifier: String) {
+        let codeVerifier = generateCodeVerifier()
+        let codeChallenge = base64UrlEncode(sha256(string: codeVerifier))
+        return (codeChallenge, codeVerifier)
+    }
+
+    private static func generateCodeVerifier() -> String {
+        var buffer = [UInt8](repeating: 0, count: 32)
+        _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
+        return base64UrlEncode(Data(buffer))
+    }
+
+    private static func base64UrlEncode(_ data: Data?) -> String {
+        guard let data = data else { return "" }
+        return data.base64EncodedString()
+                   .replacingOccurrences(of: "+", with: "-")
+                   .replacingOccurrences(of: "/", with: "_")
+    }
+
+    private static func sha256(string: String) -> Data? {
+        guard let data = string.data(using: .utf8) else { return nil }
+        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+        data.withUnsafeBytes {
+            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
+        }
+        return Data(hash)
+    }
+}
diff --git a/FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift b/FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift
index 1894258..e67a1e3 100644
--- a/FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift
+++ b/FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift
@@ -10,90 +10,83 @@
 //
 
 import UIKit
 import SafariServices
 
+extension Notification.Name {
+    static let callbackURLNotification = Notification.Name("callbackURL")
+}
+
 class LoginViewController: UIViewController, Navigating {
     static var navigableItem: NavigableItem = .login
 
     private let guardianAPI = DependencyManager.shared.guardianAPI
     private var safariViewController: SFSafariViewController?
-    private var verificationURL: URL?
-    private var verifyTimer: Timer?
-    private var isVerifying = false
+    private let accountManager = DependencyManager.shared.accountManager
+    private let PKCECode: (String, String) = PKCECodeGenerator.generateCode
 
     init() {
         super.init(nibName: nil, bundle: nil)
-        guardianAPI.initiateUserLogin { [weak self] result in
-            switch result {
-            case .success(let checkpointModel):
-                guard let loginURL = checkpointModel.loginUrl else { return }
-                self?.verificationURL = checkpointModel.verificationUrl
-                let safariViewController = SFSafariViewController(url: loginURL)
-                DispatchQueue.main.async {
-                    self?.addChild(safariViewController)
-                    self?.view.addSubview(safariViewController.view)
-                    safariViewController.view.frame = self?.view.bounds ?? CGRect.zero
-                    safariViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-                    safariViewController.didMove(toParent: self)
-                }
-                safariViewController.delegate = self
-                self?.safariViewController = safariViewController
-            case .failure(let error):
-                let loginError = error.getLoginError()
-                let context: NavigableContext = loginError == .maxDevicesReached ? .maxDevicesReached : .error(loginError)
-                self?.navigate(to: .landing, context: context)
-            }
-        }
-    }
-
-    deinit {
-        verifyTimer?.invalidate()
     }
 
     required init?(coder: NSCoder) {
         fatalError("init(coder:) has not been implemented")
     }
 
-    @objc private func verify() {
-        guard let verificationURL = verificationURL else { return }
-        if isVerifying { return }
-        isVerifying = true
+    override func viewDidLoad() {
+        super.viewDidLoad()
 
-        guardianAPI.verify(urlString: verificationURL.absoluteString) { [weak self] result in
-            guard let self = self else { return }
+        let safariViewController = SFSafariViewController(url: GuardianURLRequest.pkceLoginURL(codeChallenge: PKCECode.0))
+        addChild(safariViewController)
+        view.addSubview(safariViewController.view)
+        safariViewController.view.frame = self.view.bounds
+        safariViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+        safariViewController.didMove(toParent: self)
+        safariViewController.delegate = self
+        self.safariViewController = safariViewController
+
+        NotificationCenter.default.addObserver(self, selector: #selector(handleCallback), name: .callbackURLNotification, object: nil)
+    }
 
+    @objc private func handleCallback(notification: Notification) {
+        guard let url = notification.userInfo?["callbackURL"] as? URL,
+            let queryItems = URLComponents(string: url.absoluteString)?.queryItems,
+            let code = queryItems.first(where: { $0.name == "code" })?.value else {
+            navigate(to: .landing)
+            return
+        }
+        verify(code: code)
+    }
+
+    private func verify(code: String) {
+        guardianAPI.verify(code: code, codeVerifier: PKCECode.1) { [weak self] result in
+            guard let self = self else { return }
             switch result {
             case .success(let verification):
-                DependencyManager.shared.accountManager.login(with: verification) { loginResult in
-                    self.isVerifying = false
-                    self.verifyTimer?.invalidate()
-                    switch loginResult {
-                    case .success:
-                        self.navigate(to: .home)
-                    case .failure(let error):
-                        Logger.global?.log(message: "Authentication Error: \(error)")
-                        let context: NavigableContext = error == .maxDevicesReached ? .maxDevicesReached : .error(error)
-                        self.navigate(to: .landing, context: context)
-                    }
-                }
-            case .failure:
-                self.isVerifying = false
-                return
+                self.login(verification: verification)
+            case .failure(let error):
+                self.navigate(to: .landing, context: .error(error))
             }
         }
     }
-}
 
-// MARK: - SFSafariViewControllerDelegate
-extension LoginViewController: SFSafariViewControllerDelegate {
-    func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
-        if didLoadSuccessfully && verifyTimer == nil {
-            verifyTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(verify), userInfo: nil, repeats: true)
+    private func login(verification: VerifyResponse) {
+        accountManager.login(with: verification) { [weak self] loginResult in
+            guard let self = self else { return }
+            switch loginResult {
+            case .success:
+                self.navigate(to: .home)
+            case .failure(let error):
+                Logger.global?.log(message: "Authentication Error: \(error)")
+                let context: NavigableContext = error == .maxDevicesReached ? .maxDevicesReached : .error(error)
+                self.navigate(to: .landing, context: context)
+            }
         }
     }
+}
 
+// MARK: - SFSafariViewControllerDelegate
+extension LoginViewController: SFSafariViewControllerDelegate {
     func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
-        self.verifyTimer?.invalidate()
         navigate(to: .landing)
     }
 }
diff --git a/FirefoxPrivateNetworkVPNTests/Mocks/MockGuardianAPI.swift b/FirefoxPrivateNetworkVPNTests/Mocks/MockGuardianAPI.swift
index ef1d4ce..55a5330 100644
--- a/FirefoxPrivateNetworkVPNTests/Mocks/MockGuardianAPI.swift
+++ b/FirefoxPrivateNetworkVPNTests/Mocks/MockGuardianAPI.swift
@@ -15,27 +15,19 @@ import RxSwift
 
 class MockGuardianAPI: NetworkRequesting {
 
     var apiCallObservable = PublishSubject<Void>()
 
-    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void) {
-        apiCallObservable.onNext(())
-        guard let jsonUserURL = Bundle.main.url(forResource: "loginCheckpointModel", withExtension: "json"),
-            let loginCheckpointModel = try? JSONDecoder().decode(LoginCheckpointModel.self, from: Data(contentsOf: jsonUserURL))
-            else { return }
-        completion(Result.success(loginCheckpointModel))
-    }
-
     func accountInfo(token: String, completion: @escaping (Result<User, GuardianAPIError>) -> Void) {
         apiCallObservable.onNext(())
         guard let jsonUserURL = Bundle.main.url(forResource: "user", withExtension: "json"),
             let user = try? JSONDecoder().decode(User.self, from: Data(contentsOf: jsonUserURL))
             else { return }
         completion(Result.success(user))
     }
 
-    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
+    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
         apiCallObservable.onNext(())
         guard let jsonUserURL = Bundle.main.url(forResource: "verifyResponse", withExtension: "json"),
             let verifyResponse = try? JSONDecoder().decode(VerifyResponse.self, from: Data(contentsOf: jsonUserURL))
             else { return }
         completion(Result.success(verifyResponse))

Vulnerable code:     func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void) {
        let urlRequest = GuardianURLRequest.urlRequest(request: .login, type: .POST)
        networkLayer.fire(urlRequest: urlRequest) { result in
            DispatchQueue.main.async {
                completion(result.decode(to: LoginCheckpointModel.self))
            }
        }
    }

    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
        let urlRequest = GuardianURLRequest.urlRequest(with: urlString, type: .GET)
    private func headers(with token: String) -> [String: String] {
        return ["Authorization": "Bearer \(token)",
            "Content-Type": "application/json",
            "User-Agent": userAgentInfo]
    static func urlRequest(with urlString: String,
                           type: HttpMethod,
                           queryParameters: [String: String]? = nil,
                           httpHeaderParams: [String: String]? = nil,
                           body: Data? = nil) -> URLRequest {
        var urlComponent = URLComponents(string: urlString)!
        if let queryParameters = queryParameters {
            let queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
            urlComponent.queryItems = queryItems
        }

        var urlRequest = URLRequest(url: urlComponent.url!)
    case verify(String)
            return prefix + "login"
        case .verify(let token):
            return prefix + "login/verify/" + token
    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void)
    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void)
    private var verificationURL: URL?
    private var verifyTimer: Timer?
    private var isVerifying = false
        guardianAPI.initiateUserLogin { [weak self] result in
            switch result {
            case .success(let checkpointModel):
                guard let loginURL = checkpointModel.loginUrl else { return }
                self?.verificationURL = checkpointModel.verificationUrl
                let safariViewController = SFSafariViewController(url: loginURL)
                DispatchQueue.main.async {
                    self?.addChild(safariViewController)
                    self?.view.addSubview(safariViewController.view)
                    safariViewController.view.frame = self?.view.bounds ?? CGRect.zero
                    safariViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
                    safariViewController.didMove(toParent: self)
                }
                safariViewController.delegate = self
                self?.safariViewController = safariViewController
            case .failure(let error):
                let loginError = error.getLoginError()
                let context: NavigableContext = loginError == .maxDevicesReached ? .maxDevicesReached : .error(loginError)
                self?.navigate(to: .landing, context: context)
            }
        }
    }

    deinit {
        verifyTimer?.invalidate()
    @objc private func verify() {
        guard let verificationURL = verificationURL else { return }
        if isVerifying { return }
        isVerifying = true
        guardianAPI.verify(urlString: verificationURL.absoluteString) { [weak self] result in
            guard let self = self else { return }
                DependencyManager.shared.accountManager.login(with: verification) { loginResult in
                    self.isVerifying = false
                    self.verifyTimer?.invalidate()
                    switch loginResult {
                    case .success:
                        self.navigate(to: .home)
                    case .failure(let error):
                        Logger.global?.log(message: "Authentication Error: \(error)")
                        let context: NavigableContext = error == .maxDevicesReached ? .maxDevicesReached : .error(error)
                        self.navigate(to: .landing, context: context)
                    }
                }
            case .failure:
                self.isVerifying = false
                return
}
// MARK: - SFSafariViewControllerDelegate
extension LoginViewController: SFSafariViewControllerDelegate {
    func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
        if didLoadSuccessfully && verifyTimer == nil {
            verifyTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(verify), userInfo: nil, repeats: true)
        self.verifyTimer?.invalidate()
    func initiateUserLogin(completion: @escaping (Result<LoginCheckpointModel, GuardianAPIError>) -> Void) {
        apiCallObservable.onNext(())
        guard let jsonUserURL = Bundle.main.url(forResource: "loginCheckpointModel", withExtension: "json"),
            let loginCheckpointModel = try? JSONDecoder().decode(LoginCheckpointModel.self, from: Data(contentsOf: jsonUserURL))
            else { return }
        completion(Result.success(loginCheckpointModel))
    }

    func verify(urlString: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
Fixed code: 		D54D31DD2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */; };
		D54D31DE2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */; };
		D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKCECodeGenerator.swift; sourceTree = "<group>"; };
				D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */,
				D54D31DD2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */,
				D54D31DE2519F33500D6ED57 /* PKCECodeGenerator.swift in Sources */,
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        if let sourceApplication = options[.sourceApplication] as? String,
            sourceApplication == "com.apple.SafariViewService" {
           NotificationCenter.default.post(name: .callbackURLNotification, object: nil,
                                           userInfo: ["callbackURL": url])
           return true
        }
        return false
    }

    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {
        let body: [String: Any] = ["code": code, "code_verifier": codeVerifier]
        guard let data = try? JSONSerialization.data(withJSONObject: body) else {
            completion(.failure(.couldNotEncodeData))
            return
        }

        let urlRequest = GuardianURLRequest.urlRequest(request: .verify, type: .POST, httpHeaderParams: headers(), body: data)
    private func headers(with token: String = "") -> [String: String] {
        var headers = ["Content-Type": "application/json",
                       "User-Agent": userAgentInfo]
        if !token.isEmpty {
            headers["Authorization"] = "Bearer \(token)"
        }
        return headers
    static private func urlRequest(with urlString: String,
                                   type: HttpMethod,
                                   queryParameters: [String: String]? = nil,
                                   httpHeaderParams: [String: String]? = nil,
                                   body: Data? = nil) -> URLRequest {
        let url = generateURL(urlString: urlString, queryParameters: queryParameters)
        var urlRequest = URLRequest(url: url!)

    static private func generateURL(urlString: String, queryParameters: [String: String]?) -> URL? {
        var urlComponent = URLComponents(string: urlString)!
        if let queryParameters = queryParameters {
            let queryItems = queryParameters.map { URLQueryItem(name: $0.key, value: $0.value) }
            urlComponent.queryItems = queryItems
        }

        return urlComponent.url
    }

    static func pkceLoginURL(codeChallenge: String) -> URL {
        let urlString = "\(GuardianURLRequest.baseURL)\(GuardianURLRequestPath.login.endpoint)"
        let queryParameters = ["code_challenge": codeChallenge, "code_challenge_method": "S256"]
        return generateURL(urlString: urlString, queryParameters: queryParameters)!
    }
    case verify
        let v2Prefix = "api/v2/vpn/"
            return v2Prefix + "login/ios"
        case .verify:
            return v2Prefix + "login/verify"
#import "CommonCrypto/CommonCrypto.h"
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleTypeRole</key>
			<string>Editor</string>
			<key>CFBundleURLName</key>
			<string>org.mozilla.ios.FirefoxVPN</string>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>mozilla-vpn</string>
			</array>
		</dict>
	</array>
    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void)
//
//  PKCECodeGenerator
//  FirefoxPrivateNetworkVPN
//
//  This Source Code Form is subject to the terms of the Mozilla Public
//  License, v. 2.0. If a copy of the MPL was not distributed with this
//  file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
//  Copyright © 2020 Mozilla Corporation.
//

import Foundation

struct PKCECodeGenerator {
    static var generateCode: (codeChallenge: String, codeVerifier: String) {
        let codeVerifier = generateCodeVerifier()
        let codeChallenge = base64UrlEncode(sha256(string: codeVerifier))
        return (codeChallenge, codeVerifier)
    }

    private static func generateCodeVerifier() -> String {
        var buffer = [UInt8](repeating: 0, count: 32)
        _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
        return base64UrlEncode(Data(buffer))
    }

    private static func base64UrlEncode(_ data: Data?) -> String {
        guard let data = data else { return "" }
        return data.base64EncodedString()
                   .replacingOccurrences(of: "+", with: "-")
                   .replacingOccurrences(of: "/", with: "_")
    }

    private static func sha256(string: String) -> Data? {
        guard let data = string.data(using: .utf8) else { return nil }
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
        }
        return Data(hash)
    }
}
extension Notification.Name {
    static let callbackURLNotification = Notification.Name("callbackURL")
}

    private let accountManager = DependencyManager.shared.accountManager
    private let PKCECode: (String, String) = PKCECodeGenerator.generateCode
    override func viewDidLoad() {
        super.viewDidLoad()
        let safariViewController = SFSafariViewController(url: GuardianURLRequest.pkceLoginURL(codeChallenge: PKCECode.0))
        addChild(safariViewController)
        view.addSubview(safariViewController.view)
        safariViewController.view.frame = self.view.bounds
        safariViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        safariViewController.didMove(toParent: self)
        safariViewController.delegate = self
        self.safariViewController = safariViewController

        NotificationCenter.default.addObserver(self, selector: #selector(handleCallback), name: .callbackURLNotification, object: nil)
    }
    @objc private func handleCallback(notification: Notification) {
        guard let url = notification.userInfo?["callbackURL"] as? URL,
            let queryItems = URLComponents(string: url.absoluteString)?.queryItems,
            let code = queryItems.first(where: { $0.name == "code" })?.value else {
            navigate(to: .landing)
            return
        }
        verify(code: code)
    }

    private func verify(code: String) {
        guardianAPI.verify(code: code, codeVerifier: PKCECode.1) { [weak self] result in
            guard let self = self else { return }
                self.login(verification: verification)
            case .failure(let error):
                self.navigate(to: .landing, context: .error(error))
    private func login(verification: VerifyResponse) {
        accountManager.login(with: verification) { [weak self] loginResult in
            guard let self = self else { return }
            switch loginResult {
            case .success:
                self.navigate(to: .home)
            case .failure(let error):
                Logger.global?.log(message: "Authentication Error: \(error)")
                let context: NavigableContext = error == .maxDevicesReached ? .maxDevicesReached : .error(error)
                self.navigate(to: .landing, context: context)
            }
}
// MARK: - SFSafariViewControllerDelegate
extension LoginViewController: SFSafariViewControllerDelegate {
    func verify(code: String, codeVerifier: String, completion: @escaping (Result<VerifyResponse, GuardianAPIError>) -> Void) {