FaceID should fallback to Passcode but does not - ios

I have inherited a code base with the following class providing support for Face/Touch ID.
The expected behaviour is that on Face/Touch ID success the user is signed in. This works.
However, should the user fail Face ID and opt to enter their passcode, they are signed out once the completion handler is called. I believe opting to use passcode is triggering
else {
self.authState = .unauthenticated
completion(.unauthenticated)
}
How can I trigger the passcode prompt instead? Should I create a second policy using LAPolicy.deviceOwnerAuthentication and evaluate that instead?
import LocalAuthentication
public enum AuthenticationState {
case unknown
case authenticated
case unauthenticated
public func isAuthenticated() -> Bool {
return self == .authenticated
}
}
public protocol TouchIDAuthenticatorType {
var authState: AuthenticationState { get }
func authenticate(reason: String, completion: #escaping (AuthenticationState) -> Void) -> Void
func removeAuthentication() -> Void
}
public protocol LAContextType: class {
func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool
func evaluatePolicy(_ policy: LAPolicy, localizedReason: String, reply: #escaping (Bool, Error?) -> Void)
}
public class TouchIDAuthenticator: TouchIDAuthenticatorType {
public var authState: AuthenticationState = .unknown
private var context: LAContextType
private var policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
public init(context: LAContextType = LAContext()) {
self.context = context
}
public func authenticate(reason: String, completion: #escaping (AuthenticationState) -> Void) -> Void {
var error: NSError?
if context.canEvaluatePolicy(policy, error: &error) {
context.evaluatePolicy(policy, localizedReason: reason) { (success, error) in
DispatchQueue.main.async {
if success {
self.authState = .authenticated
completion(.authenticated)
} else {
self.authState = .unauthenticated
completion(.unauthenticated)
}
}
}
} else {
authState = .authenticated
completion(.authenticated)
}
}
public func removeAuthentication() -> Void {
authState = .unknown
context = LAContext() // reset the context
}
}
extension LAContext: LAContextType { }
I should point out, on the simulator this appears to work as expected, but on a device it does not and I signed out.

You have to use .deviceOwnerAuthentication instead of asking for biometrics. If FaceID is available, it will force the attempt to use this either way.
If you try enough times then you will get another dialogue to "Cancel" or fallback to "Use passcode".
Choosing the fallback will show you the passcode screen.
However, if you specified .deviceOwnerAuthenticationWithBiometrics, you will get the same fallback option. Rather than getting this dialogue I would have expected to receive an error of LAError.Code.biometryLockout. But instead I get this fallback option dialogue. But that is ok...
However, if I then tap the fallback option to "Use passcode", it will NOT present the passcode alert. Instead it fails with a LAError.Code.userFallback error.
If you use the policy without biometrics, you will not get and be able to catch the .userFallback error.
So to sum things up:
If you ask for the deviceOwnerAuthenticationWithBiometrics policy then you will have to handle the fallback yourself.
If you ask for deviceOwnerAuthentication only, then biometrics will be used if available and authorized, otherwise it will automatically fall back to passcode if biometrics is unavailable, or give you the fallback option to enter passcode automatically if biometrics attemps fail.

Related

How to check if user revoked HealthKit permissions through Settings and act in response to it?

I am using Apple's HealthKit to build an app, and am requesting permission from the user to read and write Sleep Data as soon as a View loads using SwiftUI's .onAppear modifier.
It all works fine and I get the data I need using this method.
However, if a user revokes the read and write permissions for my app through Settings, instead of requesting permission again, the app crashes. This is how I have set things up.
#State var authorized: Bool = false
var body: some View {
Text("\(initTextContent)")
.onAppear {
healthstore.getHealthAuthorization(completionAuth: {success in
authorized = success
if self.authorized == true {
healthstore.getSleepMetrics(startDate: sevendaysAgo, endDate: sevendaysAfter, completion: sleepDictAssign)
}
else {
initTextContent = "Required Authorization Not Provided"
}
})
}
}
I've created a class called healthstore and am simply using HealthKit's requestAuthorization method as follows:
var healthStore: HKHealthStore()
func getHealthAuthorization(completionAuth: #escaping (Bool) -> Void) {
//we will call this inside our view
healthStore.requestAuthorization(toShare: [sleepSampleType], read: [sleepSampleType]) { (success, error) in
if let error = error {
// Handle the error here.
fatalError("*** An error occurred \(error.localizedDescription) ***")
}
else {
completion(success)
}
}
}
If the user revoked the permisions they are revoked. You cannot ask again. If you want to handle this scenario you would need to throw the error and handle it outside of the function by showing a message to the user asking them to enable it again.
Or simply return the success boolean ignoring the error.
func getHealthAuthorization(completionAuth: #escaping (Bool) -> Void) {
//we will call this inside our view
healthStore.requestAuthorization(toShare: [sleepSampleType], read: [sleepSampleType]) { (success, error) in
//ignore error since your func can just return a boolean
completion(success)
}
}

Unable to attach debugger to Network Extension iOS ("Waiting to Attach")

I'm new to iOS development, and am writing a VPN app, using the OpenVPN framework. I'm making use of the Network Extension target to tunnel network traffic to our OpenVPN server. However, when trying to debug, I can't attach the debugger to the Network Extension - it's stuck at "Waiting to Attach" in the Debug Navigator view.
In this blog post by Alex Grebenyuk he gives some recommendations for troubleshooting when you fail to attach the debugger to the extension. I've meticulously checked all of them, and my app correctly uses everything there: Both the app and the network extension have the Packet Tunnel Network Extension capability and bundle names all work out.
Some more info on the bundle names in case it might be relevant:
Bundle name for the app: com.example.Example-App
Bundle name for the Network Extension: com.example.Example-App.Example-VPN
Group name: group.com.example.Example-App
I'm out of ideas here. Could the problem be in the Network Extension never being started? It doesn't seem to be the case, because the startTunnel() function is called. The code for starting the VPN through the extension:
func configureVPN(serverAddress: String) {
guard let configData = readTestFile() else { return }
// Test file with ovpn configuration seems is read in correctly - contents are shown if printed to console.
self.providerManager?.loadFromPreferences { error in
if error == nil {
let tunnelProtocol = NETunnelProviderProtocol()
tunnelProtocol.serverAddress = serverAddress
tunnelProtocol.providerBundleIdentifier = "com.example.Example-App.Example-VPN" // bundle id of the network extension target
tunnelProtocol.providerConfiguration = ["ovpn": configData]
self.providerManager.protocolConfiguration = tunnelProtocol
self.providerManager.localizedDescription = "Example VPN"
self.providerManager.isEnabled = true
self.providerManager.saveToPreferences(completionHandler: { (error) in
if error == nil {
self.providerManager.loadFromPreferences(completionHandler: { (error) in
do {
print("Trying to start connection now") // This line is printed to the Console
try self.providerManager.connection.startVPNTunnel() // start the VPN tunnel.
} catch let error {
print(error.localizedDescription)
}
})
}
})
}
}
}
Not sure if this is relevant information, but I am using the OpenVPNAdapter in the PacketTunnelProvider, following this tutorial:
import NetworkExtension
import OpenVPNAdapter
class PacketTunnelProvider: NEPacketTunnelProvider {
lazy var vpnAdapter: OpenVPNAdapter = {
let adapter = OpenVPNAdapter()
adapter.delegate = self
return adapter
}()
let vpnReachability = OpenVPNReachability()
var startHandler: ((Error?) -> Void)?
var stopHandler: (() -> Void)?
override func startTunnel(
options: [String : NSObject]?,
completionHandler: #escaping (Error?) -> Void
) {
// There are many ways to provide OpenVPN settings to the tunnel provider. For instance,
// you can use `options` argument of `startTunnel(options:completionHandler:)` method or get
// settings from `protocolConfiguration.providerConfiguration` property of `NEPacketTunnelProvider`
// class. Also you may provide just content of a ovpn file or use key:value pairs
// that may be provided exclusively or in addition to file content.
// In our case we need providerConfiguration dictionary to retrieve content
// of the OpenVPN configuration file. Other options related to the tunnel
// provider also can be stored there.
guard
let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
let providerConfiguration = protocolConfiguration.providerConfiguration
else {
fatalError()
}
guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else {
fatalError()
}
let configuration = OpenVPNConfiguration()
configuration.fileContent = ovpnFileContent
configuration.settings = [ :
// Additional parameters as key:value pairs may be provided here
]
// Uncomment this line if you want to keep TUN interface active during pauses or reconnections
// configuration.tunPersist = true
// Apply OpenVPN configuration
let evaluation: OpenVPNConfigurationEvaluation
do {
evaluation = try vpnAdapter.apply(configuration: configuration)
} catch {
completionHandler(error)
return
}
// Provide credentials if needed
if !evaluation.autologin {
// If your VPN configuration requires user credentials you can provide them by
// `protocolConfiguration.username` and `protocolConfiguration.passwordReference`
// properties. It is recommended to use persistent keychain reference to a keychain
// item containing the password.
guard let username: String = protocolConfiguration.username else {
fatalError()
}
// Retrieve a password from the keychain
let password: String = "Test"
let credentials = OpenVPNCredentials()
credentials.username = username
credentials.password = password
do {
try vpnAdapter.provide(credentials: credentials)
} catch {
completionHandler(error)
return
}
}
// Checking reachability. In some cases after switching from cellular to
// WiFi the adapter still uses cellular data. Changing reachability forces
// reconnection so the adapter will use actual connection.
vpnReachability.startTracking { [weak self] status in
guard status == .reachableViaWiFi else { return }
self?.vpnAdapter.reconnect(afterTimeInterval: 5)
}
// Establish connection and wait for .connected event
startHandler = completionHandler
vpnAdapter.connect(using: packetFlow as! OpenVPNAdapterPacketFlow)
}
override func stopTunnel(
with reason: NEProviderStopReason,
completionHandler: #escaping () -> Void
) {
stopHandler = completionHandler
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
vpnAdapter.disconnect()
}
}
extension PacketTunnelProvider: OpenVPNAdapterDelegate {
// OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
// `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
// protocol if the tunnel is configured without errors. Otherwise send nil.
// `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
// you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
// send `self.packetFlow` to `completionHandler` callback.
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?,
completionHandler: #escaping (Error?) -> Void
) {
// In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
// send empty string to NEDNSSettings.matchDomains
networkSettings?.dnsSettings?.matchDomains = [""]
// Set the network settings for the current tunneling session.
setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
}
// Process events returned by the OpenVPN library
func openVPNAdapter(
_ openVPNAdapter: OpenVPNAdapter,
handleEvent event:
OpenVPNAdapterEvent, message: String?
) {
switch event {
case .connected:
if reasserting {
reasserting = false
}
guard let startHandler = startHandler else { return }
startHandler(nil)
self.startHandler = nil
case .disconnected:
guard let stopHandler = stopHandler else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
stopHandler()
self.stopHandler = nil
case .reconnecting:
reasserting = true
default:
break
}
}
// Handle errors thrown by the OpenVPN library
func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
// Handle only fatal errors
guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool,
fatal == true else { return }
if vpnReachability.isTracking {
vpnReachability.stopTracking()
}
if let startHandler = startHandler {
startHandler(error)
self.startHandler = nil
} else {
cancelTunnelWithError(error)
}
}
}
Any help with this would be hugely appreciated! Thanks.
You need to attache the bundle name and not the bundle identifier and the bundle name should not be an identifier. The PRODUCT_BUNDLE_IDENTIFIER of your network extension would be something like com.example.my-network-extension-id (it cannot be com.example.Example-App.Example-VPN, as a third period is not allowed for network extension IDs) and the PRODUCT_NAME would be something like My Network Extension, and you would then attach to My Network Extension (yes, with spaces and no extension).

