Skip to content
Permalink
Browse files Browse the repository at this point in the history
PGAND-410 Resolve oauth session fixation on iOS (#272)
* update login flow with PKCE

* fix MockGuardianAPI
  • Loading branch information
CelesteTang committed Sep 24, 2020
1 parent ee7e380 commit 4309f5c
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 98 deletions.
6 changes: 6 additions & 0 deletions FirefoxPrivateNetworkVPN.xcodeproj/project.pbxproj
Expand Up @@ -244,6 +244,8 @@
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 */; };
Expand Down Expand Up @@ -593,6 +595,7 @@
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>"; };
Expand Down Expand Up @@ -1134,6 +1137,7 @@
F4056031239E9F31000A93CC /* KeychainStored.swift */,
F43DFC0A234D0B5C003EA64A /* TunnelConfigurationBuilder.swift */,
F4108050242CDAB300973421 /* NetworkingUtilities.swift */,
D54D31DC2519F33500D6ED57 /* PKCECodeGenerator.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -2256,6 +2260,7 @@
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 */,
Expand Down Expand Up @@ -2404,6 +2409,7 @@
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 */,
Expand Down
10 changes: 10 additions & 0 deletions FirefoxPrivateNetworkVPN/AppDelegate.swift
Expand Up @@ -65,6 +65,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
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()
Expand Down
30 changes: 15 additions & 15 deletions FirefoxPrivateNetworkVPN/Networking/GuardianAPI.swift
Expand Up @@ -20,15 +20,6 @@ class GuardianAPI: NetworkRequesting {
}

// 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
Expand All @@ -38,8 +29,14 @@ class GuardianAPI: NetworkRequesting {
}
}

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))
Expand Down Expand Up @@ -106,9 +103,12 @@ class GuardianAPI: NetworkRequesting {
}

// 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
}
}
35 changes: 23 additions & 12 deletions FirefoxPrivateNetworkVPN/Networking/GuardianURLRequest.swift
Expand Up @@ -31,18 +31,13 @@ struct GuardianURLRequest {
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)
Expand All @@ -57,4 +52,20 @@ struct GuardianURLRequest {

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)!
}
}
Expand Up @@ -11,7 +11,7 @@
enum GuardianURLRequestPath {
case login
case verify(String)
case verify
case retrieveServers
case account
case addDevice
Expand All @@ -20,11 +20,12 @@ enum GuardianURLRequestPath {

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:
Expand Down
1 change: 1 addition & 0 deletions FirefoxPrivateNetworkVPN/Project Files/Bridging-Header.h
Expand Up @@ -18,5 +18,6 @@
#include "ringlogger.h"
#include "key.h"
#import "SimplePing.h"
#import "CommonCrypto/CommonCrypto.h"

#endif /* Guardian_Bridging_Header_h */
13 changes: 13 additions & 0 deletions FirefoxPrivateNetworkVPN/Project Files/Info.plist
Expand Up @@ -70,6 +70,19 @@
<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>
Expand Down
3 changes: 1 addition & 2 deletions FirefoxPrivateNetworkVPN/Protocols/NetworkRequesting.swift
Expand Up @@ -12,9 +12,8 @@
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)
Expand Down
42 changes: 42 additions & 0 deletions 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)
}
}
105 changes: 49 additions & 56 deletions FirefoxPrivateNetworkVPN/ViewControllers/LoginViewController.swift
Expand Up @@ -12,88 +12,81 @@
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)
}
}

0 comments on commit 4309f5c

Please sign in to comment.