Commit f1001fcc authored by Jose Blaya's avatar Jose Blaya
Browse files

Merge branch...

Merge branch '65-auth-token-store-the-token-to-be-used-in-all-account-requests' into '64-auth-token-create-new-service-post-api-client-v2-token'

Resolve "AUTH-TOKEN Store the TOKEN to be used in all account requests"

See merge request ios/client-library-apple!121
parents 650573e8 fe94ed97
......@@ -249,6 +249,7 @@
84D5DA702126CE2900F753F8 /* QRCameraScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D5DA6F2126CE2900F753F8 /* QRCameraScannerViewController.swift */; };
A6680249A33738280B9AD733 /* Pods_PIALibrary_PIALibraryHost_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 566B62D4C21DD5A90EF1BAE6 /* Pods_PIALibrary_PIALibraryHost_iOS.framework */; };
DD58F4BB21AEB99C00D043F7 /* GlossToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58F4BA21AEB99C00D043F7 /* GlossToken.swift */; };
DD58F4BD21AEF76100D043F7 /* String+Components.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58F4BC21AEF76100D043F7 /* String+Components.swift */; };
DDC812482176166600CB290C /* SwiftGen+ScenesStoryboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC812472176166500CB290C /* SwiftGen+ScenesStoryboards.swift */; };
DDC8124A217617F900CB290C /* SwiftGen+SeguesStoryboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC81249217617F900CB290C /* SwiftGen+SeguesStoryboards.swift */; };
/* End PBXBuildFile section */
......@@ -493,6 +494,7 @@
BB47885FCCA9DDFF53237D2C /* Pods-PIALibrary-PIALibraryTests-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PIALibrary-PIALibraryTests-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-PIALibrary-PIALibraryTests-iOS/Pods-PIALibrary-PIALibraryTests-iOS.release.xcconfig"; sourceTree = "<group>"; };
D9FDC8980713EDCEDD51B936 /* Pods-PIALibrary-PIALibrary-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PIALibrary-PIALibrary-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PIALibrary-PIALibrary-iOS/Pods-PIALibrary-PIALibrary-iOS.debug.xcconfig"; sourceTree = "<group>"; };
DD58F4BA21AEB99C00D043F7 /* GlossToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlossToken.swift; sourceTree = "<group>"; };
DD58F4BC21AEF76100D043F7 /* String+Components.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Components.swift"; sourceTree = "<group>"; };
DDC812472176166500CB290C /* SwiftGen+ScenesStoryboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+ScenesStoryboards.swift"; sourceTree = "<group>"; };
DDC81249217617F900CB290C /* SwiftGen+SeguesStoryboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+SeguesStoryboards.swift"; sourceTree = "<group>"; };
E526D8161FFFEE65AC2DF844 /* Pods-PIALibrary-PIALibraryTests-iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PIALibrary-PIALibraryTests-iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PIALibrary-PIALibraryTests-iOS/Pods-PIALibrary-PIALibraryTests-iOS.debug.xcconfig"; sourceTree = "<group>"; };
......@@ -580,6 +582,7 @@
0E9D628B1FDEAC2D009A90CF /* module.modulemap */,
0E492C591FE5EA06007F23DF /* CMacros.h */,
0E492C5A1FE5EA06007F23DF /* CMacros.m */,
DD58F4BC21AEF76100D043F7 /* String+Components.swift */,
);
path = iOS;
sourceTree = "<group>";
......@@ -1556,6 +1559,7 @@
0EB8C0661F9CD38B005857E4 /* LoginViewController.swift in Sources */,
0EA8072C20A1C7A30033EC1A /* RedeemRequest.swift in Sources */,
0EC7A2A41F9D3D78006DDB91 /* RenewRequest.swift in Sources */,
DD58F4BD21AEF76100D043F7 /* String+Components.swift in Sources */,
0EE771091F9D21020029A77B /* Payment.swift in Sources */,
0E53A8531FE5D73F000C2A18 /* Client+Mock.swift in Sources */,
0E392D9D1FE31D630002160D /* MockVPNProvider.swift in Sources */,
......
......@@ -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)
}
......@@ -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,6 +107,19 @@ 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()
......@@ -102,7 +131,10 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
callback?(nil, error)
return
}
self.updateDatabaseWith(token,
andUsername: request.credentials.username)
self.webServices.info(token: token) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
callback?(nil, error)
......@@ -110,10 +142,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
//Save after confirm the login was successful.
self.accessedDatabase.plain.username = request.credentials.username
self.accessedDatabase.secure.setPassword(request.credentials.password, for: request.credentials.username)
self.accessedDatabase.plain.accountInfo = accountInfo
//TODO: PLEASE SAVE THE TOKEN HERE
let user = UserAccount(credentials: request.credentials, info: accountInfo)
Macros.postNotification(.PIAAccountDidLogin, [
......@@ -127,12 +156,31 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
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
}
//TODO: PLEASE GET THE TOKEN HERE
accountInfoWith(token, callback)
webServices.info(token: "TOKEN HERE") { (accountInfo, error) in
}
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
......@@ -140,7 +188,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
self.accessedDatabase.plain.accountInfo = accountInfo
Macros.postNotification(.PIAAccountDidRefresh, [
.accountInfo: accountInfo
])
])
callback?(accountInfo, nil)
}
}
......@@ -173,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)
}
......@@ -254,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)
......@@ -280,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)
}
}
}
......@@ -330,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()
}
......@@ -351,8 +419,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
Macros.postNotification(.PIAAccountDidRenew)
//TODO: PLEASE GET THE TOKEN HERE
self.webServices.info(token: "TOKEN HERE") { (accountInfo, error) in
self.webServices.info(token: token) { (accountInfo, error) in
guard let newAccountInfo = accountInfo else {
callback?(nil, nil)
return
......@@ -362,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)
}
......
......@@ -53,7 +53,7 @@ class PIAWebServices: WebServices, ConfigurationAccess {
429: .throttled
]
req(nil, .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
......@@ -233,17 +233,16 @@ class PIAWebServices: WebServices, ConfigurationAccess {
_ executor: RequestExecutor) {
var headers = SessionManager.defaultHTTPHeaders
//TODO: HERE ADD THE TOKEN
// headers["X-Device"] = "ios-\(Constants.iosVersion)/\(Constants.appVersion)/\(Constants.language)/\(Constants.region)"
if let credentials = credentials, let authHeader = Request.authorizationHeader(user: credentials.username, password: credentials.password) {
headers[authHeader.key] = authHeader.value
}
if useToken {
//TODO: PLEASE GET TOKEN HERE
headers["Authorization"] = "Token PLEASE THE TOKEN HERE"
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()
}
#if os(iOS)
/// :nodoc:
public func listPlanProducts(_ callback: (([Plan : InAppProduct]?, Error?) -> Void)?) {
......
......@@ -311,7 +311,7 @@ protocol WelcomeCompletionDelegate: class {
}
class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
// XXX: we want legit web services calls, yet allow the option to mock them
private var webServices: WebServices? {
guard let accountProvider = accessedProviders.accountProvider as? WebServicesConsumer else {
......@@ -326,8 +326,12 @@ class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
var isLoggedIn = false
var token: String?
var currentUser: UserAccount?
var publicUsername: String?
var currentPasswordReference: Data? {
return nil
}
......@@ -369,6 +373,10 @@ class EphemeralAccountProvider: AccountProvider, ProvidersAccess, InAppAccess {
func logout(_ callback: SuccessLibraryCallback?) {
fatalError("Not implemented")
}
func cleanDatabase() {
fatalError("Not implemented")
}
func listPlanProducts(_ callback: (([Plan : InAppProduct]?, Error?) -> Void)?) {
accessedProviders.accountProvider.listPlanProducts(callback)
......
......@@ -26,6 +26,8 @@ public class Keychain {
private let service: String?
private let accessGroup: String?
private let usernameKey = "USERNAME_KEY"
/**
Default initializer. Uses the default keychain associated with the main bundle identifier.
......@@ -226,3 +228,161 @@ public class Keychain {
}
}
}
extension Keychain {
// MARK: Username
/// :nodoc:
public func set(username: String) throws {
removeUsername()
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = usernameKey
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
query[kSecValueData as String] = username.data(using: .utf8)
let status = SecItemAdd(query as CFDictionary, nil)
guard (status == errSecSuccess) else {
throw KeychainError.add
}
}
/// :nodoc:
@discardableResult public func removeUsername() -> Bool {
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = usernameKey
let status = SecItemDelete(query as CFDictionary)
return (status == errSecSuccess)
}
/// :nodoc:
public func username() throws -> String {
var query = [String: Any]()
setScope(query: &query)
query[kSecClass as String] = kSecClassGenericPassword
query[kSecAttrAccount as String] = usernameKey
//query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
query[kSecMatchLimit as String] = kSecMatchLimitOne
query[kSecReturnData as String] = true
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard (status == errSecSuccess) else {
throw KeychainError.notFound
}
guard let data = result as? Data else {
throw KeychainError.notFound
}
guard let token = String(data: data, encoding: .utf8) else {
throw KeychainError.notFound
}
return token