-
-
Save n0an/a3efb280b80ded4a0542f41de24d8540 to your computer and use it in GitHub Desktop.
This gist demonstrates how to save authentication token on Keychain
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//SAMPLE CODE FROM APPLE | |
//https://developer.apple.com/library/content/samplecode/GenericKeychain/Listings/README_md.html | |
enum Key { | |
//... | |
//.... | |
enum Headers { | |
static let Authorization = "Authorization" | |
static let ContentType = "Content-Type" | |
} | |
} | |
struct KeychainConfiguration { | |
static let serviceName = "LoginService" | |
static let accessGroup: String? = nil //takes the bundle idenitifer | |
} | |
struct KeychainAccessWrapper { | |
enum KeychainError: Error { | |
case noData | |
case unexpectedData | |
case unexpectedItemData | |
case unhandledError(status: OSStatus) | |
} | |
let service: String | |
private(set) var key: String | |
let accessGroup: String? | |
// MARK: Intialization | |
init(service: String, key: String, accessGroup: String? = nil) { | |
self.service = service | |
self.key = key | |
self.accessGroup = accessGroup | |
} | |
// MARK: Keychain access | |
func getData() throws -> String { | |
/* | |
Build a query to find the item that matches the service, account and | |
access group. | |
*/ | |
var query = KeychainAccessWrapper.keychainQuery(withService: service, key: key, accessGroup: accessGroup) | |
query[kSecMatchLimit as String] = kSecMatchLimitOne | |
query[kSecReturnAttributes as String] = kCFBooleanTrue | |
query[kSecReturnData as String] = kCFBooleanTrue | |
// Try to fetch the existing keychain item that matches the query. | |
var queryResult: AnyObject? | |
let status = withUnsafeMutablePointer(to: &queryResult) { | |
SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) | |
} | |
// Check the return status and throw an error if appropriate. | |
guard status != errSecItemNotFound else { throw KeychainError.noData } | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
// Parse the password string from the query result. | |
guard let existingItem = queryResult as? [String : AnyObject], let accessTokenData = existingItem[kSecValueData as String] as? Data, let accessToken = String(data: accessTokenData, encoding: String.Encoding.utf8) | |
else { | |
throw KeychainError.unexpectedData | |
} | |
return accessToken | |
} | |
func saveData(_ value: String) throws { | |
// Encode the dataString into an Data object. | |
let encodedToken = value.data(using: String.Encoding.utf8)! | |
do { | |
// Check for an existing item in the keychain. | |
try _ = getData() | |
// Update the existing item with the new token. | |
var attributesToUpdate = [String : AnyObject]() | |
attributesToUpdate[kSecValueData as String] = encodedToken as AnyObject? | |
let query = KeychainAccessWrapper.keychainQuery(withService: service, key: key, accessGroup: accessGroup) | |
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
} | |
catch KeychainError.noData { | |
/* | |
No data was found in the keychain. Create a dictionary to save | |
as a new keychain item. | |
*/ | |
var newItem = KeychainAccessWrapper.keychainQuery(withService: service, key: key, accessGroup: accessGroup) | |
newItem[kSecValueData as String] = encodedToken as AnyObject? | |
// Add a the new data to the keychain. | |
let status = SecItemAdd(newItem as CFDictionary, nil) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr else { throw KeychainError.unhandledError(status: status) } | |
} | |
} | |
func deleteData() throws { | |
// Delete the existing item from the keychain. | |
let query = KeychainAccessWrapper.keychainQuery(withService: service, key: key, accessGroup: accessGroup) | |
let status = SecItemDelete(query as CFDictionary) | |
// Throw an error if an unexpected status was returned. | |
guard status == noErr || status == errSecItemNotFound else { throw KeychainError.unhandledError(status: status) } | |
} | |
// MARK: Convenience | |
private static func keychainQuery(withService service: String, key: String? = nil, accessGroup: String? = nil) -> [String : AnyObject] { | |
var query = [String : AnyObject]() | |
query[kSecClass as String] = kSecClassGenericPassword | |
query[kSecAttrService as String] = service as AnyObject? | |
if let key = key { | |
query[kSecAttrAccount as String] = key as AnyObject? | |
} | |
if let accessGroup = accessGroup { | |
query[kSecAttrAccessGroup as String] = accessGroup as AnyObject? | |
} | |
return query | |
} | |
} | |
//HOW TO USE: | |
//Clear keychain if app is deleted manually | |
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { | |
if !isUserLoggedIn{ | |
clearKeyChainValues() | |
} | |
func clearKeyChainValues(){ | |
let tokenItem = KeychainAccessWrapper(service: KeychainConfiguration.serviceName, key: Key.Headers.Authorization, accessGroup: KeychainConfiguration.accessGroup) | |
do { | |
// Save the token for the new item. | |
try tokenItem.deleteData() | |
}catch { | |
fatalError("Error updating keychain - \(error)") | |
} | |
} | |
} | |
//ON LOGOUT DO NOT FORGET TO CLEAR KEYCHAIN | |
func clearCredentials() { | |
//.... | |
//.... | |
self.clearKeyChainValues() | |
} | |
//TO SAVE AUTHENTICATION TOKEN | |
class RequestParser{ | |
//.... | |
//.... | |
private func parseJson(request:DataRequest,responseCallBack:@escaping (Any)->Void){ | |
request.responseJSON { json in | |
if json.response?.statusCode != nil{ | |
self.processHeaders(request: request, response: json) | |
} | |
else{ | |
print(error.localizedDescription) | |
} | |
} | |
} | |
} | |
func processHeaders(request:DataRequest,response:DataResponse<Any>){ | |
if request.request?.url?.absoluteString == APPURL.Login || request.request?.url?.absoluteString == APPURL.FacebookLogin { | |
if response.response?.statusCode != 401{ | |
let headers = response.response?.allHeaderFields | |
saveRefreshToken(headers: headers! as! [String : Any]) | |
} | |
} | |
} | |
func saveRefreshToken(headers:[String:Any]) { | |
let tokenStr = headers[Key.Headers.Authorization]! as! String | |
let tokenItem = KeychainAccessWrapper(service: KeychainConfiguration.serviceName, key: Key.Headers.Authorization, accessGroup: KeychainConfiguration.accessGroup) | |
do { | |
// Save the token for the new item. | |
print("token is \(tokenStr)") | |
try tokenItem.saveData(tokenStr) | |
}catch { | |
fatalError("Error updating keychain - \(error)") | |
} | |
} | |
} | |
//UPDATE TOKEN IN KEYCHAIN AFTER REFRESHING | |
class RequestRetrierHandler: RequestRetrier { | |
// MARK: - RequestRetrier | |
func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { | |
lock.lock() ; defer { lock.unlock() } | |
if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 { | |
requestsToRetry.append(completion) | |
if !isRefreshing { | |
refreshTokens { [weak self] succeeded in | |
guard let strongSelf = self else { return } | |
strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() } | |
strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) } | |
strongSelf.requestsToRetry.removeAll() | |
} | |
} | |
} else { | |
//cannot refresh token | |
completion(false, 0.0) | |
} | |
} | |
// MARK: - Private - Refresh Tokens | |
private func refreshTokens(completion: @escaping RefreshCompletion) { | |
guard !isRefreshing else { return } | |
isRefreshing = true | |
sessionManager.request(APPURL.RefreshToken, method: .get, parameters: nil, encoding: JSONEncoding.default) | |
.responseJSON { response in | |
switch(response.result) { | |
case .success(let data): | |
let responseJson = Mapper<BaseResponse<AuthenticationToken>>().map(JSONObject:data) | |
if responseJson!.status!.success! { | |
print("token refreshed successfully") | |
let headers = response.response?.allHeaderFields | |
let responseHeader = headers! as! [String : Any] | |
saveRefreshToken(headers:responseHeader ) | |
completion(true) | |
}else{ | |
PrinterHelper.messagePrinter("Cannot refresh token") | |
} | |
case .failure(_): | |
PrinterHelper.messagePrinter("Cannot refresh token") | |
completion(false) | |
} | |
self.isRefreshing = false | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment