Tutorial: External App Service

External web services may be integrated in myApps the same as services running on an App Platform. The minimum requirement is, that these should make use of the PBX authentication. This tutorial explains how to integrate the PBX authentication in such a web service.

For demonstration purposes we implement a simple HTTP GET handler in our NewApp1 project to simulate the external web service. This functionality can then be transfered to the external web service to whatever technology (PHP, Go, Java, ...) is used there.

The tutorial is based on a newly created innovaphone App with the Visual Studio plugin.

The used file and class names in this example are based on a newly created App with the name NewApp1 and the company name innovaphone.

Simulation of external Web Service

To simulate the external web service, we add a file webservice.htm, to the apps folder containing a very simple page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Webservice</title>
</head>

<body>
    <h1>Webservice</h1>
</body>
</html>

To add this file to the static files served by the App, we add the line

    $(APPWEBPATH)/webservice.htm \
to the statement
APPWEBSRC_ZIP +=
in apps.mak

You can now build and run

You should be able to open the webservice.htm with your browser from the URL http://<AP>/newapp1/webservice.htm

Simulation of Authentication Checking on the External Web Service

The authentication works with a shared secret between the PBX App object and the web service. In this example we use a static string "secret" for this.

For the login the App should first read a "challenge" from the webservice. This should be a random string wich is used to create unique login requests, so that a replay attack is not possible. This challenge is forwarded to the PBX.

The PBX uses this challenge to create a login request. This login request contains

app
The filename which is configure in the URL of the PBX App object. This identifies the App Service addressed and can thus be used to determine righs associated with the user or licensing.
domain
The domain of the PBX
sip
SIP URI user part of the user who logs in.
guid
The guid of the user
dn
The display name of the user
info
An optional Json data structure containing more information.
digest
A SHA256 hash calculated over
<app>:<domain>:<sip>:<guid>:<dn>:<info>:<challenge>:<secret>

The web service can verify the login by calculating the same hash using the shared secret and verifying that the result is the same.

We now add code the the NewApp1, which implements the handling of GET requests to read the challenge and to verify the login

Implement GET handler

Add the Login Page to the Web Service

For the login page we add a file login.htm, to the apps folder containing the login code:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Webservice</title>
    <script type="text/javascript" src="phpsession.js"></script>
    <script type="text/javascript">
        function start() {
            var args = location.search.split("&");
            if (args[0].startsWith("?")) args[0] = args[0].slice(1);
            var name = args.find(function (e) { return e.startsWith("name=") });
            if (name) {
                new innovaphone.PhpSession("login", name.split("=")[1], "webservice.htm", "");
            }
        }
    </script>
</head>

<body onload="start()">
    <h1>Login</h1>
</body>
</html>

This page requires a simple self-contained library phpsession.js, which is also found as innovaphone.phpsession.js in web1/phpsession. For your web service you can copy the file and change the path in login.htm accordingly. It does the HTTP GET requests to the web service and the forwarding to the myApps client, which does the communication to the PBX.

We add a file phpsession.js, to the apps folder containing the phpsession code:

var innovaphone = innovaphone || {};
innovaphone.PhpSession = innovaphone.PhpSession || function (url, app, appStart, params) {
    var instance = this,
        url = url,
        challenge,
        timer;

    window.addEventListener('message', onpostmessage);
    login();


    function login() {
        httpGet(url + "?mt=AppChallenge", onchallenge);
        timer = setTimeout(login, 10000);
    }

    function onchallenge(text) {
        var obj = JSON.parse(text);
        if (obj && obj.mt) {
            challenge = obj.challenge;
            if (obj.mt == "AppChallengeResult") {
                window.parent.postMessage(JSON.stringify({ mt: "getLogin", app: app, challenge: obj.challenge }), "*");
            }
        }
    }

    function onpostmessage(e) {
        var obj = JSON.parse(e.data);
        if (obj.mt && obj.mt == "Login") {
            console.log(app + ": AppLogin(" + obj.sip + "@" + obj.domain + ")");
            httpGet(url + "?mt=AppLogin&app=" + encodeURIComponent(obj.app) +
                          "&domain=" + encodeURIComponent(obj.domain) +
                          "&sip=" + encodeURIComponent(obj.sip) +
                          "&guid=" + encodeURIComponent(obj.guid) +
                          "&dn=" + encodeURIComponent(obj.dn) +
                          (obj.info ? "&info=" + encodeURIComponent(JSON.stringify(obj.info)) : "") +
                          "&digest=" + encodeURIComponent(obj.digest) +
                          "&challenge=" + encodeURIComponent(challenge), onlogin);
        }
    }

    function onlogin(text) {
        var obj = JSON.parse(text);
        if (obj && obj.mt && obj.mt == "AppLoginResult" && obj.ok) {
            var u = location.href.substring(0, location.href.lastIndexOf("/") + 1);
            location.href = u + appStart + params;
            clearTimeout(timer);
        }
    }

    function httpGet(url, funcComplete, funcFailed) {
        var xmlReq = new window.XMLHttpRequest();
        if (xmlReq) {
            xmlReq.open("GET", url, funcComplete ? true : false);
            xmlReq.send(null);
            if (funcComplete) {
                xmlReq.onreadystatechange = function () {
                    if (this.readyState == 4) {
                        if (this.status == 200) {
                            funcComplete(this.responseText, this.responseXML);
                        }
                        else {
                            if (funcFailed) funcFailed(this);
                            else funcComplete("{}");
                        }
                    }
                }
            }
        }
        return xmlReq;
    }
};

To add these file to the static files served by the App, we add the lines

    $(APPWEBPATH)/login.htm \
    $(APPWEBPATH)/phpsession.js \
to the statement
APPWEBSRC_ZIP +=
in apps.mak

The first argument of the innovaphone.PhpSession constructor is the filename for the HTTP GET requests used for authentication to the web service.

The minnovaphone myApps client opens this file, when the URL is provided in an App object. It uses a URL argument "name" to identify the name of the App object. This name is passed as argument to the innovaphone.PhpSession() constructor.

The third and fourth argument for innovaphone.PhpSession are the filename of the web service to be opened and any URL arguments, which should be added.

If you execute the newly built App now, you should be able to open the URL http://<AP>/newapp1/login.htm

Add the App to the PBX

To add the App to the PBX, you have to create an App object for your webservice. Use the Advanced user interface to create an App object with the following properties:

Type
App
Long Name
Webservice
Name
webservice
Password
secret
App/URL
http://<AP>/newapp1/login.htm

If you now grant a user the right to access this App, by checking "webservice" on the Apps tab in the Advanced user interface when opening the user object, the user should find the Webservice App in All Apps. In your Webservice you can add a icon for myApps by putting a 50x50px png file at the same place as login.htm with the name login.png. Of course you can use any name for these files instead of login.