Skip to Content
Author's profile photo Waldemar Schlegel

SAP Cloud Platform iOS SDK On boarding flow with Touch ID, Passcode and Basic Authentication

SAP Cloud Platform iOS SDK Onboarding with Basic Authentication

This blog post shows how to setup the onboarding flow for an iOS Application with the SAP Cloud Platform iOS SDK using Basic Authentication, Passcode and Touch ID protection.

The onboarding flow includes the following screens and features:

  1. Welcome screen
  2. Login screen where the user provides his username and password on the first app start
  3. Passcode screen where the user creates a passcode after his first login
  4. Touch ID enablement screen where the user decides if he wants to enable Touch ID after specifying his passcode
  5. Passcode screen where the user can authenticate with Touch ID or passcode.

The user needs to enter his login credentials on the first app start which will be stored in the secure Apple Keychain.

Screen flow

Prerequisites

Hana Trial Account (https://account.hanatrial.ondemand.com/)

SAP Cloud Platform iOS SDK Assistant (https://store.sap.com/sap/cp/ui/resources/store/html/SolutionDetails.html?pid=0000014485)

XCode IDE (https://itunes.apple.com/us/app/xcode/id497799835?mt=12)

Finished application

A version of the finished application (without the SAP Cloud Platform iOS SDK Framework files) you can find on Github (https://github.com/walde1024/iOS-on-boarding-sample)

Prepare Project

Start the SAP Cloud Platform iOS SDK Assistant and click on the + Button to add a new application.

Specify the project attributes and click on Next.

 

Click on the account drop down and select Add new account.

 

Enter you account credentials and press Save.

The Admin API URL looks like the following:
https://hcpmsadmin-d056936trial.hanatrial.ondemand.com (Replace d056936trial with your own username)

 

Create a new application with basic authentication. Enter therefore the application name, identifier and select Basic Authentication for the authentication type. Press Next.

 

Press Add to add the sample backend.

Specify the following URL as backend URL: https://hcpms-d056936trial.hanatrial.ondemand.com/SampleServices/ESPM.svc

Don’t forget to replace the d056936trial with your own username. Press OK.

 

 

Press Finish.

 

After that the iOS project will be generated and opened in the XCode IDE.

Now you can run the application and authenticate yourself. After that you will see some screens with the sample data from the sample OData Service.

 

If you need more information about how to setup a project with the iOS SDK Assistant you can checkout the mobile interactive tutorials (https://www.sap.com/germany/developer/tutorial-navigator/mobile-interactive-tutorials.html)

Implementing on boarding flow

Extend storyboard

Create a new UIViewController which will be the new starting point of the iOS application. This UIViewController will be responsible for the onboarding flow.
Therefore create a Cocoa Touch Class named OnBoardingViewController in the ViewControllers Group.

 

Now open the Main storyboard and add a new ViewController to the storyboard which uses the OnBoardingViewController class which you have created in the previous step.

 

Embed the OnBoardingViewController in a navigation controller and set it as main entry point of the application.

 

Add three storyboard references to the Main storyboard.

 

Specify the following attributes on the Attributes Inspector Tab for the Storyboard references:

  1. FUIWelcomeScreen:Storyboard: FUIWelcomeScreen
    Bundle: com.sap.cp.sdk.ios.SAPFiori
  2. FUIPasscodeCreateControllerStoryBoard: FUIPasscodeCreateController
    Bundle: com.sap.cp.sdk.ios.SAPFiori
  3. FUIPasscodeInputControllerStoryBoard: FUIPasscodeInputController
    Bundle: com.sap.cp.sdk.ios.SAPFiori

 

Setup 3 navigation segues from the OnBoardingViewController to the storyboard references which you have created in the previous step with the following attributes:

  1. Segue from OnBoardingViewController to FUIWelcomeScreen
    Identifier: showFUIWelcomeScreen
    Kind: Show
  2. Segue from OnBoardingViewController to FUIPasscodeCreateController
    Identifier: showCreatePasscodeScreen
    Kind: Show
  3. Segue from OnBoardingViewController to FUIPasscodeInputController
    Identifier: showFUIPasscodeInput
    Kind: Show

Don’t forget to uncheck the Animates checkbox for the showFUIWelcomeScreen segue.

 

Embed the BasicAuthViewController in a navigation controller and set the title in the navigation bar.

 

Create a Show segue from the OnBoardingViewController to the BasicAuthViewController and name it showLoginScreen.

 

Add a cancel button to the navigation bar of the BasicAuthViewController.

 

Specify an action for the cancel button in the BasicAuthViewController with the following implementation:

@IBAction func onCancel(_ sender: UIBarButtonItem) {
	dismiss(animated: true, completion: nil)
}

 

The storyboard is now ready to handle the on boarding process.

Implement Onboarding

Create a Swift file named KeychainUtils.swfit in the Utils Group of the project. This is a helper class which is responsible for storing the user credentials in the keychain.

Copy and paste the following code into the file:

//
//  KeychainUtils.swift
//  iOS-on-boarding
//
//  Created by Schlegel, Waldemar on 19/07/2017.
//  Copyright © 2017 SAP. All rights reserved.
//

import Foundation

// functions for keychain save and retrieve data
// Note: The "Keychain Sharing" capability for the app needs to be turned on
// in order for all these operations to be successful.


/**
 Saving a String to keychain.
 Note: The "Keychain Sharing" capability for the app needs to be turned on
 in order for this operation be successful.
 - parameter key: The key for the String.
 - parameter value: The String value to be saved.
 */
func saveKeychainString(key: String, value: String) {
    let data = value.data(using: .utf8)
    saveKeychainData(key: key, data: data!)
}

/**
 Saving a Bool value to keychain.
 Note: The "Keychain Sharing" capability for the app needs to be turned on
 in order for this operation be successful.
 - parameter key: The key for the Bool.
 - parameter value: The Bool value to be saved.
 */
func saveKeychainBool(key: String, value: Bool) {
    let byte = value ? 0x1 : 0 as UInt8
    let data = Data.init(bytes: [byte])
    saveKeychainData(key: key, data: data)
}

/**
 Saving an Int value to keychain.
 Note: The "Keychain Sharing" capability for the app needs to be turned on
 in order for this operation be successful.
 - parameter key: The key for the Int.
 - parameter value: The Int value to be saved.
 */
func saveKeychainInt(key: String, value: Int) {
    let s = String(value)
    saveKeychainString(key: key, value: s)
}

/**
 Load the Bool value previously saved in the keychain.
 Note: The "Keychain Sharing" capability for the app needs to be turned on
 in order for this operation be successful.
 - parameter key: The key for the Bool value.
 - returns: The previously stored Bool value. Or, nil if there was no value saved for this key.
 */
func loadKeychainBool(key: String) -> Bool? {
    let data = loadKeychainData(key: key)
    if data != nil {
        guard let firstByte = data!.first else {
            return nil
        }
        return firstByte == 0x1
    }
    return nil
}

/**
 Load the String value previously saved in the keychain.
 Note: The "Keychain Sharing" capability for the app needs to be turned on
 in order for this operation be successful.
 - parameter key: The key for the String value.
 - returns: The previously stored String value. Or, nil if there was no value saved for this key.
 */
func loadKeychainString(key: String) -> String? {
    let data = loadKeychainData(key: key)
    if data != nil {
        return String(data: data!, encoding: .utf8)
    }
    return nil
}

/**
 Load the Int value previously saved in the keychain.
 Note: The "Keychain Sharing" capability for the app needs to be turned on
 in order for this operation be successful.
 - parameter key: The key for the Int value.
 - returns: The previously stored Int value. Or, nil if there was no value saved for this key.
 */
func loadKeychainInt(key: String) -> Int? {
    let s = loadKeychainString(key: key)
    if s != nil {
        // let sb: String = s!
        return Int.init(s!)
    }
    return nil
}

// First implementation using kSecClassGenericPassword.
// Device runs fine without turning on the "Keychain Sharing" capability.
// However, simulator needs to turn on "Keychain Sharing" capbility.
// Otherwise, error -34018 is returned.
private func saveKeychainData(key: String, data: Data) {
    removeKeychainValue(key: key)
    
    var keychainQuery: [String: AnyObject] = [String: AnyObject]()
    keychainQuery[kSecClass as String] = kSecClassGenericPassword
    keychainQuery[kSecAttrAccount as String] = key as AnyObject
    keychainQuery[kSecValueData as String] = data as AnyObject
    keychainQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAlwaysThisDeviceOnly
    
    let result = SecItemAdd(keychainQuery as CFDictionary, nil)
    print("result = \(result)")
}

private func loadKeychainData(key: String) -> Data? {
    
    var keychainQuery: [String: AnyObject] = [String: AnyObject]()
    keychainQuery[kSecClass as String] = kSecClassGenericPassword
    keychainQuery[kSecAttrAccount as String] = key as AnyObject
    keychainQuery[kSecMatchLimit as String] = kSecMatchLimitOne
    keychainQuery[kSecReturnData as String] = kCFBooleanTrue
    
    var resultValue: AnyObject?
    
    let result = withUnsafeMutablePointer(to: &resultValue) {
        SecItemCopyMatching(keychainQuery as CFDictionary, UnsafeMutablePointer($0))
    }
    
    if result == noErr {
        return resultValue as? Data
    }
    
    return nil
}


func removeKeychainValue(key: String) {
    let keychainQuery: NSMutableDictionary = NSMutableDictionary(
        objects: [kSecClassGenericPassword as NSString, key],
        forKeys: [kSecClass as NSString, kSecAttrAccount as NSString])
    SecItemDelete(keychainQuery)
}

// Second implementation using kSecClassKey.
// Both device and simulator need to turn on "Keychain Sharing" capbility.
// Otherwise, error -34018 is returned.
private func saveKeychainData1(key: String, data: Data) {
    removeKeychainValue(key: key)
    
    var keychainQuery: [String: AnyObject] = [String: AnyObject]()
    keychainQuery[kSecClass as String] = kSecClassKey
    keychainQuery[kSecAttrApplicationTag as String] = key as AnyObject
    
    keychainQuery[kSecValueData as String] = data as AnyObject
    keychainQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAlwaysThisDeviceOnly
    
    let result = SecItemAdd(keychainQuery as CFDictionary, nil)
    print("result = \(result)")
}

private func loadKeychainData1(key: String) -> Data? {
    
    var keychainQuery: [String: AnyObject] = [String: AnyObject]()
    keychainQuery[kSecClass as String] = kSecClassKey
    keychainQuery[kSecAttrApplicationTag as String] = key as AnyObject
    keychainQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAlwaysThisDeviceOnly
    keychainQuery[kSecMatchLimit as String] = kSecMatchLimitOne
    keychainQuery[kSecReturnData as String] = kCFBooleanTrue
    keychainQuery[kSecReturnAttributes as String] = kCFBooleanTrue
    
    var resultValue: AnyObject?
    
    let result = withUnsafeMutablePointer(to: &resultValue) {
        SecItemCopyMatching(keychainQuery as CFDictionary, UnsafeMutablePointer($0))
    }
    
    if result == noErr {
        let resultDict:[String: NSObject] = resultValue as! [String: NSObject]
        return resultDict[kSecValueData as String] as? Data
    }
    
    return nil
}

private func removeKeychainValue1(key: String) {
    var keychainQuery: [String: AnyObject] = [String: AnyObject]()
    keychainQuery[kSecClass as String] = kSecClassKey
    keychainQuery[kSecAttrApplicationTag as String] = key as AnyObject
    
    SecItemDelete(keychainQuery as CFDictionary)
}

 

Create a new group named OnBoarding. This group will contain all onboarding related source files which we are going to create in the next steps.

 

Create a file named AppLogonHelper.swift in the OnBoarding Group. This file will contain a class with the logic to manage the user credentials with the keychain.

Copy and paste the following code into the AppLogonHelper.swift file.

//
//  AppLogonHelper.swift
//  iOS-on-boarding
//
//  Created by Schlegel, Waldemar on 13/06/2017.
//  Copyright © 2017 SAP. All rights reserved.
//

import Foundation
import LocalAuthentication

/**
 This class provides helper functions for local authentication (touchId and passcode)
 and gives access to the basic auth user credentials.
 **/
class AppLogonHelper {
    
    private static let context = LAContext()
    
    enum AppLogonError: Error {
        case maxRetriesReached
        case passcodeMismatch(remainingRetries: Int)
    }
    
    enum AppLogonKeychain {
        static let username = "usernameKey"
        static let password = "passwordKey"
        
        static let passcodeRetriesRemaining = "passcodeRetriesRemainingKey"
        static let currentPasscode = "currentPasscodeKey"
        static let passcodeRetriesLimit = "passcodeRetryLimitKey"
    }
    
    /**
     Return true if the user credentials for the basic auth are available in the keychain.
     **/
    static func userCredentialsAreAvailableInTheKeychain() -> Bool {
        let username = loadKeychainString(key: AppLogonKeychain.username)
        let password = loadKeychainString(key: AppLogonKeychain.password)
        
        guard username != nil, password != nil else {
            return false
        }
        
        return true
    }
    
    /**
     Authenticates user with touch id if touch is enabled. Completion handler returns:
     string with a potential error message for the user
     bool value which indicates if the logon was successfull.
     string with the username for the basic authentication
     string with the password for the basic authentication
     bool value which is true if the user requested a passcode fallback
     **/
    static func authenticateUserWithTouchId(completion: @escaping (String?, Bool, String?, String?, Bool) -> Void) {
        
        guard isTouchIdAvailable() else {
            completion("Touch ID not available", false, nil, nil, false)
            return
        }
        
        context.localizedFallbackTitle = "Enter Passcode"
        
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Logging in with Touch ID") {
            (success, evaluateError) in
            
            if success {
                let username = loadKeychainString(key: AppLogonKeychain.username)
                let password = loadKeychainString(key: AppLogonKeychain.password)
                
                DispatchQueue.main.async {
                    completion(nil, true, username, password, false)
                }
            } else {
                let message: String?
                var requestedPasswordFallback = false
                
                switch evaluateError {
                case LAError.authenticationFailed?:
                    message = "There was a problem verifying your identity."
                case LAError.userCancel?:
                    message = nil
                case LAError.userFallback?:
                    message = nil
                    requestedPasswordFallback = true
                default:
                    message = nil
                }
                
                DispatchQueue.main.async {
                    completion(message, false, nil, nil, requestedPasswordFallback)
                }
            }
        }
    }
    
    static func isTouchIdAvailable() -> Bool {
        return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
    }
    
    // MARK: - Passcode Helper
    
    static func storeUserCredentials(username: String, password: String, passcode: String, maxPasscodeRetries: Int) {
        saveKeychainString(key: AppLogonKeychain.currentPasscode, value: passcode)
        saveKeychainInt(key: AppLogonKeychain.passcodeRetriesRemaining, value: maxPasscodeRetries)
        saveKeychainInt(key: AppLogonKeychain.passcodeRetriesLimit, value: maxPasscodeRetries)
        saveKeychainString(key: AppLogonKeychain.username, value: username)
        saveKeychainString(key: AppLogonKeychain.password, value: password)
    }
    
    static func resetUserCredentials() {
        removeKeychainValue(key: AppLogonKeychain.currentPasscode)
        removeKeychainValue(key: AppLogonKeychain.username)
        removeKeychainValue(key: AppLogonKeychain.password)
        removeKeychainValue(key: AppLogonKeychain.passcodeRetriesLimit)
        removeKeychainValue(key: AppLogonKeychain.passcodeRetriesRemaining)
    }
    
    static func areThereRemainingPasscodeRetries() -> Bool {
        let retryRemainingSaved = loadKeychainInt(key: AppLogonKeychain.passcodeRetriesRemaining)
        if retryRemainingSaved == 0 {
            return false
        }
        
        return true
    }
    
    /**
     Updates the retries count and returns the username and password with the completion handler if the passcode matches with the passcode stored in the keychain.
     
     Throws error if maximum retries are already reached or the passcode does not match.
     **/
    static func tryPasscodeLogon(with passcode: String, completion: @escaping (String, String) -> Void) throws {
        guard areThereRemainingPasscodeRetries() else {
            throw AppLogonError.maxRetriesReached
        }
        
        let passcodeInKeychain = loadKeychainString(key: AppLogonKeychain.currentPasscode)
        
        if (passcodeInKeychain == passcode) {
            let maxRetries = loadKeychainInt(key: AppLogonKeychain.passcodeRetriesLimit)
            if maxRetries != nil {
                saveKeychainInt(key: AppLogonKeychain.passcodeRetriesRemaining, value: maxRetries!)
            }
            
            let username = loadKeychainString(key: AppLogonKeychain.username)
            let password = loadKeychainString(key: AppLogonKeychain.password)
            
            //return true
            completion(username!, password!)
        }
        else {
            var retryRemaining = -1
            let retryRemainingSaved = loadKeychainInt(key: AppLogonKeychain.passcodeRetriesRemaining)
            
            if retryRemainingSaved != nil {
                retryRemaining = retryRemainingSaved!
            }
            
            if retryRemaining >= 0 {
                retryRemaining -= 1
                saveKeychainInt(key: AppLogonKeychain.passcodeRetriesRemaining, value: retryRemaining)
                
                if retryRemaining == 0 {
                    resetUserCredentials()
                }
            }
            
            throw AppLogonError.passcodeMismatch(remainingRetries: retryRemaining)
        }
    }
    
}

 

For the onboarding flow we are using two view controllers from the iOS SDK. The first is the FUIWelcomeController and the second is the FUIPasscodeController. To handle events which are occurring on these view controller we need to provide two delegates which will do the work.

Therefore create a file named WelcomeControllerDelegate.swift in the OnBoarding group and paste the code below into it.

This delegate handles the events of the welcome screen which shows a login button. After pressing the login button the shouldContinueUserOnboarding method is called where the delegate decides which screen needs to be presented to the user. This depends on whether user credentials are already stored in the keychain. If this is the case then the next screen will be the FUIPasscodeController otherwise the BasicAuthViewController will be presented to the user.

XCode will complain that the OnBoardingViewController does not have methods which are called showPasscodeInputScreen or showLoginScreen. This can be ignored since we will create and implement them in the next steps. 

//
//  WelcomeControllerDelegate.swift
//  iOS-on-boarding
//
//  Created by Schlegel, Waldemar on 20.07.17.
//  Copyright © 2017 SAP. All rights reserved.
//

import Foundation
import SAPFiori

class WelcomeControllerDelegate: FUIWelcomeControllerDelegate {
    
    private weak var onboardingViewController: OnBoardingViewController?
    
    private weak var welcomeController: FUIWelcomeController?
    
    private var loadingIndicator: FUILoadingIndicatorView?
    
    init(onboardingViewController: OnBoardingViewController) {
        self.onboardingViewController = onboardingViewController
    }
    
    struct Storyboard {
        static let showLoginScreen = "showLoginScreen"
    }
    
    func shouldContinueUserOnboarding(_ welcomeController: FUIWelcomeController) {
        self.welcomeController = welcomeController
        
        if AppLogonHelper.userCredentialsAreAvailableInTheKeychain() {
            onboardingViewController?.showPasscodeInputScreen();
        } else {
            onboardingViewController?.showLoginScreen();
        }
    }
    
    private func showLoadingIndicator() {
        if let welcomeController = welcomeController {
            if self.loadingIndicator == nil {
                let indicator = FUILoadingIndicatorView(frame: welcomeController.view.frame)
                indicator.text = "Logging in..."
                self.loadingIndicator = indicator
            }
            let indicator = self.loadingIndicator!
            
            DispatchQueue.main.async {
                welcomeController.view.addSubview(indicator)
                indicator.show()
            }
        }
    }
    
    private func hideLoadingIndicator() {
        DispatchQueue.main.async {
            guard let loadingIndicator = self.loadingIndicator else {
                return
            }
            loadingIndicator.dismiss()
            loadingIndicator.removeFromSuperview()
        }
    }
    
}

 

The second delegate we need is the delegate for the FUIPasscodeController. Create a file named PasscodeControllerDelegate in the OnBoarding group and paste the code below into it. 

This delegate handles the events of the FUIPasscodeController which is responsible for creating and evaluating the passcode of an user. The shoudTryPasscode method is called when the user enters a passcode. Depending on whether the user is creating a passcode or not this method stores the user credentials in the keychain or checks if the passcode is matching the stored passcode in the keychain. If the passcode is already stored and matches the passcode in the keychain the delegate performs a basic authentication and navigates the user to the home screen of the application.

//
//  PasscodeControllerDelegate.swift
//  iOS-on-boarding
//
//  Created by Schlegel, Waldemar on 20.07.17.
//  Copyright © 2017 SAP. All rights reserved.
//

import Foundation
import SAPFiori

class PasscodeControllerDelegate: FUIPasscodeControllerDelegate {
    
    private weak var onboardingViewController: OnBoardingViewController?
    
    private weak var passcodeController: SAPFiori.FUIPasscodeController?
    
    private var loadingIndicator: FUILoadingIndicatorView?
    
    public var password: String?
    public var username: String?
    
    init(onboardingViewController: OnBoardingViewController) {
        self.onboardingViewController = onboardingViewController
    }
    
    func shouldTryPasscode(_ passcode: String, forInputMode inputMode: SAPFiori.FUIPasscodeInputMode, fromController passcodeController: SAPFiori.FUIPasscodeController) throws {
        self.passcodeController = passcodeController
        
        if inputMode == .create {
            AppLogonHelper.storeUserCredentials(username: username!, password: password!, passcode: passcode, maxPasscodeRetries: passcodePolicy().retryLimit)
            
            passcodeController.dismiss(animated: true) {
                self.onboardingViewController?.showAppHomeScreen()
            }
            
            return
        }
        else if inputMode == .match {
            guard AppLogonHelper.areThereRemainingPasscodeRetries() else {
                throw FUIPasscodeControllerError.invalidPasscode(code: "Passcode Mismatch.", triesRemaining: 0)
            }
            
            do {
                try AppLogonHelper.tryPasscodeLogon(with: passcode) { [weak self] (username, password) in
                    self?.performBasicAuthentication(username: username, password: password)
                }
            }
            catch AppLogonHelper.AppLogonError.passcodeMismatch(let remainingRetries) {
                throw FUIPasscodeControllerError.invalidPasscode(code: "Passcode Mismatch.", triesRemaining: remainingRetries)
            }
        }
    }
    
    public func didCancelPasscodeEntry(fromController passcodeController: SAPFiori.FUIPasscodeController) {
        passcodeController.dismiss(animated: true, completion: nil)
    }
    
    public func didSkipPasscodeSetup(fromController passcodeController: SAPFiori.FUIPasscodeController) {
        
    }
    
    public func shouldResetPasscode(fromController passcodeController: SAPFiori.FUIPasscodeController) {
        AppLogonHelper.resetUserCredentials()
        passcodeController.dismiss(animated: true, completion: nil)
    }
    
    public func passcodePolicy() -> SAPFiori.FUIPasscodePolicy {
        var policy: FUIPasscodePolicy = FUIPasscodePolicy();
        policy.retryLimit = 3
        
        return policy;
    }
    
    // Mark: - Logon for Online Mode
    
    private func performBasicAuthentication(username: String, password: String) {
        showLoadingIndicator()
        
        ESPMService.shared.performBasicAuthentication(username: username, password: password) { (success, erroMessage) in
            self.hideLoadingIndicator()
            
            if success {
                self.onboardingViewController?.showAppHomeScreen();
            }
            else {
                if let message = erroMessage {
                    self.onboardingViewController?.displayAlert(title: "Logon process failed!", message: message)
                }
            }
        }
    }
    
    // MARK: - Loading Indicator
    
    private func showLoadingIndicator() {
        if let passcodeController = passcodeController {
            if self.loadingIndicator == nil {
                let indicator = FUILoadingIndicatorView(frame: passcodeController.view.frame)
                indicator.text = "Logging in"
                self.loadingIndicator = indicator
            }
            let indicator = self.loadingIndicator!
            
            DispatchQueue.main.async {
                passcodeController.view.addSubview(indicator)
                indicator.show()
            }
        }
    }
    
    private func hideLoadingIndicator() {
        DispatchQueue.main.async {
            guard let loadingIndicator = self.loadingIndicator else {
                return
            }
            loadingIndicator.dismiss()
            loadingIndicator.removeFromSuperview()
        }
    }
}

 

Now we need to create a service class which contains an instance of the ESPMContainerDataAccess class to provide online data access. Also the service class will contain the logic to perform the basic authentication. 

Create a file named ESPMService.swift in the Model group and paste the code below into it.

The performBasicAuthentication method creates an instance of a SAPURLSession and performs an get request against the configured endpoint which is defined in the Constants.swift file. The authentication challenge is handled by the ESPMService instance which acts as SAPURLSessionDelegate. After successful authentication we set the SAPURLSession class attribute to the SAPURLSession we created in the performBasicAuthentication method. This triggers the didSet listener of the urlSession attribute and creates a new instance of the ESPMContainerDataAccess class which we will use for data access.

//
//  ESPMService.swift
//  iOS-on-boarding
//
//  Created by Schlegel, Waldemar on 20.07.17.
//  Copyright © 2017 SAP. All rights reserved.
//

import SAPFiori
import SAPFoundation
import SAPOfflineOData
import SAPOData

class ESPMService: SAPURLSessionDelegate {
    
    static let shared: ESPMService = ESPMService()
    
    private var username: String?
    private var password: String?
    
    public var espmContainer: ESPMContainerDataAccess!
    
    private var urlSession: SAPURLSession! {
        didSet {
            self.espmContainer = ESPMContainerDataAccess(urlSession: urlSession)
        }
    }
    
    private init() {
        
    }
    
    // MARK: - Basic Authentication
    
    func performBasicAuthentication(username: String, password: String, completion: @escaping (Bool, String?) -> Void) {
        self.username = username
        self.password = password
        
        let sapUrlSession = SAPURLSession(delegate: self)
        sapUrlSession.register(SAPcpmsObserver(settingsParameters: Constants.configurationParameters))
        
        var request = URLRequest(url: Constants.appUrl)
        request.httpMethod = "GET"
        
        let dataTask = sapUrlSession.dataTask(with: request) { data, response, error in
            guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
                let message: String
                
                if let error = error {
                    print("Error while basic authentication: \(error)")
                    message = error.localizedDescription
                } else {
                    message = "Check your credentials!"
                }
                
                DispatchQueue.main.async {
                    completion(false, message)
                }
                
                return
            }
            
            print("Response returned: \(HTTPURLResponse.localizedString(forStatusCode: response.statusCode))")
            
            // We should check if we got SAML challenge from the server or not
            if self.isSAMLChallenge(response) {
                print("Logon process failure. It seems you got SAML authentication challenge.")
                
                let message = "Logon process failure. It seems you got SAML authentication challenge."
                
                DispatchQueue.main.async {
                    completion(false, message)
                }
            }
            else {
                print("Logged in successfully.")
                self.urlSession = sapUrlSession
                
                DispatchQueue.main.async {
                    DispatchQueue.main.async {
                        completion(true, nil)
                    }
                }
            }
        }
        
        dataTask.resume()
    }
    
    private func isSAMLChallenge(_ response: HTTPURLResponse) -> Bool {
        return response.statusCode == 200 && ((response.allHeaderFields["com.sap.cloud.security.login"]) != nil)
    }
    
    // MARK: - SAPURLSessionDelegate
    
    func sapURLSession(_ session: SAPURLSession, task: SAPURLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping(SAPURLSession.AuthChallengeDisposition) -> Void) {
        if challenge.previousFailureCount > 0 {
            completionHandler(.performDefaultHandling)
            return
        }
        
        let credential = URLCredential(user: self.username!, password: self.password!, persistence: .forSession)
        completionHandler(.use(credential))
    }
    
}

 

Now we need to extend the OnBoardingViewController class with the missing methods which are used by the PasscodeControllerDelegate and the WelcomeControllerDelegate.

Therefore copy and paste the code below into the OnBoardingViewController.swift file.

The OnBoardingViewController does not contain much logic since it’s main reason is to handle the navigation between the FUIPasscodeController, WelcomeController and the HomeScreen of the application.

After pasting the code you will see an issue in the OnBoardingViewController which says that the protocol BasicAuthViewControllerDelegate is undeclared. We will resolve this issue in the next step.

//
//  OnBoardingViewController.swift
//  iOS-on-boarding
//
//  Created by Schlegel, Waldemar on 19/07/2017.
//  Copyright © 2017 SAP. All rights reserved.
//

import UIKit
import SAPFiori
import LocalAuthentication

class OnBoardingViewController: UIViewController, FUIWelcomeControllerDelegate, BasicAuthViewControllerDelegate, Notifier {
    
    struct Storyboard {
        static let showFUIWelcomeScreen = "showFUIWelcomeScreen"
        static let showLoginScreen = "showLoginScreen"
        static let showCreatePasscodeScreen = "showCreatePasscodeScreen"
        static let showFUIPasscodeInput = "showFUIPasscodeInput"
        static let split = "Split"
    }
    
    private var passcodeDelegate: PasscodeControllerDelegate!
    private var welcomeDelegate: WelcomeControllerDelegate!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        welcomeDelegate = WelcomeControllerDelegate(onboardingViewController: self)
        passcodeDelegate = PasscodeControllerDelegate(onboardingViewController: self)
        
        self.performSegue(withIdentifier: Storyboard.showFUIWelcomeScreen, sender: self)
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    // MARK: - BasicAuthViewControllerListener
    
    func onLoginSuccess(username: String, password: String) {
        passcodeDelegate.password = password
        passcodeDelegate.username = username
        
        self.performSegue(withIdentifier: Storyboard.showCreatePasscodeScreen, sender: self)
    }
    
    // MARK: - Navigation
    
    func showLoginScreen() {
        self.performSegue(withIdentifier: Storyboard.showLoginScreen, sender: self)
    }
    
    func showAppHomeScreen() {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let splitView = storyboard.instantiateViewController(withIdentifier: Storyboard.split) as! UISplitViewController
        let appDelegate = (UIApplication.shared.delegate as! AppDelegate)
        
        splitView.delegate = appDelegate
        appDelegate.window!.rootViewController = splitView
    }
    
    func showPasscodeInputScreen() {
        self.performSegue(withIdentifier: Storyboard.showFUIPasscodeInput, sender: self)
    }
    
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == Storyboard.showFUIWelcomeScreen {
            let vc = segue.destination as! FUIWelcomeScreen
            vc.state = .isConfigured
            vc.delegate = welcomeDelegate
            vc.isDemoAvailable = false
            vc.primaryActionButton.setTitle("Login", for: .normal)
            
            vc.detailLabel.text = "SAP Cloud Platform iOS SDK On Boarding Sample Application"
        }
        else if segue.identifier == Storyboard.showLoginScreen {
            let nvc = segue.destination as! UINavigationController
            let vc = nvc.visibleViewController as! BasicAuthViewController
            vc.setBasicAuthDelegate(self);
        }
        else if segue.identifier == Storyboard.showCreatePasscodeScreen {
            let nvc = segue.destination as! UINavigationController
            let vc = nvc.visibleViewController as! FUIPasscodeCreateController
            vc.canEnableTouchID = true;
            
            vc.delegate = passcodeDelegate
        }
        else if segue.identifier == Storyboard.showFUIPasscodeInput {
            let nvc = segue.destination as! UINavigationController
            let vc = nvc.visibleViewController as! FUIPasscodeInputController
            vc.isToShowCancelBarItem = true
            
            vc.delegate = passcodeDelegate
        }
    }
    
}

 

To resolve the issue with the undeclared BasicAuthViewControllerDelegate we need to adjust the BasicAuthViewController.swift file. Therefore we need to create the BasicAuthViewControllerDelegate protocol and add a method to register a delegate of this type.

You can find the updated code of the BasicAuthViewController.swift file below. Just copy and paste it into yours.

//
// BasicAuthViewController.swift
// iOS-on-boarding
//
// Created by SAP Cloud Platform SDK for iOS Assistant application on 18/07/17
//

import SAPFiori
import SAPFoundation
import SAPCommon

protocol BasicAuthViewControllerDelegate {
    func onLoginSuccess(username: String, password: String)
}

class BasicAuthViewController: UIViewController, UITextFieldDelegate, Notifier, LoadingIndicator {
    
    var loadingIndicator: FUILoadingIndicatorView?
    
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet var usernameTextField: UITextField!
    @IBOutlet var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!
    
    var activeTextField: UITextField?
    
    private var basicAuthListener: BasicAuthViewControllerDelegate?
    
    private var espmService = ESPMService.shared
    
    // MARK: - Actions
    
    @IBAction func onCancelLogin(_ sender: UIBarButtonItem) {
        dismiss(animated: true, completion: nil)
    }
    
    @IBAction func loginButtonTapped(_ sender: AnyObject) {
        
        // Validate
        if (self.usernameTextField.text!.isEmpty || self.passwordTextField.text!.isEmpty) {
            displayAlert(title: NSLocalizedString("keyErrorLoginTitle", value: "Error", comment: "XTIT: Title of alert message about login failure."),
                         message: NSLocalizedString("keyErrorLoginBody", value: "Username or Password is missing", comment: "XMSG: Body of alert message about login failure."))
            return
        }
        
        self.showIndicator()
        self.loginButton.isEnabled = false
        
        espmService.performBasicAuthentication(username: self.usernameTextField.text!, password: self.passwordTextField.text!) { (success, message) in
            
            self.hideIndicator()
            
            if success {
                self.dismiss(animated: true) {
                    if let basicAuthListener = self.basicAuthListener {
                        basicAuthListener.onLoginSuccess(username: self.usernameTextField.text!, password: self.passwordTextField.text!)
                    }
                }
            }
            else {
                if let message = message {
                    self.displayAlert(title: NSLocalizedString("keyErrorLogonProcessFailedNoResponseTitle", value: "Logon process failed!", comment: "XTIT: Title of alert message about logon process failure."),
                                      message: message)
                }
                
                self.loginButton.isEnabled = true
            }
        }
    }
    
    //MARK: - Controller Lifecycle
    
    override func viewDidLoad() {
        self.usernameTextField.delegate = self
        self.passwordTextField.delegate = self
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }
    
    // MARK: - BasicAuthViewControllerListener
    
    func setBasicAuthDelegate(_ listener: BasicAuthViewControllerDelegate) {
        self.basicAuthListener = listener
    }
    
    override func viewWillAppear(_ animated: Bool) {
        // Notification for keyboard show/hide
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    }
    
    // MARK: Notification and TextField
    
    // Shrink Table if keyboard show notification comes
    func keyboardWillShow(notification: NSNotification) {
        self.scrollView.isScrollEnabled = true
        if let info = notification.userInfo, let keyboardSize = (info[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size {
            // Need to calculate keyboard exact size due to Apple suggestions
            let contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0)
            
            self.scrollView.contentInset = contentInsets
            self.scrollView.scrollIndicatorInsets = contentInsets
            
            var aRect = self.view.frame
            aRect.size.height -= keyboardSize.height
            if let activeField = self.activeTextField, (!self.view.frame.contains(activeField.frame.origin)) {
                let scrollPoint = CGPoint(x: 0, y: activeField.frame.origin.y - keyboardSize.height)
                self.scrollView.scrollRectToVisible(activeField.frame, animated: true)
                self.scrollView.setContentOffset(scrollPoint, animated: true)
            }
        }
    }
    
    // Resize Table if keyboard hide notification comes
    func keyboardWillHide(notification: NSNotification) {
        // Once keyboard disappears, restore original positions
        let contentInsets = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0)
        self.scrollView.contentInset = contentInsets
        self.scrollView.scrollIndicatorInsets = contentInsets
        self.scrollView.isScrollEnabled = false
    }
    
    func textFieldDidBeginEditing(_ textField: UITextField) {
        self.activeTextField = textField
    }
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        self.activeTextField = nil
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        self.activeTextField?.resignFirstResponder()
        return true
    }
    
}

 

Since we have changed the app flow we need to adjust the AppDelegate class which is currently responsible for the navigation during the application start, log upload and the registration for push messages. Also the the AppDelegate provides an instance of the ESPMContainerDataAccess class which is used by the view controllers to fetch data from the OData service. Since we handle the initial navigation with the storyboard and the only thing we are interested in is to provide an instance of the ESPMContainerDataAccess class we can simplify the AppDelegate a lot.

Replace you AppDelegate implementation with the code below.

//
// AppDelegate.swift
// iOS-on-boarding
//
// Created by SAP Cloud Platform SDK for iOS Assistant application on 18/07/17
//

import SAPFiori
import SAPFoundation
import SAPCommon
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?

    private let logger = Logger.shared(named: "AppDelegateLogger")
    var espmContainer: ESPMContainerDataAccess! {
        get {
            return ESPMService.shared.espmContainer
        }
    }

    func applicationDidFinishLaunching(_ application: UIApplication) {
        UINavigationBar.applyFioriStyle()
    }
    
    // MARK: - Split view
    
    func splitViewController(_ splitViewController: UISplitViewController, collapseSecondary secondaryViewController: UIViewController, onto primaryViewController: UIViewController) -> Bool {
        guard let secondaryAsNavController = secondaryViewController as? UINavigationController else { return false }
        guard let topAsMasterController = secondaryAsNavController.topViewController as? MasterViewController else { return false }
        // Without this, on iPhone the main screen is the detailview and the masterview can not be reached.
        if (topAsMasterController.collectionType == .none) {
            // Return true to indicate that we have handled the collapse by doing nothing; the secondary controller will be discarded.
            return true
        }
        
        return false
    }
    
}

 

