Back to Getting Started With Kapsel

Appendix F: Using OAuth with Kapsel

With OAuth and SAML, the application does not itself request your user name and password but redirects to a service such as SAP Cloud Identity (or for more info Identity Authentication) which does. The application does not receive your credentials but an access token which is valid for a specified period and which can be revoked. In theory multiple different applications would use this service to authenticate users and limit what they are authorized to do. In this scenario, one common user name and password could be used across multiple applications which simplifies things for the user.

The following are some additional links on OAuth.
OAuth2 and Google
https://www.youtube.com/watch?v=YLHyeSuBspI
https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2
Configuring OAuth Authentication
Protecting Applications with OAuth 2.0

The following sections demonstrate using OAuth in a Kapsel app.
Registering using OAuth with HCPms
Client Certificates and OAuth
Handling Session Timeout
Offline Considerations

Registering using OAuth with HCPms

The following steps demonstrate how to configure the Logon example to use OAuth as the authentication provider for the application.

  • Using the HANA Mobile service cockpit, modify the Security Configuration of the application com.kapsel.gs from Basic to OAuth.
  • Under OAuth Settings, click on Add and then save.
    Note the access token value can be very short such as 1 minute as it is used to set up an SMP or HCPms session which by default lasts 20 minutes. The refresh token lifetime value can be much longer. The user will need to re-enter and authorize once this lifetime value expires.
  • On the Backend tab, delete the SSO provider and add a new Basic Authentication provider but this time specify the user name and password required to access the OData service.
    https://sapes4.sapdevcenter.com/sap/opu/odata/IWFND/RMTSAMPLEFLIGHT.
  • Modify the context variable in LogonDemo\www\serverContext.js to indicate that OAuth should be used. ┬áNote the values below such as authorizationEndpoint, tokenEndpoint and clientID can be copied from the HCPms cockpit.
    "auth": [{
        "type": "oauth2",
        "config": {
            "oauth2.authorizationEndpoint": "https://oauthasservices-i82xxxxtrial.hanatrial.ondemand.com/oauth2/api/v1/authorize",
            "oauth2.tokenEndpoint": "https://oauthasservices-i82xxxxtrial.hanatrial.ondemand.com/oauth2/api/v1/token",
            "oauth2.clientID": "1d04af3b-8a72-4cd6-9737-143d97d13b3c",
            "oauth2.grantType": "authorization_code"
        }
    }],

    Note, a problem was seen during the OAuth registration flow if the following setting wasn’t included in the context variable.

    "communicatorId": "REST",
    
  • Add the settings plugin so that the user name of the authenticated user can be queried.
    cordova plugin add kapsel-plugin-settings --searchpath %KAPSEL_HOME%/plugins
    or 
    cordova plugin add kapsel-plugin-settings --searchpath $KAPSEL_HOME/plugins
  • Modify the index.html file and add the following button.
    <button onclick="sap.Settings.getConfigProperty(function(value) {alert(value)}, errorCallback, 'UserName')">Get User Name</button>

    Note that in SAML or OAuth registration, the user field returned from the registrationContext is empty so this is an alternative way for your application to know the currently registered user.

  • Prepare, build and deploy the app with the following command.
    cordova run android
    or
    cordova run ios
    or
    cordova run windows -- --archs=x64


    Note, if the Remember me checkbox is checked, a cookie will be set that will remain valid for three months so the user name and password will not need to be re-entered.


    Notice above that the above screen can be customized in the SAP Cloud Platform.

  • Also note that it is possible to revoke access to a previously granted Access Token.

Client Certificates and OAuth

Android

If the client is presented with a certificate challenge the following screens will appear to allow the user to register with an installed certificate.

This screen will appear if the user has not configured a PIN or password on their device.

If you do not wish the user to see this screen, handling of client certificate challenges can be disabled by the following setting in the config.xml.

<preference name="SAPKapselHandleX509Challenges" value="false" />

iOS

Note that certificates installed on the system store (Settings > General > Profiles & Device Management) on iOS are not available to the app. For further information see Making Certificates and Keys Available To Your App. One technique is to use a Mobile Device Management (MDM) software such as SAP Afaria or Mobile Iron to provision the user certificate into the app keychain or a shared keychain.

