Authentication with iOS/Swift - ios

I have a very simple question, I have a node.js/express server that will handle backend authentication part, it is using token not cookies, the server part is working correctly, whenever someone register/login it would return with a JSON web token.
For example:
{
"token" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdW"
}
I'm using Alamofire to handle the HTTP request from iOS itself. The real question lies is how do I persist the token in the iOS/Swift itself?
What is the simplest way to do this?

You should use the iOS Keychain to save sensitive information.
You should not use NSUserDefaults to store an authentication token or any other potentially sensitive information. It's unencrypted and easily accessible on a rooted device.
How would you like someone getting your authentication token and making requests to your private API at will (e.g. on the command line using curl)?
I've used the KeychainAccess CocoaPod and its usage is simple:
static let keychain = Keychain(service: "com.example.myapp")
keychain["secret_code"] = secretCode // Save something
let secretCode = keychain["secret_code"] // Retrieve something

The simplest way is to store it in NSUserDefaults like this:
Writing:
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setObject("Your Variable Value", forKey: "token")
Reading:
let defaults = NSUserDefaults.standardUserDefaults()
if let token = defaults.stringForKey("token") {
print(name)
}

The simplest way may be NSUserDefaults, but the most secure way would be to store the token in the iOS Keychain. Note there are several wrapper libraries (for example) available to make working with the keychain easier in Swift. The API can be a bit intimidating at times.

Related

How to create a "challenge" for my Cloud Functions server

I'm trying use Apple's new DeviceCheck API to verify that network calls in my app are actually coming from an uncompromised version of my app.
Documentation
After successfully verifying a key’s attestation, your server can
require the app to assert its legitimacy for any or all future server
requests. The app does this by signing the request. In the app, first
obtain a unique, one-time challenge from the server. You use a
challenge here, like for attestation, to avoid replay attacks. Then
combine the challenge with the server request to create a hash:
let challenge = <# A string from your server #>
let request = [ "action": "getGameLevel",
"levelId": "1234",
"challenge": challenge ]
guard let clientData = try? JSONEncoder().encode(request) else { return }
let clientDataHash = Data(SHA256.hash(data: clientData))
Use this hash and the key identifier that you generated earlier to
create an assertion object by calling the
generateAssertion(_:clientDataHash:completionHandler:) method:
service.generateAssertion(keyId, clientDataHash: clientDataHash) { assertion, error in
guard error == nil else { /* Handle the error. */ }
// Send the assertion and request to your server.
}
I'm trying to add this assertion functionality to my Swift function, which is a helper function that calls a Firebase Cloud Function.
I want the assertion object to be passed as data to the Cloud Function, to verify that the Cloud Function is being called from an uncompromised version of my app:
func callFunction(name: String, data: [String:Any?], completion: #escaping (HTTPSCallableResult?, Error?)->()){
var functions = Functions.functions()
functions.httpsCallable(name).call(data){ (result, error) in
completion(result, error)
}
}
(Example of callFunction() being used below):
let data: [String:Any?] = [
"gameId": self.game?.id,
"answer": answer,
"answeredAt": Date().millisecondsSince1970
]
callFunction(name: "answerQuestion", data: data){ res, err in
print("Submitted answer: \(res.debugDescription) | Error: \(err)")
if let err = err {
self.game?.question?.state = .initial
}
}
To generate the assertion object to send to my server (cloud function), it requires me to generate a challenge as stated above. However I'm not sure how to generate this challenge.
Apple says it should be "A string from your server". But I'm not sure what the string should be. Is it meant to be a dynamic string based on the user's UID? A Base64-encoded string of the user ID and a static secret string? And when I try to retrieve this string from the server, the user will just be able to read it as they can see incoming network JSON (I presume I would retrieve the string with a Cloud Function call) - so it seems pointless as it's not a secret string anymore?
Any idea how I can make the challenge work securely?
The point of the challenge is to avoid replay attacks, so it can be any randomised string. A UUID would be fine. It doesn't need to be a secret.
The challenge string is combined with the transaction data and a hash is generated. You send the hash to and you send that to generateAssertion and receive the assertion object. You then send this to your server along with the request data.
Now your server can combine the received request data with the challenge (which it knows, since it sent it to the client initially), generate the same hash and validate the attestation.
The server-side attestation article provides detail on the challenge data:
Provide a Challenge
Every time your app needs to communicate attestation data to your server, the app first asks the server for a unique, one-time challenge. App Attest integrates this challenge into the objects that it provides, and that your app sends back to your server for validation. This makes it harder for an attacker to implement a replay attack.
When asked for a challenge, provide your app with a randomized data value, and remember the value for use when verifying the corresponding attestation or assertion objects sent by the client. How you use the challenge data depends on the kind of object that you need to validate.