RealmSwift: Implementing one time login with MongoDB Realm in swift

I want user to login once and not have to reenter their login info everytime they open app unless they logout in the last session.
Login screen is currently displayed everytime the app is open. This is my rootview
struct AppRootView: View {
var body: some View {
AnyView {
// check if user has already logged in here and then route them accordingly
if auth.token != nil {
homeMainView()
} else {
LoginController()
}
}
}
}
currently this is what I use to login users
#objc func signUp() {
setLoading(true);
app.usernamePasswordProviderClient().registerEmail(username!, password: password!, completion: {[weak self](error) in
// Completion handlers are not necessarily called on the UI thread.
// This call to DispatchQueue.main.sync ensures that any changes to the UI,
// namely disabling the loading indicator and navigating to the next page,
// are handled on the UI thread:
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
print("Signup failed: \(error!)")
self!.errorLabel.text = "Signup failed: \(error!.localizedDescription)"
return
}
print("Signup successful!")
// Registering just registers. Now we need to sign in, but we can reuse the existing username and password.
self!.errorLabel.text = "Signup successful! Signing in..."
self!.signIn()
}
})
}
#objc func signIn() {
print("Log in as user: \(username!)");
setLoading(true);
app.login(withCredential: AppCredentials(username: username!, password: password!)) { [weak self](maybeUser, error) in
DispatchQueue.main.sync {
self!.setLoading(false);
guard error == nil else {
// Auth error: user already exists? Try logging in as that user.
print("Login failed: \(error!)");
self!.errorLabel.text = "Login failed: \(error!.localizedDescription)"
return
}
guard let user = maybeUser else {
fatalError("Invalid user object?")
}
print("Login succeeded!");
//
let hostingController = UIHostingController(rootView: ContentView())
self?.navigationController?.pushViewController(hostingController, animated: true)
}
how could I implement one time login so that users do have to login each time they open the app?
A correctly configured and initialized RealmApp class will persist the session information for you between app restarts, you can check for an existing session using the .currentUser() method from this class. So in your case something like:
if app.currentUser() != nil {
homeMainView()
} else {
LoginController()
}
While using Realm to persist login is a good idea, but I would highly
advice against using it for managing user authentication credentials such
as passwords. A better approach if you want to save sensitive information is
using KeyChain just like what Apple and password manager apps do. With a light
weight keyChain wrapper library such as SwiftKeychainWrapper You can easily
save your login credentials in the most secure way.
Here is a sample using a keyChain wrapper linked above.
With simple modification you can use this helper class to manage your sign in credentials anywhere in your app.
import SwiftKeychainWrapper
class KeyChainService {
// Make a singleton
static let shared = KeyChainService()
// Strings which will be used to map data in keychain
private let passwordKey = "passwordKey"
private let emailKey = "emailKey"
private let signInTokenKey = "signInTokenKey"
// Saving sign in info to keyChain
func saveUserSignInInformation(
email: String,
password: String,
token: String
onError: #escaping() -> Void,
onSuccess: #escaping() -> Void
) {
DispatchQueue.global(qos: .default).async {
let passwordIsSaved: Bool = KeychainWrapper.standard.set(password, forKey: self.passwordKey)
let emailIsSaved: Bool = KeychainWrapper.standard.set(email, forKey: self.emailKey)
let tokenIsSaved: Bool = KeychainWrapper.standard.set(token, forKey: self.signInTokenKey)
DispatchQueue.main.async {
// Verify that everything is saved as expected.
if passwordIsSaved && emailIsSaved && tokenIsSaved {
onSuccess()
}else {
onError()
}
}
}
}
// Retrieve signIn information for auto login
func retrieveSignInInfo(onError: #escaping() -> Void, onSuccess: #escaping(UserModel) -> Void) {
DispatchQueue.main.async {
let retrievedPassword: String? = KeychainWrapper.standard.string(forKey: self.passwordKey)
let retrievedEmail: String? = KeychainWrapper.standard.string(forKey: self.emailKey)
let retrievedToken: String? = KeychainWrapper.standard.string(forKey: self.signInTokenKey)
if let password = retrievedPassword,
let email = retrievedEmail,
let token = retrievedToken {
// Assuming that you have a custom user model named "UserModel"
let user = UserModel(email: email, password: password,token: token)
// Here is your user info which you can use to verify with server if needed and auto login user.
onSuccess(user)
}else {
onError()
}
}
}
}

Is there a way to unit test Touch Id using XCTest?

Is there a way to unit test TouchId's "evaluatePolicy" or "canevaluatePolicy" using XCTest? I want to unit test using simulator as base.
Thanks
I have described one way of writing unit test for Touch ID code in this post.
Basically, if you have some Touch ID managing class similar to this one:
final public class TouchIDManager {
/// Authentication context object we use for Touch ID. Typically it's just a `LAContext` instance that is mocked when testing.
internal var authenticationContext = LAContext()
/**
Checks Touch ID availability.
- returns: Flag indicating if Touch ID is available
*/
public func touchIDAvailable() -> Bool {
return authenticationContext.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: nil)
}
/**
Authenticates the user with Touch ID
- parameter completion: Completion handler
*/
public func authenticate(completion: (success: Bool) -> ()) {
guard touchIDAvailable() else {
completion(false)
return
}
authenticationContext.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: "Wanna Touch my ID?") {
(success: Bool, error: NSError?) -> Void in
completion(success: success)
}
}
}
You can simply fake the LAContext behaviour with a StubLAContext class:
func testSuccessfulAuthentication() {
/// A class faking Touch ID availability and successful user verification
class StubLAContext: LAContext {
override func evaluatePolicy(policy: LAPolicy, localizedReason: String, reply: (Bool, NSError?) -> Void) { reply(true, nil) }
override func canEvaluatePolicy(policy: LAPolicy, error: NSErrorPointer) -> Bool { return true }
}
let manager = TouchIDManager()
manager.authenticationContext = StubLAContext()
manager.authenticate { (success) in
XCTAssertTrue(success)
}
}
Clearly this particular test doesn't do much, but it might be a good starting point to test other Touch ID related logic you have in your application.

