Commit 194d2d46 authored by Jose Blaya's avatar Jose Blaya
Browse files

Merge branch 'develop' into release/2.0.0

# Conflicts:
#	PIALibrary.podspec
#	PIALibrary.xcodeproj/project.pbxproj
#	PIALibrary/Resources/UI/iOS/Signup.storyboard
#	PIALibrary/Resources/UI/iOS/Welcome.storyboard
#	Podfile.lock
parents a0248dea 962f9794
......@@ -38,7 +38,7 @@ Pod::Spec.new do |s|
p.frameworks = "NetworkExtension"
p.pod_target_xcconfig = { "APPLICATION_EXTENSION_API_ONLY" => "YES" }
p.dependency "PIATunnel", "~> 1.1.7"
p.dependency "PIATunnel", "~> 1.1.8"
p.dependency "PIALibrary/Library"
end
......
This diff is collapsed.
......@@ -22,6 +22,12 @@ public protocol AccountProvider: class {
/// The user account currently logged in, or `nil` if logged out.
var currentUser: UserAccount? { get set }
/// The current auth token, or 'nil' if logged out.
var token: String? { get }
/// The public username to be displayed in the views.
var publicUsername: String? { get }
/// The password reference object associated with the currentUser, or `nil` if logged out.
var currentPasswordReference: Data? { get }
......@@ -74,6 +80,11 @@ public protocol AccountProvider: class {
*/
func logout(_ callback: SuccessLibraryCallback?)
/**
Remove all data from the plain and secure internal database
*/
func cleanDatabase()
#if os(iOS)
/**
Lists the available plans with their corresponding product to purchase in order to get them.
......
......@@ -12,7 +12,7 @@ protocol PlainStore: class {
// MARK: Account
var username: String? { get set }
var publicUsername: String? { get set }
var accountInfo: AccountInfo? { get set }
......
......@@ -13,11 +13,23 @@ protocol SecureStore: class {
@discardableResult func setPublicKey(withData data: Data) -> SecKey?
func username() -> String?
func setUsername(_ username: String?)
func password(for username: String) -> String?
func setPassword(_ password: String?, for username: String)
func passwordReference(for username: String) -> Data?
func token(for username: String) -> String?
func setToken(_ token: String?, for username: String)
func tokenReference(for username: String) -> Data?
func tokenKey(for username: String) -> String
func clear(for username: String)
}
......@@ -22,4 +22,13 @@ public struct Credentials {
self.username = username
self.password = password
}
}
public extension Credentials {
public func toJSONDictionary() -> [String: Any] {
return ["username":username, "password": password]
}
}
......@@ -15,8 +15,10 @@ protocol WebServicesConsumer {
protocol WebServices: class {
// MARK: Account
func info(credentials: Credentials, _ callback: LibraryCallback<AccountInfo>?)
func token(credentials: Credentials, _ callback: LibraryCallback<String>?)
func info(token: String, _ callback: LibraryCallback<AccountInfo>?)
func update(credentials: Credentials, email: String, _ callback: SuccessLibraryCallback?)
......
......@@ -41,15 +41,22 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
#endif
var isLoggedIn: Bool {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return false
}
return (accessedDatabase.secure.password(for: username) != nil)
}
var publicUsername: String? {
guard let username = accessedDatabase.plain.publicUsername else {
return nil
}
return username
}
var currentUser: UserAccount? {
get {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return nil
}
guard let password = accessedDatabase.secure.password(for: username) else {
......@@ -62,21 +69,30 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
set {
if let user = newValue {
accessedDatabase.plain.username = user.credentials.username
accessedDatabase.plain.publicUsername = user.credentials.username
accessedDatabase.secure.setPassword(user.credentials.password, for: user.credentials.username)
accessedDatabase.plain.accountInfo = user.info
} else {
if let username = accessedDatabase.plain.username {
if let username = accessedDatabase.secure.username() {
accessedDatabase.secure.setPassword(nil, for: username)
accessedDatabase.secure.setUsername(nil)
}
accessedDatabase.plain.username = nil
accessedDatabase.plain.publicUsername = nil
accessedDatabase.plain.accountInfo = nil
}
}
}
var token: String? {
guard let username = accessedDatabase.secure.username() else {
return nil
}
return accessedDatabase.secure.token(for: accessedDatabase.secure.tokenKey(for: username))
}
var currentPasswordReference: Data? {
guard let username = accessedDatabase.plain.username else {
guard let username = accessedDatabase.secure.username() else {
return nil
}
return accessedDatabase.secure.passwordReference(for: username)
......@@ -91,32 +107,80 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
#endif
private func updateDatabaseWith(_ token: String, andUsername username: String) {
let tokenComponents = token.split(by: token.count/2)
if let first = tokenComponents.first,
let last = tokenComponents.last {
self.accessedDatabase.plain.publicUsername = username
self.accessedDatabase.secure.setUsername(first)
self.accessedDatabase.secure.setToken(token,
for: self.accessedDatabase.secure.tokenKey(for: first))
self.accessedDatabase.secure.setPassword(last,
for: first)
}
}
func login(with request: LoginRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
guard !isLoggedIn else {
preconditionFailure()
}
webServices.info(credentials: request.credentials) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
webServices.token(credentials: request.credentials) { (token, error) in
guard let token = token else {
callback?(nil, error)
return
}
self.accessedDatabase.plain.username = request.credentials.username
self.accessedDatabase.secure.setPassword(request.credentials.password, for: request.credentials.username)
self.accessedDatabase.plain.accountInfo = accountInfo
let user = UserAccount(credentials: request.credentials, info: accountInfo)
Macros.postNotification(.PIAAccountDidLogin, [
.user: user
])
callback?(user, nil)
self.updateDatabaseWith(token,
andUsername: request.credentials.username)
self.webServices.info(token: token) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
callback?(nil, error)
return
}
//Save after confirm the login was successful.
self.accessedDatabase.plain.accountInfo = accountInfo
let user = UserAccount(credentials: request.credentials, info: accountInfo)
Macros.postNotification(.PIAAccountDidLogin, [
.user: user
])
callback?(user, nil)
}
}
}
func refreshAccountInfo(_ callback: ((AccountInfo?, Error?) -> Void)?) {
guard let user = currentUser else {
preconditionFailure()
guard let token = self.token else {
guard let user = currentUser else {
preconditionFailure()
}
self.webServices.token(credentials: user.credentials) { (token, error) in
if let token = token {
self.updateDatabaseWith(token,
andUsername: user.credentials.username)
self.accountInfoWith(token, callback)
}
}
return
}
webServices.info(credentials: user.credentials) { (accountInfo, error) in
accountInfoWith(token, callback)
}
private func accountInfoWith(_ token: String, _ callback: ((AccountInfo?, Error?) -> Void)?) {
webServices.info(token: token) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
callback?(nil, error)
return
......@@ -124,7 +188,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
self.accessedDatabase.plain.accountInfo = accountInfo
Macros.postNotification(.PIAAccountDidRefresh, [
.accountInfo: accountInfo
])
])
callback?(accountInfo, nil)
}
}
......@@ -157,11 +221,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
guard isLoggedIn else {
preconditionFailure()
}
if let username = accessedDatabase.plain.username {
accessedDatabase.secure.setPassword(nil, for: username)
}
accessedDatabase.plain.username = nil
accessedDatabase.plain.accountInfo = nil
cleanDatabase()
Macros.postNotification(.PIAAccountDidLogout)
callback?(nil)
}
......@@ -238,7 +298,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
self.accessedStore.finishTransaction(transaction, success: true)
}
self.accessedDatabase.plain.lastSignupEmail = nil
self.accessedDatabase.plain.username = credentials.username
self.accessedDatabase.plain.publicUsername = credentials.username
self.accessedDatabase.secure.setPassword(credentials.password, for: credentials.username)
let user = UserAccount(credentials: credentials, info: nil)
......@@ -264,14 +324,35 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
callback?(nil, error)
return
}
self.accessedDatabase.plain.username = credentials.username
self.accessedDatabase.secure.setPassword(credentials.password, for: credentials.username)
let user = UserAccount(credentials: credentials, info: nil)
Macros.postNotification(.PIAAccountDidSignup, [
.user: user
])
callback?(user, nil)
self.webServices.token(credentials: credentials) { (token, error) in
guard let token = token else {
callback?(nil, error)
return
}
let tokenComponents = token.split(by: token.count/2)
if let first = tokenComponents.first,
let last = tokenComponents.last {
self.accessedDatabase.plain.publicUsername = credentials.username
self.accessedDatabase.secure.setUsername(first)
self.accessedDatabase.secure.setToken(token,
for: self.accessedDatabase.secure.tokenKey(for: first))
self.accessedDatabase.secure.setPassword(last,
for: first)
}
let user = UserAccount(credentials: credentials, info: nil)
Macros.postNotification(.PIAAccountDidSignup, [
.user: user
])
callback?(user, nil)
}
}
}
......@@ -314,6 +395,9 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
func renew(with request: RenewRequest, _ callback: ((UserAccount?, Error?) -> Void)?) {
guard let token = token else {
preconditionFailure()
}
guard let user = currentUser else {
preconditionFailure()
}
......@@ -335,7 +419,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
Macros.postNotification(.PIAAccountDidRenew)
self.webServices.info(credentials: user.credentials) { (accountInfo, error) in
self.webServices.info(token: token) { (accountInfo, error) in
guard let newAccountInfo = accountInfo else {
callback?(nil, nil)
return
......@@ -345,11 +429,24 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
let user = UserAccount(credentials: user.credentials, info: newAccountInfo)
Macros.postNotification(.PIAAccountDidRefresh, [
.user: user
])
])
callback?(user, nil)
}
}
}
/**
Remove all data from the plain and secure internal database
*/
func cleanDatabase() {
if let username = accessedDatabase.secure.username() {
accessedDatabase.secure.setPassword(nil, for: username)
accessedDatabase.secure.setUsername(nil)
accessedDatabase.secure.setToken(nil, for: accessedDatabase.secure.tokenKey(for: username))
}
accessedDatabase.plain.publicUsername = nil
accessedDatabase.plain.accountInfo = nil
}
#endif
// MARK: WebServicesConsumer
......
......@@ -57,7 +57,7 @@ extension Client {
- Returns: `self`
*/
@discardableResult public func truncate() -> Self {
if let username = plain.username {
if let username = plain.publicUsername {
secure.clear(for: username)
}
plain.clear()
......
......@@ -58,6 +58,46 @@ class KeychainStore: SecureStore {
func clear(for username: String) {
backend.removePassword(for: username)
backend.removeToken(for: tokenKey(for: username))
backend.remove(publicKeyWithIdentifier: Entries.publicKey)
}
}
extension KeychainStore {
func username() -> String? {
return try? backend.username()
}
func setUsername(_ username: String?) {
if let username = username {
try? backend.set(username: username)
} else {
backend.removeUsername()
}
}
}
extension KeychainStore {
func token(for username: String) -> String? {
return try? backend.token(for: username)
}
func setToken(_ token: String?, for username: String) {
if let token = token {
try? backend.set(token: token, for: username)
} else {
backend.removeToken(for: username)
}
}
func tokenReference(for username: String) -> Data? {
return try? backend.tokenReference(for: username)
}
func tokenKey(for username: String) -> String {
return "auth-token: \(username)"
}
}
......@@ -76,7 +76,7 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
// MARK: Account
var username: String? {
var publicUsername: String? {
get {
return backend.string(forKey: Entries.username)
}
......
......@@ -17,7 +17,9 @@ enum ClientEndpoint: String, Endpoint {
case redeem = "giftcard_redeem"
case account
case token = "v2/token"
case account = "v2/account"
case payment
......
//
// GlossToken.swift
// PIALibrary-iOS
//
// Created by Jose Antonio Blaya Garcia on 28/11/2018.
// Copyright © 2018 London Trust Media. All rights reserved.
//
import Foundation
import Gloss
class GlossToken: GlossParser {
let parsed: String
required init?(json: JSON) {
let token: String? = "token" <~~ json
parsed = token ?? ""
}
}
......@@ -16,7 +16,36 @@ private let log = SwiftyBeaver.self
class PIAWebServices: WebServices, ConfigurationAccess {
private static let serversVersion = 60
func info(credentials: Credentials, _ callback: ((AccountInfo?, Error?) -> Void)?) {
/***
Generates a new auth token for the specific user
*/
func token(credentials: Credentials, _ callback: ((String?, Error?) -> Void)?) {
let endpoint = ClientEndpoint.token
let status = [200, 401, 429]
let errors: [Int: ClientError] = [
401: .unauthorized,
429: .throttled
]
let parameters = credentials.toJSONDictionary()
req(nil, .post, endpoint, parameters, status, JSONRequestExecutor() { (json, status, error) in
if let knownError = self.knownError(endpoint, status, errors) {
callback?(nil, knownError)
return
}
guard let json = json else {
callback?(nil, error)
return
}
guard let token = GlossToken(json: json)?.parsed else {
callback?(nil, ClientError.malformedResponseData)
return
}
callback?(token, nil)
})
}
func info(token: String, _ callback: ((AccountInfo?, Error?) -> Void)?) {
let endpoint = ClientEndpoint.account
let status = [200, 401, 429]
let errors: [Int: ClientError] = [
......@@ -24,7 +53,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {
429: .throttled
]
req(credentials, .get, endpoint, nil, status, JSONRequestExecutor() { (json, status, error) in
req(nil, .get, endpoint, useAuthToken: true, nil, status, JSONRequestExecutor() { (json, status, error) in
if let knownError = self.knownError(endpoint, status, errors) {
callback?(nil, knownError)
return
......@@ -186,17 +215,19 @@ class PIAWebServices: WebServices, ConfigurationAccess {
_ credentials: Credentials?,
_ method: HTTPMethod,
_ endpoint: Endpoint,
useAuthToken useToken: Bool = false,
_ parameters: [String: Any]?,
_ statuses: [Int],
_ executor: RequestExecutor) {
req(credentials, method, endpoint.url, parameters, statuses, executor)
req(credentials, method, endpoint.url, useToken, parameters, statuses, executor)
}
private func req(
_ credentials: Credentials?,
_ method: HTTPMethod,
_ url: URL,
_ useToken: Bool,
_ parameters: [String: Any]?,
_ statuses: [Int],
_ executor: RequestExecutor) {
......@@ -207,6 +238,11 @@ class PIAWebServices: WebServices, ConfigurationAccess {
headers[authHeader.key] = authHeader.value
}
if useToken,
let token = Client.providers.accountProvider.token {
headers["Authorization"] = "Token \(token)"
}
if let parameters = parameters {
log.debug("Request: \(method) \"\(url)\", parameters: \(parameters), headers: \(headers)")
} else {
......
......@@ -10,7 +10,7 @@ import Foundation
/// Simulates account-related operations
public class MockAccountProvider: AccountProvider, WebServicesConsumer {
/// Mocks the outcome of a sign-up operation.
///
/// - Seealso: `AccountProvider.signup(...)`
......@@ -111,6 +111,14 @@ public class MockAccountProvider: AccountProvider, WebServicesConsumer {
return delegate.isLoggedIn
}
public var token: String? {
return "TOKEN"
}
public var publicUsername: String? {
return "p0000000"
}
/// :nodoc:
public var currentUser: UserAccount? {
get {
......@@ -161,6 +169,11 @@ public class MockAccountProvider: AccountProvider, WebServicesConsumer {
delegate.logout(callback)
}
/// :nodoc:
public func cleanDatabase() {
delegate.cleanDatabase()
}