Apple, iOS 13, CryptoKit, Secure Enclave - Enforce biometric authentication ahead of private key usage

I am working with Apple's new cryptokit library and am trying to get a basic use case to work.
Goal: I would like to create a private key in the secure enclave via the cryptokit, store the key's reference in the iOS device's key chain and ensure that the key can only be reinitialized in the secure enclave after the user has authenticated himself via some biometric authentication method.
Current state: So far, I am able to initialize a private key in the secure enclave via the following code:
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init();
Furthermore, I can store and retrieve the corresponding private key's reference from the key chain. After retrieving the reference, I can reinitialize the private key in the secure enclave with the following code:
var privateKeyReference = getPrivateKeyReferenceFromKeyChain();
var privateKey = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
dataRepresentation: privateKeyReference
);
So far everything works as expected and all cryptographic operations with the private key succeed.
Now, as far as I understand the spare documentation by Apple, I should be able to modify the first initialization of the private key to something as follows.
let authContext = LAContext();
let accessCtrl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccesibleWhenUnlockedThisDeviceOnly,
[.privateKeyUsage, .userPresence, .biometryCurrentSet],
nil
);
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
accessControl: accessCtrl!,
authenticationContext: authContext
);
Thereby, ensuring that the private key can only be reinitialized, when the user authenticates himself via some biometric authentication method. The initial initialization stil works without any errors.
Problem: However, adding the previous code, I do not get any biometric authentication prompt and can not use the private key at all after reinitialization. The following error is logged whenever I try to execute some cryptographic operation with the reinitialized key, here for example some signing:
Error Domain=CryptoTokenKit Code=-9 "setoken: unable to sign digest" UserInfo={NSLocalizedDescription=setoken: unable to sign digest})
As far as I could guess from here, I think that Code=-9 refers to the "authenticationNeeded" error.
Question: Can someone point me to some documentation or tutorial how to achieve what I am looking for or explain to me what I am missing?
Thanks!
Cross-Post: https://forums.developer.apple.com/message/387746
After a couple of days of patience I was able to obtain an answer from the Apple development support. They suggested the following method which only differs a little bit from my approach:
var error: Unmanaged<CFError>? = nil;
let accessCtrl = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccesibleAfterFirstUnlockThisDeviceOnly,
[.privateKeyUsage, .biometryCurrentSet],
&error
);
var privateKeyReference = try CryptoKit.SecureEnclave.P256.KeyAgreement.PrivateKey.init(
accessControl: accessCtrl
);
Additionally, in the meantime iOS version 13.1.3 was released and, after upgrading my device, the above code started working. So either there is a subtle difference between mine and Apple's code or it is related to the update. Nevertheless, it is working now.

GetStream iOS native - How to update Client token after first initialization

I'm looking for an API to update the Client.Config.token value for the GetStream Library on iOS. Seems like this token is the only way for the library to parse the user_id from the JWT, however it is only parsed at init time of the singleton, Client.shared.
What happens if we need to update the token if the user has logged out of one account and into a different account?
Thank you for your question. For now, the shared Client doesn't support Token update, but you can use a not shared instance of the Client. So, when another user logged in you can create a new Client instance with another token instead of the existing one.
To finish an instance client setup you need to create the current user like this:
if let currentUserId = client.currentUserId {
client.create(user: User(id: currentUserId)) { [weak client] result in
client?.currentUser = try? result.get()
}
}
We'll add a token update for the shared Client in future releases.

