Non-Interactive REST Access

In this article

    How can applications do server-to server communications using SuperOffice REST APIs?

    While applications hosted in a SuperOffice web panel are better off using one of the supported OpenID Connect flows, for authenticating and providing access to users, there isn't [yet] a seamless REST-ful workflow for non-interactive service-type applications.

    It is interesting to note that, once a user interactively signs into SuperID using an OAuth flow, a users refresh token can be stored and used at a later time to non-interactively invoke web services, simulating an impersonated context.

    For typical server-to-server communications using a system user context, applications must use the SOAP based System User authentication workflow. This workflow gets system-level access by exchanging for a system user token (magic string) for a system user ticket. The application can then use the system user ticket to invoke SuperOffice RESTful service APIs.

    So what does the system user workflow look like? First and foremost, the application must have already been authenticated and approved by an administrative user. Once approved, the admin user is redirected (via a POST) to the applications' redirect URL with a security token. The secirity token is either a JSON Web Token (JWT) or a Security Assertion Markup Language (SAML) token, and contains several claims including the tenant id, system user token, username, netserver_url (SOAP) and webapi_url (REST)

    Using the system user token claim, an application invokes the PartnerSystemUserService SOAP endpoint to exchange the system user token for another security token which contains a system user ticket claim; the value of which is a system user ticket.

    Although it states the response is a SAML token, JWT's are also an option. In both cases, the response token must be validated and the claims within the security token response include:

    1. associate id
    2. identity provider
    3. email address
    4. tenant id
    5. database serial number
    6. netserver url (SOAP)
    7. webapi url (REST)
    8. system user ticket

    Starting from the beginning, let's review the fundamentals:

    1. A customer tenant administrator must approve your application. This is done by authenticating an admin user using one of the following urls. This is the same URL used by the Install button in the App Store.

      // Old Form: Returns SAML or JWT with claims, including:
      // admin user ticket, system user token
      http://sod.superoffice.com/login/?app_id=YOUR-APP-ID&redirect_url=YOUR-REDIRECT-URL​
      or
      // OAuth (preferred) Returns admin user access_token and id_token 
      // id_token includes system user token claim, and more
      https://sod.superoffice.com/login/common/oauth/authorize?response_type=id_token token
      &client_id=YOUR-APP-ID&redirect_uri=YOUR-REDIRECT-URL&scope=openid&state=12345
      &nonce=7362CAEA-9CA5-4B43-9BA3-34D7C303EBA7​


    2. The administrator must login (if not already logged in) and give consent/approve that your application can access his installation via web services.

    3. Once approval is giving, the administrator is sent (via a POST) to your redirect URL. What is contained in the request depends on the URL used. With the old URL form, the request contains either a JSON Web Token (JWT) or a Security Assertion Markup Language (SAML) token. The OAuth URL form POST request will contain, among other things, an access_token and id_token.

    4. It's up to the application to 1) validate the token, 2) extract the claims and 3) store the claims information in your application in a multi-tenant fashion and use it as needed.

    5. Using the system user token claim, invoke the PartnerSystemUserService SOAP endpoint to exchange the system user token for a security token that contains a system user ticket claim. The PartnerSystemUserService WSDL is available for download here. The endpoint location is https://{environment}.superoffice.com/Login/services/PartnerSystemUserService.svc.

    The last point, #5, is described in better detail in the System User example article. Once the system user ticket is obtained, the application can begin to call the REST service endpoints with the appropriate headers.

    In contrast to the interactive workflow, where the Authorization header uses "Bearer [access_token]", the non-interactive workflow requires the Authorization header use SOTicket instead of Bearer, followed by the ticket value. The ApplicationToken header must also be specified as SO-AppToken with the application token value provided when the application was registered.

    Authorization: SOTicket System_User_Ticket_Value
    SO-AppToken: Application_Token_Value

    The same System User ticket rules apply as in the SOAP scenario - meaning that the ticket is only good for a short period of time and should be periodically renewed. Recommended updates interval is at least every hour.

    That's it! Now with a system user ticket, use it to perform REST API calls.

    GET /Cust26759/api/v1/MDOList/category?flat=True HTTP/1.1
    Host: sod.superoffice.com
    accept: application/json
    authorization: SOTICKET 7T:MAA3AGYAMQBlAGQAZAAxAGQAZQAwADgAYgAxAGEAYwBkADYAOAA0ADcAMQA2ADkAOQBhADQAZgBiADMAOQAyADsAMgAwADgANwAzADEANQAwADQAMwA7AEMAdQBzAHQAMgA2ADcANQA5AA==
    accept-language: en
    SO-AppToken: f2398a3a7wer3759d4b220e9a9c94321
    

    System User Token => System User Ticket
    (NodeJS Example)

    Once the application has the system user token, it's easy to exchange it for a system user ticket. Below is a NodeJS example that demonstrates how to invoke the web service endpoint.

    What this code does is:

    1.  Read partner private PEM file
    2. Generates a signed system user token
    3. Send SOAP request to SuperOffice
    4. Convert XML response to JSON and obtain the JWT
    5. Validate JWT token.
    6. Extract the System User Ticket

    There is an extensive amount of logging to the console to see the output from each step, and the last output is the system user ticket.

    package.json file:

    {
      "name": "partnersystemuserjs",
      "version": "0.0.1",
      "description": "Exchange system user token for system user ticket.",
      "main": "index.js",
      "scripts": {
        "start": "node index.js"
      },
      "author": "Tony Yates",
      "license": "MIT",
      "dependencies": {
        "crypto": "^1.0.1",
        "jsonwebtoken": "^8.4.0",
        "moment": "^2.22.2",
        "request": "^2.88.0",
        "xml2js": "^0.4.19"
      }
    }

    index.js code:

    const crypto    = require('crypto');
    const moment    = require('moment');
    const fs        = require('fs');
    const request   = require('request');
    const jwt       = require('jsonwebtoken');
    const xml2js    = require('xml2js');
    
    // Partner Application Token (AKA Client Secret)
    const appToken    = 'YOUR_APPLICATION_TOKEN_GOES_HERE';
    
    // Your Online Sandbox Customer Identifier
    const contextId   = 'Cust12345';
    
    // SystemUserToken provided as a claim in the callback (Redirect URL)
    // when a tenant administrator successfully signs into SuperID.
    const systemToken = 'YOUR_SYSTEM_USER_TOKEN_GOES_HERE';
    
    // Partners private key
    // http://www.platanus.cz/blog/converting-rsa-xml-key-to-pem
    const privKeyFile = 'privatekey.pem';
    
    // SuperOffice public key (SOD)
    // included in SuperOfficeOnlineCertificates.zip (in Development folder)
    // http://community.superoffice.com/online-sdk-downloads
    const publKeyFile = 'SuperOfficeFederatedLogin.pem';
    
    /*
        WHAT THIS CODE DOES:
        ----------------------------------------------------
        1. Read partner private PEM file
        2. Generates a signed system user token
        3. Send SOAP request to SuperOffice
        4. Convert XML response to JSON and obtain the JWT
        6. Validate JWT token
        7. Extract the System User Ticket
    
        There is an extensive amount of logging to the console
        to see the output from each step.
    */
    
    fs.readFile(privKeyFile, 'utf8', function (err, rsaPrivateKey) {
    
        if (err) {
            return console.log(err);
        }
    
        console.log(rsaPrivateKey);
    
        const utcTimestamp = moment.utc().format('YYYYMMDDHHmm');
        const data = `${systemToken}.${utcTimestamp}`;
    
        console.log('');
        console.log('Token.Time: ' + data);
    
        let sign = crypto.createSign('SHA256');
        sign.update(data);
        sign.end();
    
        sign = sign.sign(rsaPrivateKey, 'base64');
        const signedToken = `${data}.${sign}`;
    
        console.log('');
        console.log('Signed Token: ' + signedToken);
    
        var soapEnvelope = `<?xml version="1.0" encoding="UTF-8"?>
        <SOAP-ENV:Envelope xmlns:ns0="http://schemas.xmlsoap.org/soap/envelope/"
                                            xmlns:ns1="http://www.superoffice.com/superid/partnersystemuser/0.1"
                                            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                                            xmlns:tns="http://www.superoffice.com/superid/partnersystemuser/0.1"
                                            xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
            <SOAP-ENV:Header>
                <tns:ApplicationToken>${appToken}</tns:ApplicationToken>
                <tns:ContextIdentifier>${contextId}</tns:ContextIdentifier>
            </SOAP-ENV:Header>
            <ns0:Body>
                <ns1:AuthenticationRequest>
                    <ns1:SignedSystemToken>${signedToken}</ns1:SignedSystemToken>
                    <ns1:ReturnTokenType>Jwt</ns1:ReturnTokenType>
                </ns1:AuthenticationRequest>
            </ns0:Body>
        </SOAP-ENV:Envelope>`;
    
        console.log('');
        console.log('SOAP: ' + soapEnvelope);
    
        // send the SOAP envelope to SuperOffice!
    
        request.post({
            url: 'https://sod.superoffice.com/login/services/PartnerSystemUserService.svc',
            body: soapEnvelope,
            headers: {
                "Content-Type": "text/xml;charset=UTF-8",
                "SOAPAction": "http://www.superoffice.com/superid/partnersystemuser/0.1/IPartnerSystemUserService/Authenticate"
            }
        }, function(error, response, body){
            if(!error) {
    
                // convert the XML response to JSON!
    
                console.log('');
                console.log('Response: ' + body);
    
                xml2js.parseString(body,{tagNameProcessors: [xml2js.processors.stripPrefix]}, function(err, result){
                    console.log('');
    
                    if(err) {
                        console.log('Error converting XML to JSON!');
                    } else {
                        console.log('Converted to JSON: ' + JSON.stringify(result));
    
                        // drill into the JSON to determine if token was received.
    
                        var successful = result.Envelope.Body[0].AuthenticationResponse[0].IsSuccessful[0];
    
                        if(successful) {
    
                            // extract the token
    
                            var token = result.Envelope.Body[0].AuthenticationResponse[0].Token[0];
    
                            console.log('');
                            console.log('Token: ' + JSON.stringify(token));
    
                            var verifyOptions = {
                                ignoreExpiration: true,
                                algorithm:  ["RS256"]
                            };
                            try {
                                var publicKEY  = fs.readFileSync(publKeyFile, 'utf8');
    
                                // Verify the public SuperOffice certificate is loaded
                                // this is used to validate the JWT sent back from SuperOffice
    
                                if(publicKEY) {
                                    console.log("good to go!");
                                } else {
                                    console.log("NOT good to go!");
                                    return;
                                }
    
                                // validate the JWT and extract the claims
    
                                var decoded = jwt.verify(token, publicKEY, verifyOptions);
    
                                 // write out the ticket to the console, DONE!
    
                                console.log('');
                                console.log('System User Ticket: ' + decoded["http://schemes.superoffice.net/identity/ticket"]);
    
                            } catch (err) {
                                console.log('');
                                console.log('Error: ' + err);
                                return false;
                            }
                        } else {
                            console.log('Authentication failed and no token was received!');
                        }
                    }
                });
            } else {
                console.log('Error: ' + error);
            }
        });
    });

    I hope this helps