diff --git a/AeroGearOAuth2.xcodeproj/project.pbxproj b/AeroGearOAuth2.xcodeproj/project.pbxproj index a223231..d420d54 100644 --- a/AeroGearOAuth2.xcodeproj/project.pbxproj +++ b/AeroGearOAuth2.xcodeproj/project.pbxproj @@ -8,10 +8,13 @@ /* Begin PBXBuildFile section */ 48070BF819B07AA000FCE5FA /* ConfigTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48070BF719B07AA000FCE5FA /* ConfigTest.swift */; }; + 4814EF4B19C1C0FA008BAC4D /* TrustedPersistantOAuth2Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4814EF4919C1C0FA008BAC4D /* TrustedPersistantOAuth2Session.swift */; }; + 4814EF4C19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4814EF4A19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift */; }; + 4814EF4E19C1C10C008BAC4D /* FacebookOAuth2Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4814EF4D19C1C10C008BAC4D /* FacebookOAuth2Module.swift */; }; 4833046819AF1635002F8DA9 /* AeroGearOAuth2.h in Headers */ = {isa = PBXBuildFile; fileRef = 4833046719AF1635002F8DA9 /* AeroGearOAuth2.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 4833047219AF1635002F8DA9 /* AeroGearOAuth2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4833047119AF1635002F8DA9 /* AeroGearOAuth2Tests.swift */; }; 483304A019AF327D002F8DA9 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4833049F19AF327D002F8DA9 /* AccountManager.swift */; }; 483304A519AF44DF002F8DA9 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 483304A419AF44DF002F8DA9 /* Config.swift */; }; + 48C94D8019C1F0970000ABBB /* OAuth2ModuleTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48C94D7F19C1F0970000ABBB /* OAuth2ModuleTest.swift */; }; 48CD52D719B0C1CB008D0694 /* OAuth2Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CD52D619B0C1CB008D0694 /* OAuth2Module.swift */; }; 48CD52DD19B0C22A008D0694 /* OAuth2Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CD52DC19B0C22A008D0694 /* OAuth2Session.swift */; }; 48CD52DF19B0CB5A008D0694 /* Oauth2SessionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CD52DE19B0CB5A008D0694 /* Oauth2SessionTest.swift */; }; @@ -50,16 +53,19 @@ /* Begin PBXFileReference section */ 48070BF719B07AA000FCE5FA /* ConfigTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigTest.swift; sourceTree = ""; }; + 4814EF4919C1C0FA008BAC4D /* TrustedPersistantOAuth2Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrustedPersistantOAuth2Session.swift; sourceTree = ""; }; + 4814EF4A19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UntrustedMemoryOAuth2Session.swift; sourceTree = ""; }; + 4814EF4D19C1C10C008BAC4D /* FacebookOAuth2Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FacebookOAuth2Module.swift; sourceTree = ""; }; 4833046219AF1635002F8DA9 /* AeroGearOAuth2.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AeroGearOAuth2.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4833046619AF1635002F8DA9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4833046719AF1635002F8DA9 /* AeroGearOAuth2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AeroGearOAuth2.h; sourceTree = ""; }; 4833046D19AF1635002F8DA9 /* AeroGearOAuth2Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AeroGearOAuth2Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4833047019AF1635002F8DA9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 4833047119AF1635002F8DA9 /* AeroGearOAuth2Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AeroGearOAuth2Tests.swift; sourceTree = ""; }; 4833047B19AF28AD002F8DA9 /* AGURLSessionStubs.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = AGURLSessionStubs.xcodeproj; path = "aerogear-ios-httpstub/AGURLSessionStubs.xcodeproj"; sourceTree = SOURCE_ROOT; }; 4833048419AF28D9002F8DA9 /* AeroGearHttp.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = AeroGearHttp.xcodeproj; path = "aerogear-ios-http/AeroGearHttp.xcodeproj"; sourceTree = SOURCE_ROOT; }; 4833049F19AF327D002F8DA9 /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; 483304A419AF44DF002F8DA9 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; + 48C94D7F19C1F0970000ABBB /* OAuth2ModuleTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2ModuleTest.swift; sourceTree = ""; }; 48CD52D619B0C1CB008D0694 /* OAuth2Module.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Module.swift; sourceTree = ""; }; 48CD52DC19B0C22A008D0694 /* OAuth2Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OAuth2Session.swift; sourceTree = ""; }; 48CD52DE19B0CB5A008D0694 /* Oauth2SessionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Oauth2SessionTest.swift; sourceTree = ""; }; @@ -110,6 +116,9 @@ 483304A419AF44DF002F8DA9 /* Config.swift */, 48CD52D619B0C1CB008D0694 /* OAuth2Module.swift */, 48CD52DC19B0C22A008D0694 /* OAuth2Session.swift */, + 4814EF4919C1C0FA008BAC4D /* TrustedPersistantOAuth2Session.swift */, + 4814EF4A19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift */, + 4814EF4D19C1C10C008BAC4D /* FacebookOAuth2Module.swift */, ); path = AeroGearOAuth2; sourceTree = ""; @@ -128,7 +137,7 @@ children = ( 48070BF719B07AA000FCE5FA /* ConfigTest.swift */, 48CD52DE19B0CB5A008D0694 /* Oauth2SessionTest.swift */, - 4833047119AF1635002F8DA9 /* AeroGearOAuth2Tests.swift */, + 48C94D7F19C1F0970000ABBB /* OAuth2ModuleTest.swift */, 4833046F19AF1635002F8DA9 /* Supporting Files */, ); path = AeroGearOAuth2Tests; @@ -311,7 +320,10 @@ 483304A019AF327D002F8DA9 /* AccountManager.swift in Sources */, 48CD52DD19B0C22A008D0694 /* OAuth2Session.swift in Sources */, 48CD52D719B0C1CB008D0694 /* OAuth2Module.swift in Sources */, + 4814EF4B19C1C0FA008BAC4D /* TrustedPersistantOAuth2Session.swift in Sources */, + 4814EF4E19C1C10C008BAC4D /* FacebookOAuth2Module.swift in Sources */, 483304A519AF44DF002F8DA9 /* Config.swift in Sources */, + 4814EF4C19C1C0FA008BAC4D /* UntrustedMemoryOAuth2Session.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -319,8 +331,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 48C94D8019C1F0970000ABBB /* OAuth2ModuleTest.swift in Sources */, 48CD52DF19B0CB5A008D0694 /* Oauth2SessionTest.swift in Sources */, - 4833047219AF1635002F8DA9 /* AeroGearOAuth2Tests.swift in Sources */, 48070BF819B07AA000FCE5FA /* ConfigTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/AeroGearOAuth2/AccountManager.swift b/AeroGearOAuth2/AccountManager.swift index 9296808..a135bd6 100644 --- a/AeroGearOAuth2/AccountManager.swift +++ b/AeroGearOAuth2/AccountManager.swift @@ -17,6 +17,88 @@ import Foundation +public class FacebookConfig: Config { + public init(clientId: String, clientSecret: String, scopes: [String], accountId: String? = nil) { + super.init(base: "", + authzEndpoint: "https://www.facebook.com/dialog/oauth", + redirectURL: "fb\(clientId)://authorize/", + accessTokenEndpoint: "https://graph.facebook.com/oauth/access_token", + clientId: clientId, + clientSecret: clientSecret, + revokeTokenEndpoint: "https://www.facebook.com/me/permissions", + scopes: scopes, + accountId: accountId) + } +} + +public class GoogleConfig: Config { + public init(clientId: String, scopes: [String], accountId: String? = nil) { + let bundleString = NSBundle.mainBundle().bundleIdentifier! + super.init(base: "https://accounts.google.com", + authzEndpoint: "o/oauth2/auth", + redirectURL: "\(bundleString):/oauth2Callback", + accessTokenEndpoint: "o/oauth2/token", + clientId: clientId, + revokeTokenEndpoint: "rest/revoke", + scopes: scopes, + accountId: accountId) + } +} + public class AccountManager { - public init() {} + + var modules: [String: OAuth2Module] + + init() { + self.modules = [String: OAuth2Module]() + } + + public class var sharedInstance: AccountManager { + struct Singleton { + static let instance = AccountManager() + } + return Singleton.instance + } + + public class func addAccount(config: Config, moduleClass: OAuth2Module.Type) -> OAuth2Module { + var myModule:OAuth2Module + myModule = moduleClass(config: config) + // TODO check accountId is unique in modules list + sharedInstance.modules[myModule.oauth2Session.accountId] = myModule + return myModule + } + + public class func removeAccount(name: String, config: Config, moduleClass: OAuth2Module.Type) -> OAuth2Module? { + return sharedInstance.modules.removeValueForKey(name) + } + + public class func getAccountByName(name: String) -> OAuth2Module? { + return sharedInstance.modules[name] + } + + public class func getAccountsByClienId(clientId: String) -> [OAuth2Module] { + let modules: [OAuth2Module] = [OAuth2Module](sharedInstance.modules.values) + return modules.filter {$0.config.clientId == clientId } + } + + public class func getAccountByConfig(config: Config) -> OAuth2Module? { + if config.accountId != nil { + return sharedInstance.modules[config.accountId!] + } else { + let modules = getAccountsByClienId(config.clientId) + if modules.count > 0 { + return modules[0] + } else { + return nil + } + } + } + + public class func addFacebookAccount(config: FacebookConfig) -> FacebookOAuth2Module { + return addAccount(config, moduleClass: FacebookOAuth2Module.self) as FacebookOAuth2Module + } + + public class func addGoogleAccount(config: GoogleConfig) -> OAuth2Module { + return addAccount(config, moduleClass: OAuth2Module.self) + } } diff --git a/AeroGearOAuth2/Config.swift b/AeroGearOAuth2/Config.swift index 5796049..18e1194 100644 --- a/AeroGearOAuth2/Config.swift +++ b/AeroGearOAuth2/Config.swift @@ -17,7 +17,7 @@ import Foundation -public struct Config { +public class Config { /** * Applies the baseURL to the configuration. */ diff --git a/AeroGearOAuth2/FacebookOAuth2Module.swift b/AeroGearOAuth2/FacebookOAuth2Module.swift index 6b2d253..2fd6576 100644 --- a/AeroGearOAuth2/FacebookOAuth2Module.swift +++ b/AeroGearOAuth2/FacebookOAuth2Module.swift @@ -16,11 +16,12 @@ */ import Foundation +import AeroGearHttp public class FacebookOAuth2Module: OAuth2Module { - override public init(config: Config, accountId: String) { - super.init(config: config, accountId: accountId) + required public init(config: Config, accountId: String, session: OAuth2Session) { + super.init(config: config, accountId: accountId, session: session) self.httpAuthz = Http(url: config.base, sessionConfig: NSURLSessionConfiguration.defaultSessionConfiguration(), requestSerializer: JsonRequestSerializer(url: NSURL(string: config.base), headers: [String: String]()), responseSerializer: StringResponseSerializer()) } @@ -52,7 +53,7 @@ public class FacebookOAuth2Module: OAuth2Module { } } //println("access:\(accessToken!) expires:\(expiredIn!)") - self.oauth2Session.saveAccessToken(accessToken: accessToken, refreshToken: nil, expiration: expiredIn) + self.oauth2Session.saveAccessToken(accessToken, refreshToken: nil, expiration: expiredIn) success(accessToken) } }, failure: {(error: NSError) -> () in diff --git a/AeroGearOAuth2/OAuth2Module.swift b/AeroGearOAuth2/OAuth2Module.swift index 3e34946..ebb6cae 100644 --- a/AeroGearOAuth2/OAuth2Module.swift +++ b/AeroGearOAuth2/OAuth2Module.swift @@ -37,35 +37,36 @@ public class OAuth2Module { let config: Config var httpAuthz: Http - public lazy var http: Http = { - var headerFields: [String: String]? - if (self.isAuthorized()) { - headerFields = self.authorizationFields() - return Http(url: nil, sessionConfig: nil, headers: headerFields != nil ? headerFields! : [String: String]()) + public var http: Http { + get { + var headerFields: [String: String]? + if (self.isAuthorized()) { + headerFields = self.authorizationFields() + return Http(url: nil, sessionConfig: nil, headers: headerFields != nil ? headerFields! : [String: String]()) + } + return Http() } - - return Http() - }() + } var oauth2Session: OAuth2Session var applicationLaunchNotificationObserver: NSObjectProtocol? var applicationDidBecomeActiveNotificationObserver: NSObjectProtocol? var state: AuthorizationState - // used without AccountManager, default accountId, not really usefull - public convenience init(config: Config) { + // Default accountId, default to TrustedPersistantOAuth2Session + public required convenience init(config: Config) { if (config.accountId != nil) { - self.init(config: config, accountId:config.accountId!) + self.init(config: config, accountId:config.accountId!, session: TrustedPersistantOAuth2Session(accountId: config.accountId!)) } else { - self.init(config: config, accountId:"ACCOUNT_FOR_CLIENTID_\(config.clientId)") + let accountId = "ACCOUNT_FOR_CLIENTID_\(config.clientId)" + self.init(config: config, accountId: accountId, session: TrustedPersistantOAuth2Session(accountId: accountId)) } } - // used by AccountManager with a user given accountId - public init(config: Config, accountId: String) { + public required init(config: Config, accountId: String, session: OAuth2Session) { self.config = config // TODO use timeout config paramter self.httpAuthz = Http(url: config.base, sessionConfig: NSURLSessionConfiguration.defaultSessionConfiguration()) - self.oauth2Session = OAuth2Session(accountId:accountId) + self.oauth2Session = session self.state = .AuthorizationStateUnknown } @@ -114,7 +115,7 @@ public class OAuth2Module { let expiration = unwrappedResponse["expires_in"] as NSNumber let exp: String = expiration.stringValue - self.oauth2Session.saveAccessToken(accessToken: accessToken, refreshToken: unwrappedRefreshToken, expiration: exp) + self.oauth2Session.saveAccessToken(accessToken, refreshToken: unwrappedRefreshToken, expiration: exp) success(unwrappedResponse["access_token"]); } }, failure: { (error: NSError) -> Void in @@ -139,7 +140,7 @@ public class OAuth2Module { let expiration = unwrappedResponse["expires_in"] as NSNumber let exp: String = expiration.stringValue - self.oauth2Session.saveAccessToken(accessToken: accessToken, refreshToken: refreshToken, expiration: exp) + self.oauth2Session.saveAccessToken(accessToken, refreshToken: refreshToken, expiration: exp) success(accessToken) } }, failure: {(error: NSError) -> () in diff --git a/AeroGearOAuth2/OAuth2Session.swift b/AeroGearOAuth2/OAuth2Session.swift index b9c70fb..2d47a6d 100644 --- a/AeroGearOAuth2/OAuth2Session.swift +++ b/AeroGearOAuth2/OAuth2Session.swift @@ -16,58 +16,36 @@ */ import Foundation -extension String { - var doubleValue: Double { - return (self as NSString).doubleValue - } -} - -public class OAuth2Session { - +public protocol OAuth2Session { /** * The account id. */ - public let accountId: String + var accountId: String {get} /** * The access token which expires. */ - var accessToken: String? + var accessToken: String? {get set} /** * The access token's expiration date. */ - var accessTokenExpirationDate: NSDate? + var accessTokenExpirationDate: NSDate? {get set} /** * The refresh tokens. This toke does not expire and should be used to renew access token when expired. */ - var refreshToken: String? + var refreshToken: String? {get set} /** * Check validity of accessToken. return true if still valid, false when expired. */ - func tokenIsNotExpired() -> Bool { - return self.accessTokenExpirationDate?.timeIntervalSinceDate(NSDate()) > 0 ; - } + func tokenIsNotExpired() -> Bool /** * Save in memory tokens information. Saving tokens allow you to refresh accesstoken transparently for the user without prompting * for grant access. */ - func saveAccessToken(accessToken: String? = nil, refreshToken: String? = nil, expiration: String? = nil) { - self.accessToken = accessToken - self.refreshToken = refreshToken - let now = NSDate() - if let inter = expiration?.doubleValue { - self.accessTokenExpirationDate = now.dateByAddingTimeInterval(inter) - } - } - - public init(accountId: String, accessToken: String? = nil, accessTokenExpirationDate: NSDate? = nil, refreshToken: String? = nil) { - self.accessToken = accessToken - self.accessTokenExpirationDate = accessTokenExpirationDate - self.refreshToken = refreshToken - self.accountId = accountId - } + func saveAccessToken() + func saveAccessToken(accessToken: String?, refreshToken: String?, expiration: String?) } diff --git a/AeroGearOAuth2/TrustedPersistantOAuth2Session.swift b/AeroGearOAuth2/TrustedPersistantOAuth2Session.swift new file mode 100644 index 0000000..86e8c34 --- /dev/null +++ b/AeroGearOAuth2/TrustedPersistantOAuth2Session.swift @@ -0,0 +1,243 @@ +/* +* JBoss, Home of Professional Open Source. +* Copyright Red Hat, Inc., and individual contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import Foundation + +import Security +// TODO AGIOS-256: Keychain wrapper implemented as part of AGIOS-103 +// should be moved in aerogear-ios-crypto +import UIKit + +//public enum ACL { +// case WhenUnlockedAndPasswordSet +// case WhenUnlocked +// // TODO AGIOS-258 add acl for background processing +//} + +public enum TokenType: String { + case AccessToken = "AccessToken" + case RefreshToken = "RefreshToken" +} + +public class KeychainWrap { + public var serviceIdentifier: String + + public init() { + if let bundle = NSBundle.mainBundle().bundleIdentifier { + self.serviceIdentifier = bundle + } else { + self.serviceIdentifier = "unkown" + } + } + + public func save(key: String, tokenType: TokenType, value: String) -> Bool { + var dataFromString: NSData? = value.dataUsingEncoding(NSUTF8StringEncoding) + if (dataFromString == nil) { + return false + } + + // Instantiate a new default keychain query + var keychainQuery = NSMutableDictionary() + keychainQuery[kSecClass] = kSecClassGenericPassword + keychainQuery[kSecAttrService] = self.serviceIdentifier + keychainQuery[kSecAttrAccount] = key + "_" + tokenType.toRaw() + keychainQuery[kSecAttrAccessible] = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + + // TODO AGIOS-259 configure Swift version to get touchID access control + // As of version beta7 kSecAccessControlUserPresence is not available in swift + /* + var error: Unmanaged? + var sac: Unmanaged? + sac = SecAccessControlCreateWithFlags(kCFAllocatorDefault, + kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, kSecAccessControlUserPresence, &error) + + let opaque = sac?.toOpaque() + if let op = opaque? { + let retrievedData = Unmanaged.fromOpaque(op).takeUnretainedValue() + keychainQuery[kSecAttrAccessControl] = retrievedData + keychainQuery[kSecUseNoAuthenticationUI] = false + } + */ + + // Search for the keychain items + let statusSearch: OSStatus = SecItemCopyMatching(keychainQuery, nil) + + // if found update + if (Int(statusSearch) == errSecSuccess) { + if (dataFromString != nil) { + let attributesToUpdate = NSMutableDictionary() + attributesToUpdate[kSecValueData] = dataFromString! + + var statusUpdate: OSStatus = SecItemUpdate(keychainQuery, attributesToUpdate) + if (Int(statusUpdate) != errSecSuccess) { + println("tokens not updated") + return false + } + } else { // revoked token or newly installed app, clear KC + return self.resetKeychain() + } + } else if(Int(statusSearch) == errSecItemNotFound) { // if new, add + keychainQuery[kSecValueData] = dataFromString! + var statusAdd: OSStatus = SecItemAdd(keychainQuery, nil) + if(Int(statusAdd) != errSecSuccess) { + println("tokens not saved") + return false + } + } else { // error case + return false + } + + return true + } + + public func read(userAccount: String, tokenType: TokenType) -> NSString? { + var keychainQuery = NSMutableDictionary() + keychainQuery[kSecClass] = kSecClassGenericPassword + keychainQuery[kSecAttrService] = self.serviceIdentifier + keychainQuery[kSecAttrAccount] = userAccount + "_" + tokenType.toRaw() + keychainQuery[kSecReturnData] = true + keychainQuery[kSecAttrAccessible] = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + + var dataTypeRef: Unmanaged? + + // Search for the keychain items + let status: OSStatus = SecItemCopyMatching(keychainQuery, &dataTypeRef) + + if (Int(status) == errSecItemNotFound) { + println("\(tokenType.toRaw()) not found") + return nil + } else if (Int(status) != errSecSuccess) { + println("Error attempting to retrieve \(tokenType.toRaw()) with error code \(status) ") + return nil + } + + let opaque = dataTypeRef?.toOpaque() + + var contentsOfKeychain: NSString? + + if let op = opaque? { + let retrievedData = Unmanaged.fromOpaque(op).takeUnretainedValue() + + // Convert the data retrieved from the keychain into a string + contentsOfKeychain = NSString(data: retrievedData, encoding: NSUTF8StringEncoding) + } else { + println("Nothing was retrieved from the keychain. Status code \(status)") + } + + return contentsOfKeychain + } + + // when uninstalling app you may wish to clear keyclain app info + public func resetKeychain() -> Bool { + return self.deleteAllKeysForSecClass(kSecClassGenericPassword) && + self.deleteAllKeysForSecClass(kSecClassInternetPassword) && + self.deleteAllKeysForSecClass(kSecClassCertificate) && + self.deleteAllKeysForSecClass(kSecClassKey) && + self.deleteAllKeysForSecClass(kSecClassIdentity) + } + + func deleteAllKeysForSecClass(secClass: CFTypeRef) -> Bool { + var keychainQuery = NSMutableDictionary() + keychainQuery[kSecClass] = secClass + + let result:OSStatus = SecItemDelete(keychainQuery) + if (Int(result) == errSecSuccess) { + return true + } else { + return false + } + } +} + + +// TODO When passcode is set in iPhone settings => ok +// if passcode is not set (not secure phone) session will fail to save tokens +// in keychain, implement a customizable fallback mechanism. Maybe in form of +// closure taken as init param. +// When passcode is not set to securely safe password we need to encrypt +// so we need user to be prompted to enter a password +public class TrustedPersistantOAuth2Session: OAuth2Session { + + /** + * The account id. + */ + public var accountId: String + + /** + * The access token's expiration date. + */ + public var accessTokenExpirationDate: NSDate? + + public var accessToken: String? { + get { + return self.keychain.read(self.accountId, tokenType: .AccessToken) + } + set(value) { + if let unwrappedValue = value { + let result = self.keychain.save(self.accountId, tokenType: .AccessToken, value: unwrappedValue) + } + } + } + + public var refreshToken: String? { + get { + return self.keychain.read(self.accountId, tokenType: .RefreshToken) + } + set(value) { + if let unwrappedValue = value { + self.keychain.save(self.accountId, tokenType: .RefreshToken, value: unwrappedValue) + } + } + } + + private let keychain: KeychainWrap + + /** + * Check validity of accessToken. return true if still valid, false when expired. + */ + public func tokenIsNotExpired() -> Bool { + return self.accessTokenExpirationDate?.timeIntervalSinceDate(NSDate()) > 0 ; + } + + /** + * Save in memory tokens information. Saving tokens allow you to refresh accesstoken transparently for the user without prompting + * for grant access. + */ + public func saveAccessToken(accessToken: String?, refreshToken: String?, expiration: String?) { + self.accessToken = accessToken + self.refreshToken = refreshToken + let now = NSDate() + if let inter = expiration?.doubleValue { + self.accessTokenExpirationDate = now.dateByAddingTimeInterval(inter) + } + } + public func saveAccessToken() { + self.accessToken = nil + self.refreshToken = nil + self.accessTokenExpirationDate = nil + } + + public init(accountId: String, accessToken: String? = nil, accessTokenExpirationDate: NSDate? = nil, refreshToken: String? = nil) { + self.accessTokenExpirationDate = accessTokenExpirationDate + self.accountId = accountId + self.keychain = KeychainWrap() + // TODO Shoot config to reset all keychain + choose ACL type: with or without touchID + // for now to clear keychain contain for your app uncomment line below + //self.keychain.resetKeychain() + self.accessToken = accessToken + self.refreshToken = refreshToken + } +} diff --git a/AeroGearOAuth2/UntrustedMemoryOAuth2Session.swift b/AeroGearOAuth2/UntrustedMemoryOAuth2Session.swift new file mode 100644 index 0000000..02710ba --- /dev/null +++ b/AeroGearOAuth2/UntrustedMemoryOAuth2Session.swift @@ -0,0 +1,78 @@ +/* +* JBoss, Home of Professional Open Source. +* Copyright Red Hat, Inc., and individual contributors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +import Foundation + +extension String { + var doubleValue: Double { + return (self as NSString).doubleValue + } +} + +public class UntrustedMemoryOAuth2Session: OAuth2Session { + + /** + * The account id. + */ + public var accountId: String + + /** + * The access token which expires. + */ + public var accessToken: String? + + /** + * The access token's expiration date. + */ + public var accessTokenExpirationDate: NSDate? + + /** + * The refresh tokens. This toke does not expire and should be used to renew access token when expired. + */ + public var refreshToken: String? + + /** + * Check validity of accessToken. return true if still valid, false when expired. + */ + public func tokenIsNotExpired() -> Bool { + return self.accessTokenExpirationDate?.timeIntervalSinceDate(NSDate()) > 0 ; + } + + /** + * Save in memory tokens information. Saving tokens allow you to refresh accesstoken transparently for the user without prompting + * for grant access. + */ + public func saveAccessToken(accessToken: String?, refreshToken: String?, expiration: String?) { + self.accessToken = accessToken + self.refreshToken = refreshToken + let now = NSDate() + if let inter = expiration?.doubleValue { + self.accessTokenExpirationDate = now.dateByAddingTimeInterval(inter) + } + } + public func saveAccessToken() { + self.accessToken = nil + self.refreshToken = nil + self.accessTokenExpirationDate = nil + } + + public init(accountId: String, accessToken: String? = nil, accessTokenExpirationDate: NSDate? = nil, refreshToken: String? = nil) { + self.accessToken = accessToken + self.accessTokenExpirationDate = accessTokenExpirationDate + self.refreshToken = refreshToken + self.accountId = accountId + } +} diff --git a/AeroGearOAuth2Tests/AeroGearOAuth2Tests.swift b/AeroGearOAuth2Tests/OAuth2ModuleTest.swift similarity index 52% rename from AeroGearOAuth2Tests/AeroGearOAuth2Tests.swift rename to AeroGearOAuth2Tests/OAuth2ModuleTest.swift index b2caa12..6bb4e79 100644 --- a/AeroGearOAuth2Tests/AeroGearOAuth2Tests.swift +++ b/AeroGearOAuth2Tests/OAuth2ModuleTest.swift @@ -17,29 +17,35 @@ import UIKit import XCTest +import AeroGearOAuth2 +import AGURLSessionStubs -class AeroGearOAuth2Tests: XCTestCase { +class OAuth2ModuleTests: XCTestCase { + + func http_200(request: NSURLRequest!, params:[String: String]?) -> StubResponse { + var data: NSData + if ((params) != nil) { + data = NSJSONSerialization.dataWithJSONObject(params!, options: nil, error: nil)! + } else { + data = NSData.data() + } + return StubResponse(data:data, statusCode: 200, headers: ["Content-Type" : "text/json"]) + } + + func http_200_response(request: NSURLRequest!) -> StubResponse { + return http_200(request, params: ["key1":"value1"]) + } override func setUp() { super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. super.tearDown() + StubsManager.removeAllStubs() } -// func testExample() { -// // This is an example of a functional test case. -// XCTAssert(true, "Pass") -// } -// -// func testPerformanceExample() { -// // This is an example of a performance test case. -// self.measureBlock() { -// // Put the code you want to measure the time of here. -// } -// } - -} + func testRequestAccessSucessful() { + //TODO AGIOS-mock + } +} \ No newline at end of file diff --git a/AeroGearOAuth2Tests/Oauth2SessionTest.swift b/AeroGearOAuth2Tests/Oauth2SessionTest.swift index b53cbc2..396966c 100644 --- a/AeroGearOAuth2Tests/Oauth2SessionTest.swift +++ b/AeroGearOAuth2Tests/Oauth2SessionTest.swift @@ -19,6 +19,7 @@ import UIKit import XCTest import AeroGearOAuth2 + class OAuth2SessionTests: XCTestCase { override func setUp() { @@ -30,14 +31,32 @@ class OAuth2SessionTests: XCTestCase { super.tearDown() } - func testInitFromDictionary() { - let session = OAuth2Session(accountId: "MY_FACEBOOK_ID") + func testInitUntrustedMemoryOAuth2SessionWithoutAccessToken() { + let session = UntrustedMemoryOAuth2Session(accountId: "MY_FACEBOOK_ID") + XCTAssert(session.accountId == "MY_FACEBOOK_ID", "wrong account id") + XCTAssert(session.accessToken == nil, "session should be without access token") + } + + func testInitUntrustedMemoryOAuth2SessionWithAccessToken() { + let session = UntrustedMemoryOAuth2Session(accountId: "MY_FACEBOOK_ID", accessToken: "ACCESS") + XCTAssert(session.accountId == "MY_FACEBOOK_ID", "wrong account id") + XCTAssert(session.accessToken == "ACCESS", "session should be with access token") + } + + func testSaveNilTokens() { + let session = UntrustedMemoryOAuth2Session(accountId: "MY_FACEBOOK_ID", accessToken: "ACCESS", refreshToken: "REFRESH") + session.saveAccessToken() XCTAssert(session.accountId == "MY_FACEBOOK_ID", "wrong account id") + XCTAssert(session.accessToken == nil, "session should be without access token") + XCTAssert(session.refreshToken == nil, "session should be without refresh token") } - func testInitFromDictionaryComplete() { - let session = OAuth2Session(accountId: "MY_FACEBOOK_ID", accessToken: "ACCESS") + func testSaveTokens() { + let session = UntrustedMemoryOAuth2Session(accountId: "MY_FACEBOOK_ID", accessToken: "ACCESS", refreshToken: "REFRESH") + session.saveAccessToken() XCTAssert(session.accountId == "MY_FACEBOOK_ID", "wrong account id") + XCTAssert(session.accessToken == nil, "session should be without access token") + XCTAssert(session.refreshToken == nil, "session should be without refresh token") } } diff --git a/aerogear-ios-http b/aerogear-ios-http index dcc0ade..3f7226f 160000 --- a/aerogear-ios-http +++ b/aerogear-ios-http @@ -1 +1 @@ -Subproject commit dcc0ade999b4b4bfc4e4ca4ddff875b701f74ee8 +Subproject commit 3f7226fec6efba59457ef477ebb058854796e038