Skip to content

Instantly share code, notes, and snippets.

@n0an
Forked from anishparajuli555/KeyChainWrapper.swift
Created January 30, 2017 10:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save n0an/a3efb280b80ded4a0542f41de24d8540 to your computer and use it in GitHub Desktop.
Save n0an/a3efb280b80ded4a0542f41de24d8540 to your computer and use it in GitHub Desktop.
This gist demonstrates how to save authentication token on Keychain
//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