The below is a simplified example that uses an app named KapselGSCert to provision a certificate into an app’s keychain. This would normally be done by MDM software.

  • Modify the com.kapsel.gs application on an SMP server to use an X.509 certificate authentication scheme.
  • Create an application that will load a certificate into the app’s keychain. This app’s keychain will be shared with all other apps that wish to use the user’s certificate that has been loaded into the app.
    cordova -d create ~/Documents/Kapsel_Projects/KapselGSCert com.kapsel.gscert KapselGSCert
    cd KapselGSCert
    cordova platform add ios
    cordova plugin add kapsel-plugin-authproxy --searchpath $KAPSEL_HOME/plugins
    
  • Replace the contents of the index.html with following contents.
    <!DOCTYPE html>
    <html>
        <head>
            <script type="text/javascript" charset="utf-8" src="cordova.js"></script>
            <script>
                window.onerror = onError;
                var certCommonName = "user1";
                var certFileName = "user1.p12";
                var certPassword = "mypassword";
                var hostURL = "https://ykfn00562959a.amer.global.corp.sap:8082/odata/applications/latest/com.kapsel.gs/$metadata";
                
                function onError(msg, url, line) {
                    var idx = url.lastIndexOf("/");
                    var file = "unknown";
                    if (idx > -1) {
                        file = url.substring(idx + 1);
                    }
                    alert("An error occurred in " + file + " (at line # " + line + "): " + msg);
                    return false; //suppressErrorAlert;
                }
            
                function init() {
                    updateStatus2("EventLogging: deviceready called");
                    if (sap.Logger) {
                        sap.Logger.setLogLevel(sap.Logger.DEBUG);  //enables the display of debug log messages from the Kapsel plugins.
                        sap.Logger.debug("EventLogging: Log level set to DEBUG");
                    }
                    showScreen("MainDiv");
                    console.log("EventLogging: init completed");
                }
                
                function errorCallback(e) {
                    alert("An error occurred: " + JSON.stringify(e));
                    updateStatus2("An error occurred");
                }
                
                function showScreen(screenIDToShow) {
                    var screenToShow = document.getElementById(screenIDToShow);
                    screenToShow.style.display = "block";
                    var screens = document.getElementsByClassName('screenDiv');
                    for (var i = 0; i < screens.length; i++) {
                        if (screens[i].id != screenToShow.id) {
                            screens[i].style.display = "none";
                        }
                    }
                }
                
                function updateStatus2(msg) {
                    var d = new Date();
                    document.getElementById('statusID2').innerHTML = msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds());
                    console.log("EventLogging: " + msg + " at " + addZero(d.getHours()) + ":" + addZero(d.getMinutes()) + "." + addZero(d.getSeconds()));
                }
                
                function addZero(input) {
                    if (input < 10) {
                        return "0" + input;
                    }
                    return input;
                }
                
                function sendRequestSuccessCB(serverResponse) {
                    updateStatus2("Certificate loaded into keychain");
                    console.log(JSON.stringify(serverResponse));
                }
                
                function deleteCertSuccessCB(serverResponse) {
                    updateStatus2("Certificate deleted from keychain");
                    console.log(JSON.stringify(serverResponse));
                }
                
                function deleteCert() {
                    sap.AuthProxy.deleteCertificateFromStore(deleteCertSuccessCB, errorCallback, certCommonName);
                }
                
                //Attempts to load a local provided cert into the apps keychain
                //once the cert is loaded into the iOS keychain it can be used during the SAML/OAuth registration but not SMP/HCPms registration
                function loadCert() {
                    var clientCert = new sap.AuthProxy.CertificateFromFile(certFileName, certPassword, certCommonName);
                    sap.AuthProxy.sendRequest("GET", hostURL, null /*header*/, null /*body*/, sendRequestSuccessCB, errorCallback, null/*user*/, null/*password*/, 10, clientCert);
                }
                
                document.addEventListener("deviceready", init, false);
                
            </script>
            
        </head>
        <body onunload="onUnload()" onbeforeunload="onBeforeUnload()">
            <div class="screenDiv" id="LoadingDiv">
                <h1>Loading ...</h1>
            </div>
            
            
            
            <div class="screenDiv" id="MainDiv" style="display: none">
                <h1>Logon Sample</h1>
                <button id="loadCert" onclick="loadCert()">Load Cert into KeyChain</button>
                <button id="deleteCert" onclick="deleteCert()">Delete Cert from KeyChain</button>
                <br>
                <span id="statusID2"></span>
            </div>
        </body>
    </html>
    
  • Add the cert into the Xcode app by right-clicking on the Resource folder and select the menu item Add Files to “KapselGSCert”.
  • Run the app and press the Load Cert into KeyChain button to add the certificate to the KapselGSCert app’s keychain.
    cordova run ios

  • Modify the original application, com.kapsel.gs, that wishes to use a certificate during an OAuth or SAML registration such as the com.kapsel.gs app and add com.kapsel.gscert to the Keychain Sharing.
  • Run the com.kapsel.gs app that now has access to the com.kapsel.gscert app.
    cordova run ios

    Then try the OAuth registration again and notice this time that the certificate from the shared keychain can be used.

  • To summarize, if there are no certificates in the shared keychain the certificate dialog will not appear.

    If there is only one certificate and the following method is called in the deviceready event, the certificate will be used automatically.

    sap.AuthProxy.setAutoSelectCertificateConfig(successCB, errorCB, true);

    This setting is also available in the config.xml as of SP 15.

    <preference name="SAPAutoSelectSingleCertificate" value="true" />
    

