Authenticators
The iOS SDK offers multiple methods for user authentication:
-
PIN: is a default type of user authentication where the user has to provide a set of digits in order to authenticate. This method of authenticator has to be registered during user registration and can't be deregistered even when other authenticators are registered. When the user can't (or doesn't want to) authenticate with another method (fingerprint), the iOS SDK allows them to use their PIN instead.
-
BIOMETRIC is native Apple fingerprint or facial recognition authentication. In order to use it on a device, all requirements must be met.
- CUSTOM refers to all third-party custom authenticators that are added to the app by the app developer. In order to use such authenticators, an app developer needs to enable the custom authenticator functionality in the IDAAS-core, provide proper scripts in the extension engine, and interface implementations in the iOS SDK.
The class representing an authenticator is called Authenticator
. Objects of that class are DTOs, when returned by the iOS SDK, their property values (including registered and preferred) will not be changed or updated. Authenticator
has the following properties:
- identifier (
String
) is a unique authenticator identifier defined by its vendor. - name (
String
) is a human-readable authenticator name defined in the IDAAS-core admin panel. - type (
AuthenticatorType
) is the type of the authenticator: PIN authenticator, biometric authenticator, or custom authenticator. - isRegistered (
Bool
) indicates if this authenticator is registered for the user for which it was fetched. - isPreferred (
Bool
) indicates if this authenticator is set as preferred for the user for which it was fetched.
The UserClient
has three AuthenticatorsType
:
nonRegistered
returns a set of authenticators that are supported both client- and server-side, and are not yet registered.registered
returns a set of registered authenticators.all
returns a set of both registered and not registered authenticators.
Example: nonRegistered
let profile = SharedUserClient.instance.authenticatedUserProfile
userClient.authenticators(.nonRegistered, for: profile)
ONGUserProfile *userProfile = [[ONGUserClient sharedInstance] authenticatedUserProfile];
NSSet<ONGAuthenticator *> *authenticators = [[ONGUserClient shadedInstance] nonRegisteredAuthenticatorsForUser: userProfile];
Example: registered
let profile = SharedUserClient.instance.authenticatedUserProfile
userClient.authenticators(.registered, for: profile)
ONGUserProfile *userProfile = [[ONGUserClient sharedInstance] authenticatedUserProfile];
NSSet<ONGAuthenticator *> *authenticators = [[ONGUserClient shadedInstance] registeredAuthenticatorsForUser: userProfile];
Example: all
let profile = SharedUserClient.instance.authenticatedUserProfile
userClient.authenticators(.all, for: profile)
ONGUserProfile *userProfile = [[ONGUserClient sharedInstance] authenticatedUserProfile];
NSSet<ONGAuthenticator *> *authenticators = [[ONGUserClient shadedInstance] allAuthenticatorsForUser: userProfile];
Authenticator registration
Registration is done by calling the registerAuthenticator:delegate:
method of the UserClient
, which requires two arguments:
- authenticator (
Authenticator
) is the authenticator to be registered. - delegate (
AuthenticatorRegistrationDelegate
) is the authentication delegate.
The object passed as the delegate must conform to the AuthenticatorRegistrationDelegate
protocol. The iOS SDK will call the following methods on it:
userClient(_:didStartRegistering:for:)
is the method called when authenticator registration is started.userClient(_:didRegister:for:info:)
is the method called when authenticator registration is completed with success.userClient(_:didFailToRegister:for:error:)
is the method called when authenticator registration failed with an error.userClient(_:didReceivePinChallenge:for:)
is the method called when authenticator registration requires a PIN to continue.userClient(_:didReceiveCustomAuthFinishRegistrationChallenge:for:)
is the method called during registration of a custom authenticator.
Registering an authenticator might require the user to authenticate (in case of biometric). After successful authenticator registration, you can set an authenticator as preferred, which means it will by used for authentication by default. The user will also be able to receive push notifications that can be confirmed using that authenticator.
Example: register a fingerprint authenticator
Let the calling class also implement the required delegate.
let profile = SharedUserClient.instance.authenticatedUserProfile
let authenticators = userClient.authenticators(.nonRegistered, for: profile)
let yourAuthenticator = authenticators.filter { $0.type == .biometric }.first
SharedUserClient.instance.register(authenticator: yourAuthenticator, delegate: self)
ONGUserProfile *userProfile = [[ONGUserClient sharedInstance] authenticatedUserProfile];
NSSet<ONGAuthenticator *> *authenticators = [[ONGUserClient shadedInstance] nonRegisteredAuthenticatorsForUser:userProfile];
ONGAuthenticator *yourAuthenticator = [authenticators filteredSetUsingPredicate:yourPredicate].anyObject;
[[ONGUserClient sharedInstance] registerAuthenticator:yourAuthenticator delegate:self];
After the authenticator is registered it can be set as preferred. Without doing so, the iOS SDK will still use PIN authentication by default. To set an authenticator as preferred use the setPreferredAuthenticator
method of the UserClient.
Example: register a preferred authenticator
let profile = SharedUserClient.instance.authenticatedUserProfile
let authenticators = userClient.authenticators(.registered, for: profile)
let yourAuthenticator = authenticators.filter.first
SharedUserClient.instance.setPrefered(authenticator: yourAuthenticator)
ONGUserProfile *userProfile = [[ONGUserClient sharedInstance] authenticatedUserProfile];
NSSet<ONGAuthenticator *> *authenticators = [ONGUserClient shadedInstance] registeredAuthenticatorsForUser:userProfile];
ONGAuthenticator *yourAuthenticator = [authenticators filteredSetUsingPredicate:yourPredicate].anyObject;
[ONGUserClient sharedInstance].preferredAuthenticator = yourAuthenticator;
Authenticator deregistration
Authenticators can be deregistered for the currently authenticated user by using deregisterAuthenticator:delegate:
, which takes two parameters:
- authenticator (
Authenticator
) is the authenticator to be deregistered. - delegate (
AuthenticatorDeregistrationDelegate
) is the deregistration delegate.
Objects passed as the delegate should conform to AuthenticatorDeregistrationDelegate
. The following methods are called by the iOS SDK:
userClient(_:didStartDeregistering:for:)
is the method called when deregistration is started.userClient(_:didReceiveCustomAuthDeregistrationChallenge:)
is the method called only during custom authenticator deregistration.userClient(_:didDeregister:for:)
is the method called when authenticator deregistration succeeded.userClient(_:didFailToDeregister:for:error:)
is the method called when authenticator deregistration failed.
Example: deregister an authenticator
let profile = SharedUserClient.instance.authenticatedUserProfile
let authenticators = userClient.authenticators(.registered, for: profile)
let yourAuthenticator = authenticators.filter.first
SharedUserClient.instance.deregister(authenticator: yourAuthenticator, delegate: self)
ONGUserProfile *userProfile = [[ONGUserClient sharedInstance] authenticatedUserProfile];
NSSet<ONGAuthenticator *> *authenticators = [[ONGUserClient sharedInstance] registeredAuthenticatorsForUser:userProfile];
ONGAuthenticator *yourAuthenticator = [authenticators filteredSetUsingPredicate:yourPredicate].anyObject;
[[ONGUserClient sharedInstance] deregisterAuthenticator:yourAuthenticator delegate:self];
PIN authenticator
The PIN authenticator is the default authenticator in the iOS SDK. It is always available for a registered user. When authenticating with other authenticators, there is always an option to fallback to PIN. The PIN authenticator therefore cannot be deregistered except by deregistering the user.
PIN handling
All data-sensitive operations require the user to be authenticated first to ensure that the data is secured, even on a stolen phone. The PIN plays a crucial role in this flow:
- The iOS SDK requires a PIN for any new user as the default authentication method.
- The iOS SDK or user may fall back to PIN authentication seamlessly if any other authentication mechanism fails (such as TouchID)
The PIN is not stored in memory or on the device. This approach secures the user's PIN from being intercepted. However it has some disadvantages from the developer's perspective. Since the iOS SDK doesn't store the PIN, there is also no way to get its required length during PIN entrance for user authentication. The only source of truth in this case is the Admin Panel of the IDAAS-core.
There are two ways to deal with this: store the PIN length or hardcode it. While hardcoding is easy to achieve, it has a downside, such as an inability to dynamically change the PIN length in the IDAAS-core configuration without updating your application. Storing the PIN length makes your app more flexible while it requires few changes to support it.
For example, in an app that uses the iOS SDK, a developer has implemented a PinInputViewController
that uses a pinLength
property to build the UI and validate the PIN length locally.
Example: handle user PIN authentication
func userClient(_: UserClient, didReceivePinChallenge challenge: PinChallenge) {
let inputViewController = PinInputViewController()
inputViewController.pinLength = ?
inputViewController.completion = { pin in
if cancelled {
challenge.sender.cancel(challenge)
} else {
challenge.sender.respond(with: pin, to: challenge)
}
}
}
// id<ONGAuthenticationDelegate>
- (void)userClient:(ONGUserClient *)userClient didReceivePinChallenge:(ONGPinChallenge *)challenge
{
PinInputViewController *inputViewController = [PinInputViewController new];
inputViewController.pinLength = ?
inputViewController.completion = ^(NSString *pin, BOOL cancelled) {
if (cancelled) {
[challenge.sender cancelChallenge:challenge];
} else {
[challenge.sender respondWithPin:pin challenge:challenge];
}
};
}
PIN length
To retrieve the PIN length, we can design a class that acts as a dictionary by persisting the length for a specific user. We need to store the PIN length per user profile since the PIN policy could change at any moment in time and therefore different profiles could use different PIN lengths. Also, you should persist the length so that it does not get wiped after killing the app, since you will need it the next time the app starts and authentication is triggered.
The following is the interface description of the dictionary:
Example: retrieve the PIN length
@interface PinLengthStore : NSObject
- (NSUInteger)lengthForUser:(ONGUserProfile *)userProfile;
- (void)setLength:(NSUInteger)length forUser:(ONGUserProfile *)userProfile;
- (void)removeLengthForUser:(ONGUserProfile *)userProfile;
@end
The following is the updated userClient:challenge:
to retrieve the stored PIN length.
func userClient(_ userClient: UserClient, didReceivePinChallenge challenge: PinChallenge) {
let inputViewController = PinInputViewController()
inputViewController.pinLength = ?
}
- (void)userClient:(ONGUserClient *)userClient didReceivePinChallenge:(ONGPinChallenge *)challenge
{
PinInputViewController *inputViewController = [PinInputViewController new];
inputViewController.pinLength = [self.pinLengthStore lengthForUser:challenge.user];
// rest of the code
}
Example: write the PIN length value at registration
For storing or updating the pinLength
value, there are only two places: User registration and change PIN:
// Implementation of id<ONGRegistrationDelegate>
var userPinLength: UInt
func userClient(_ userClient: UserClient, didReceiveCreatePinChallenge challenge: CreatePinChallenge) {
self.userPinLength = challenge.pinLength
}
func userClient(_ userClient: UserClient, didRegisterUser profile: UserProfile, identityProvider: IdentityProvider, info: CustomInfo?) {
// save challenge.pinLength
}
// Implementation of id<ONGRegistrationDelegate>
@property (nonatomic) NSUInteger userPinLength;
- (void)userClient:(ONGUserClient *)userClient didReceivePinRegistrationChallenge:(ONGCreatePinChallenge *)challenge
{
self.userPinLength = challenge.pinLength;
// further challenge handling
}
- (void)userClient:(ONGUserClient *)userClient didRegisterUser:(ONGUserProfile *)userProfile identityProvider:(ONGIdentityProvider *)identityProvider info:(ONGCustomInfo *_Nullable)info
{
[self.pinLengthStore setLength:self.userPinLength forUser:userProfile];
}
Example: write the PIN length value at PIN change
The new PIN length gets stored only when an operation has been completed successfully. The reason for doing that is to prevent storing the length for non-registered users or to change the length during PIN change, when the PIN is not actually changed (and thus making it potentially invalid).
// Implementation of id<ONGChangePinDelegate>
var userPinLength: UInt
func userClient(_ userClient: UserClient, didReceiveCreatePinChallenge challenge: CreatePinChallenge) {
self.userPinLength = challenge.pinLength
}
func userClient(_ userClient: UserClient, didChangePinForUser profile: UserProfile) {
// save challenge.pinLength
}
// Implementation of id<ONGChangePinDelegate>
var userPinLength: UInt
func userClient(_ userClient: UserClient, didReceiveCreatePinChallenge challenge: CreatePinChallenge) {
self.userPinLength = challenge.pinLength
}
func userClient(_ userClient: UserClient, didChangePinForUser profile: UserProfile) {
// save challenge.pinLength
}
Example: remove stored value in every delegate method
In this example, the final step is to remove the stored value in every delegate method where the user might get deregistered:
// AuthenticationDelegate
func userClient(_ userClient: UserClient,
didFailToAuthenticateUser profile: UserProfile,
authenticator: Authenticator,
error: Error) {
if error.domain.elementsEqual("ONGGenericErrorDomain") &&
error.code == ONGGenericError.userDeregistered {
// removePinLengthForUser
}
}
// id<ONGAuthenticationDelegate>
- (void)userClient:(ONGUserClient *)userClient didFailToAuthenticateUser:(ONGUserProfile *)userProfile authenticator:(ONGAuthenticator *)authenticator error:(NSError *)error
{
if ([error.domain isEqualToString:ONGGenericErrorDomain] && error.code == ONGGenericErrorUserDeregistered) {
[self.pinLengthStore removePinLengthForUser:userProfile];
// further logout cleanup
}
}
Biometric authenticator
The biometric authenticator allows users to perform biometric authentication by the use of TouchID or FaceID. The TouchID and FaceID functionalities are entirely handled by Apple's internal APIs and dedicated hardware. The TouchID and FaceID scanners and secure enclave are encapsulated within the A7 chip, which assures that the mathematical representation of scanned biometrics are not intercepted, eavesdropped on, or tampered with. The biggest advantage of enabling biometric authentication is the improved user experience. Users are now enabled to access their sensitive data or authenticate transactions using TouchID fingerprint validation or FaceID face validation.
A user can enable biometric authentication only if all the following requirements are met:
- The device has hardware support for TouchID or FaceID.
- The device is not jail-broken.
- The user has already registered at least one fingerprint or their face.
- The application configuration on the IDAAS-core allows the use of fingerprint authentication.
Limitation
When authentication with fingerprint fails due to exceeded attempts, the fingerprint refresh token is not removed from the keychain. This is caused by the bug in the Security.framework
, which returns the same error when the fingerprint authentication was cancelled by locking the device and when the user exceeds the fingerprint attempts. If the fingerprint was deregistered when the user exceeded the fingerprint attempts, it would have also been deregistered when the user locks the device during fingerprint authentication. The fingerprint refresh token stays in the keychain until the user deregisters it manually and can be accessed only if the correct fingerprint is provided. The number of attempts can be reset by providing the correct device PIN.
Biometric authenticator registration
To use Face ID, you must add the NSFaceIDUsageDescription
key into your application Info.plist
and provide a usage description as a value.
Registering the biometric authenticator requires the user to perform PIN authentication. To successfully perform authentication, implementing the userClient(_:didReceivePinChallenge:for:)
method of the AuthenticatorRegistrationDelegate
protocol is required.
Authenticate with biometrics
When a biometric authenticator is available and set as preferred, the user will be prompted to authenticate with their biometric authenticator instead of their PIN. The iOS SDK will call the userClient(_:didReceiveBiometricChallenge:)
method on your ONGAuthenticationDelegate
.
func userClient(_ userClient: UserClient,
didReceiveBiometricChallenge challenge: BiometricChallenge,
for request: MobileAuthRequest) {
// Continue authentication using fingerprint with custom prompt
challenge.sender.respond(with: "User authentication", to: challenge)
// Fallback to pin if needed
challenge.sender.respondWithPinFallback(to: challenge)
// Or cancel challenge
challenge.sender.cancel(challenge)
}
- (void)userClient:(ONGUserClient *)userClient didReceiveBiometricChallenge:(ONGBiometricChallenge *)challenge
{
// Continue authentication using fingerprint with custom prompt
[challenge.sender respondWithPrompt:@"Confirm money transfer" challenge:challenge];
// Fallback to pin if needed
[challenge.sender respondWithPinFallbackForChallenge:challenge];
// Or cancel challenge
[challenge.sender cancelChallenge:challenge];
}
The user can always choose to fall back to PIN authentication by selecting the Cancel button.
In case the user fails to scan a valid biomeric within the allowed number of times, the iOS SDK will automatically perform a fallback to PIN authentication. When this happens, the iOS SDK triggers a call to AuthenticationDelegate#(didReceivePinChallenge:)
.
Biometric authenticator deregistration
Biometric authenticator will also be deregistered when a user changes the registered set of fingerprints for Touch ID or registered face for Face ID.
Jailbreak detection
The jailbreak detection check is applied even if jailbreak detection is disabled for the application. The reason is that jailbroken devices are more vulnerable, because the application sandbox and KeyChain can be violated. The situation is even more serious for clients who are not using tampering protection or are running on older versions of iOS.
Custom authenticator
The iOS SDK offers an API that enables you to integrate it with any custom authenticator. The iOS SDK is able to authenticate the user using the data sent from the custom authenticator to the extension engine.
Custom authentication is available only if all of the following requirements are met:
- The custom authenticators feature must be enabled in the IDAAS-core system configuration.
- The application configuration on the IDAAS-core allows the use of custom authentication.
- Registration and authentication scripts must be submitted to the custom authenticator configuration.
Register custom authentication
To register a custom authenticator, there is one additional step to implement. During registration, the iOS SDK will call the userClient(_:didReceiveCustomAuthFinishRegistrationChallenge:)
method on your delegate. This step is required in order to pass the authenticator data from your application to the extension engine.
The userClient(_:didReceiveCustomAuthFinishRegistrationChallenge:)
method has two parameters:
- userClient (
UserClient
) is the user client performing registration. - challenge (
CustomAuthFinishRegistrationChallenge
) is the challenge used to complete the registration.
The challenge object provides all information about the authenticator registration and the sender waiting for the response:
- userProfile (
UserProfile
) is the user profile for which the registration request challenge was sent. - authenticator (
Authenticator
) is the authenticator being registered. - sender (
CustomAuthFinishRegistrationChallengeSender
) is the sender waiting for a response to the registration request challenge.
Using the sender object, you will be able to respond to a challenge with data or to cancel it.
respond:to:
is used to deliver the authenticator data to the iOS SDK.cancel:underlyingError:
is used for registration cancellation. Optionally, you can pass the underlying error containing the reason behind the cancellation. The underlying error will be included within the userInfo (underNSUnderlyingErrorKey
) of cancellation error returned by the iOS SDK in failure callback.
Example: register custom authenticator
func userClient(_ userClient: UserClient,
didReceiveCustomAuthFinishRegistrationChallenge challenge: CustomAuthFinishRegistrationChallenge){
// Continue authenticator registration with data
challenge.sender.respond(with: "custom authenticator data", to: challenge)
// Or cancel challenge
challenge.sender.cancel(challenge, underlyingError: nil)
}
- (void)userClient:(ONGUserClient *)userClient didReceiveCustomAuthFinishRegistrationChallenge:(ONGCustomAuthFinishRegistrationChallenge *)challenge
{
// Continue authenticator registration with data
[challenge.sender respondWithData:@"custom authenticator data" challenge:challenge];
// Or cancel challenge
[challenge.sender cancelChallenge:challenge underlyingError:nil];
}
Authenticate using a custom authenticator
When you use a custom authenticator for authentication, you will be prompted to authenticate with the custom authenticator instead of a PIN. The iOS SDK will call the userClient(_:didReceiveCustomAuthFinishAuthenticationChallenge:)
method on your AuthenticationDelegate
. You will be able to choose how to respond to the challenge.
The userClient(_:didReceiveCustomAuthFinishAuthenticationChallenge:)
method has two parameters:
- userClient (
UserClient
) is the user client performing registration. - challenge (
CustomAuthFinishAuthenticationChallenge
) is the challenge used to complete the authentication.
The challenge object provides all information about the authentication and the sender waiting for the response:
- userProfile (
UserProfile
) is the user profile for which the authentication request challenge was sent. - authenticator (
Authenticator
) is the authenticator used for authentication. - sender (
CustomAuthFinishAuthenticationChallengeSender
) is the sender waiting for a response to the authentication challenge.
Using the sender object, you will be able to respond to the challenge with data, fallback to PIN, or cancel it.
respond:to:
is used to deliver the authenticator data to the iOS SDK.respondWithPinFallback:
is used to complete the authentication with PIN instead of custom authenticator.cancel:underlyingError:
is used for authentication cancellation. Optionally, you can pass the underlying error containing the reason behind the cancellation. The underlying error will be included within the userInfo (underNSUnderlyingErrorKey
) of cancellation error returned by the iOS SDK in failure callback.
Example: authenticate using custom authenticator
func userClient(_ userClient: UserClient,
didReceiveCustomAuthFinishAuthenticationChallenge challenge: CustomAuthFinishAuthenticationChallenge) {
// Continue authenticator registration with data
challenge.sender.respond(with: "custom authenticator data", to: challenge)
// Fallback to pin if needed
challenge.sender.responseWithPinFallback(to: challenge)
// Or cancel challenge
challenge.sender.cancel(challenge, underlyingError: nil)
}
- (void)userClient:(ONGUserClient *)userClient didReceiveCustomAuthFinishAuthenticationChallenge:(ONGCustomAuthFinishAuthenticationChallenge *)challenge
{
// Continue authentication using custom authenticator
[challenge.sender respondWithData:@"custom authenticator data" challenge:challenge];
// Fallback to pin if needed
[challenge.sender respondWithPinFallbackForChallenge:challenge];
// Or cancel challenge
[challenge.sender cancelChallenge:challenge underlyingError:nil];
}
In case of any issues encountered with the custom authenticator, the iOS SDK provides an option to fallback to PIN or cancel authentication. Calling the cancel method on the challenge will also result in cancelling the authentication process.
Deregister the custom authenticator
When using a custom authenticator, the iOS SDK will send a challenge in order to receive the authenticator data needed to complete the deregistration.
The following method will be called by the iOS SDK AuthenticatorDeregistrationDelegate userClient(_:didReceiveCustomAuthDeregistrationChallenge:)
.
- userClient (
UserClient
) is the user client performing registration. - challenge (
CustomAuthDeregistrationChallenge
) is the challenge used to complete the authenticator deregistration.
The challenge object provides all information about the authenticator deregistration and the sender waiting for the response:
- userProfile (
UserProfile
) is the user profile for which authentication request challenge was sent. - authenticator (
Authenticator
) is the authenticator used for authentication. - sender (
CustomAuthDeregistrationChallengeSender
) is the sender waiting for a response to the authentication challenge.
Using the sender object, you will be able to respond to a challenge with data or to cancel it.
respondWithData:challenge:
is used to deliver the authenticator data to the iOS SDK.cancelChallenge:underlyingError:
is used for deregistration cancellation. Optionally, you can pass the underlying error containing the reason behind the cancellation. The underlying error will be included within theuserInfo
(underNSUnderlyingErrorKey
) of cancellation error returned by the iOS SDK in failure callback.
Example: deregistering the custom authenticator
func userClient(_ userClient: UserClient,
didReceiveCustomAuthDeregistrationChallenge challenge: CustomAuthDeregistrationChallenge){
// Continue custom authenticator deregistration
challenge.sender.proceed(with: challenge)
// Or cancel challenge
challenge.sender.cancel(challenge, underlyingError: nil)
}
- (void)userClient:(ONGUserClient *)userClient didReceiveCustomAuthDeregistrationChallenge:(ONGCustomAuthDeregistrationChallenge *)challenge
{
// Continue custom authenticator deregistration
[challenge.sender respondWithData:@"custom authenticator data" challenge:challenge];
// Or cancel challenge
[challenge.sender cancelChallenge:challenge underlyingError:nil];
}