The onboarding flow is finished now and you can run the application.

Assigned Tags

      3 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo prasad badal
      prasad badal

      Waldemar 

      Thanks for the awesome post .a couple of things.

      tried following it to the t. but im getting  error after succesful login. do i need to download a more current version of the framework .

      i generate the code with SAP_CP_for_iOS_DMG_release_1_2_9_Xcode9 version

      the most recent version today is SAP_CP_for_iOS_DMG_release_2_0_0_Xcode9

       

       

      'NSInvalidArgumentException', reason: 'Could not find a storyboard named 'FUIPasscodeCreateControllerStoryBoard' in bundle NSBundle </var/containers/Bundle/Application/067CCA39-B8D7-4FB3-8DBE-CC6153D185BB/ios-onboarding.app/Frameworks/SAPFiori.framework> (loaded)'

       

      also the segue should  from onBoardingViewController to the NavigationController not the login screen. the screen shot does show that but please mention it in the text

       

      thanks

       

      Prasad Badal

      Author's profile photo prasad badal
      prasad badal

      Waldemar 

      Please ignore earlier post. Thanks  again for the awesome post . worked like a charm after a few hiccups mainly due to me being a novice swift programmer  & you missed a couple of ':'s in the storyboard reference ids

       

      Thanks

       

      Prasad Badal

       

       

       

      thanks

       

      Prasad Badal

       

      Author's profile photo Andreas Schlosser
      Andreas Schlosser

      Just because I've been pointed at this - I want to mention that what is described here still works, but I'd recommend anybody interested in authentication and onboarding flows to check out the SAPFioriFlows framework and the automated onboarding that we've built by now. Simply use a recent Assistant and generate an app and you'll see the whole thing 'just work', incl. bootstrapping via Discovery Service, any authentication method supported, tokens persisted securely, Touch ID or Face ID protection.