Windows

On windows, if you do not wish the app to use a client certificate, remove the Shared User Certificates capability as shown below.

Handling Session Timeout

A session is maintained between the client and the HCPms or SMP server. By default it times out after 20 minutes of inactivity. See also Authentication and Handling Session Timeout.
When the application starts an attempt is made to refresh the session. This behavior can be disabled via the setting refreshOAUTHSessionOnResume which is discussed further in the Offline Considerations.

If the WebView makes a request such as to open a page proxied through the HCPms server and the session has timed out, the HTTP result is 400 Bad Request and the response contains the below header.

OAuthCheckOnServer: Bearer token for OAuth is missing

This differs from a SAML response in which a redirect is part of the response.
To simulate this add the following code to index.html.

function logout() {
    updateStatus2("Calling logout");
    var sUrl = "https://" + applicationContext.registrationContext.serverHost + ":" + applicationContext.registrationContext.serverPort + "/mobileservices/sessions/logout";
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
        console.log(xmlhttp.responseText);
        if (xmlhttp.readyState == 4) {
            updateStatus2("Successfully Logged Out");
            console.log("SUCCESS " + xmlhttp.responseText + " " + xmlhttp.status)
        }
    }
    
    xmlhttp.open("POST", sUrl, true);
    xmlhttp.setRequestHeader("Content-Type", "application/json");
    xmlhttp.send();
}

function refreshSessionSuccessCallback() {
     updateStatus2("Session refreshed");
}

function refreshOAuthSession() {
    updateStatus2("Calling refreshSession");
    sap.Logon.refreshSession(refreshSessionSuccessCallback, logonErrorCallback);
}

Then near the bottom of the index.html file add the following two buttons.

<button onclick="logout()">Logout</button>
<button onclick="refreshOAuthSession()">Refresh Session</button>

Deploy and run the modified app. Notice that it is now possible to end the session between the app and the HCPms server by pressing the Logout button. If a read request is made after the session has been terminated, the HCPms server responds with a 400 Bad Request error. Note, the session between the app and the HCPms server can also be terminated by removing the app from memory.

Add the below code to the top of the errorCallback method to handle this case.

function errorCallback(e) {
    if (e.hasOwnProperty("response")) {
        if (e.response.hasOwnProperty("headers")) {
            if (e.response.headers.hasOwnProperty("X-SMP-AUTHENTICATION-STATUS-MESSAGE") && e.response.headers["X-SMP-AUTHENTICATION-STATUS-MESSAGE"] == "Bearer token for OAuth2 is missing") {
                alert("OAuth session has expired.  Please retry the operation after the session has been re-established.");
                sap.Logon.refreshSession(refreshSessionSuccessCallback, errorCallback);
                return;
            }
        }
    }

Offline Considerations

When the application starts an attempt is made to refresh the token. For an offline app that opens when there is no network connectivity this request will fail.
To disable this refresh attempt on startup add the following value to the serverContext.js file.

"refreshOAUTHSessionOnResume":"skip",  //skip, always(default)

This indicates that after the initial registration, it is up to the application to renew the OAuth session by calling sap.Logon.refreshSession method before sending the first server request. In order for this setting to take effect, you may need to unregister and re-register if a previous registration exists.

Back to Getting Started With Kapsel

To report this post you need to login first.

Be the first to leave a comment

You must be Logged on to comment or reply to a post.

Leave a Reply