swift - touchID takes long time to load

I'm working on integrating touchID into my application. The process was fairly simple, but even just using my dummy data, it takes about 5 seconds after it authenticated my fingerprint, before it performs it's task.
Here's my code:
func requestFingerprintAuthentication() {
let context = LAContext()
var authError: NSError?
let authenticationReason: String = "Login"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &authError) {
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: authenticationReason, reply: {
(success: Bool, error: NSError?) -> Void in
if success {
println("successfull signin with touchID")
self.emailInputField.text = "john.doe#gmail.com"
self.passwordInputField.text = "password"
self.signIn(self.signInButton)
} else {
println("Unable to Authenticate touchID")
}
})
}
}
even with the dummy data, it takes waaay too long.
When I login normally, by typing the email and the password into my inputfields, the signIn() function runs instantly.
To see if it was a problem with that. I tried replacing that, with 2 lines that simply takes me to the correct viewController. But it still takes several seconds after it's authenticated my fingerprint.
I know it's not the phone, nor touchID. Cause it runs my println("successfull signin with touchID") immediately. It's what comes after that, that for some reason takes several seconds for it to run?
Any help explaining this would be greatly appreciated!
The documentation states:
This method asynchronously evaluates an authentication policy.
You are running UI code on a thread that is not the main. Wrap your code to get it to perform on the main thread:
func requestFingerprintAuthentication() {
let context = LAContext()
var authError: NSError?
let authenticationReason: String = "Login"
if context.canEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, error: &authError) {
context.evaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, localizedReason: authenticationReason, reply: {
(success: Bool, error: NSError?) -> Void in
if success {
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
println("successfull signin with touchID")
self.emailInputField.text = "john.doe#gmail.com"
self.passwordInputField.text = "password"
self.signIn(self.signInButton)
})
} else {
println("Unable to Authenticate touchID")
}
})
}
}

Resources