Cannot set PubNub auth key Swift iOS

I have a webapp and the iOS app built in Swift. The thing is I don't know Swift and I'm trying to modify the iOS app in order to add the authorization key to the PubNub client before subscribing/publishing to channels.
Link to PubNub docs
PRE:
Access Manager is enabled
my_auth_key is hardcoded and already enabled form the server for the corresponding channel I want to subscribe.
Here is the code
What's the correct way to set the auth key?
Thanks in advance
Polak, mentioned docs code snippet refer to previously created instance to get it's pre-configured PNConfiguration instance and change required field. This practice can be used in case if you need change something at run-time.
If you have data for authKey at the moment of client initialization, you can set it:
var pubnubClient: PubNub = {
let configuration = PNConfiguration(publishKey: UCConstants.PubNub.publishKey, subscribeKey: UCConstants.PubNub.subscribeKey)
configuration.authKey = "my_auth_key"
return PubNub.clientWithConfiguration(configuration)
}()
Also, I've tried exact your code and don't have any issues with setting of authKey, because I can see it with subscribe request.
If you still will have troubles with PAM and auth key usage, please contact us on support#pubnub.com
Looks like you can:
let configuration = PNConfiguration(
authKey: "super_secret",
publishKey: UCConstants.PubNub.publishKey,
subscribeKey: UCConstants.PubNub.subscribeKey
)
Based on the Obj-C code: https://github.com/pubnub/objective-c/blob/8c1f0876b5b34176f33681d22844e8d763019635/PubNub/Data/PNConfiguration.m#L174-L181

Is is normal to end up with a LOT of changes on CKFetchRecordChangesOperation?

I use CloudKit in my app, which seems to be working well. However when I initialise the app on a new device, using
CKFetchRecordChangesOperation *fetchRecordChangesOperation = [[CKFetchRecordChangesOperation alloc] initWithRecordZoneID:zoneID previousServerChangeToken:NIL];
I get a LOT of changes, as all previous deletions and changes appear to be synched across.
Is there a better way? for example to just download a full set of current data and set the serverChangeToken to the current value.
When looking at the documentation of the CKServerChangeToken it looks like you can only get it when using the CKFetchRecordChangesOperation. See: https://developer.apple.com/library/prerelease/ios/documentation/CloudKit/Reference/CKServerChangeToken_class/index.html
So now how could you get a hold on that token:
Usually you will do something like this: As you can see in the CKFetchRecordChangesOperation you can initialize it with a previousServerChangeToken. This token works like a timestamp. When the operation completes, you get this token back in the fetchRecordChangesCompletionBlock. You have to save that token in for instance the user defaults. Then the next time you start a CKFetchRecordChangesOperation, you can use that token to start reading the changes since the last time you called it.
Actually saving the token can be a little tricky. I can suggest adding a property like this:
private var previousChangeToken: CKServerChangeToken? {
get {
let encodedObjectData = NSUserDefaults.standardUserDefaults().objectForKey("\(container.containerIdentifier)_lastFetchNotificationId") as? NSData
var decodedData: CKServerChangeToken? = nil
if encodedObjectData != nil {
decodedData = NSKeyedUnarchiver.unarchiveObjectWithData(encodedObjectData!) as? CKServerChangeToken
}
return decodedData
}
set(newToken) {
if newToken != nil {
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(newToken!), forKey:"\(container.containerIdentifier)_lastFetchNotificationId")
}
}
}
In your case you want a new application to start with a change token that could only have been created by an other running app. So besides saving the change token to the NSUserDefaults, you should also save it in CloudKit in the public Database in one specific settings recordType record. A newly installed app that does not have a token in it's NSUserDefaults can then read the token from your CloudKit settings record.

Resources