Skip to content

OAuth

OAauth authentication must be used by all user interfaces to authenticate a user. It can also be used to connect third party services like Amazon Alexa. Authentication is handled by C1 Auth There is no authentication information passed when the module is opened. Instead the module must redirect the user to the C1 Auth login page. If the user is logged in already, the login page immediately redirects back to the module page and the user is logged in without seeing a login form.

OAuth flow

C1 Auth requires a standard Authorization Code Flow. See https://datatracker.ietf.org/doc/html/rfc6749 for details. The chapter Endpoint usage applies, so please read it before continuing reading here.

URLs

The C1 Auth seed servers are as follows:

Branch URLs Port
Production https://n0r0c0.auth.sensaru.net, https://n1r0c0.auth.sensaru.net, https://n2r0c0.auth.sensaru.net 4002
Staging https://n0r0c0.auth-staging.sensaru.net, https://n1r0c0.auth-staging.sensaru.net, https://n2r0c0.auth-staging.sensaru.net 3002
Development https://n0r0c0.auth-dev.sensaru.net, https://n1r0c0.auth-dev.sensaru.net, https://n2r0c0.auth-dev.sensaru.net 2002

The login URLs are:

Branch URLs
Production https://login.sensaru.net
Staging https://login-staging.sensaru.net
Development https://login-dev.sensaru.net

The C1 Core seed servers are as follows:

Branch URLs REST and JSON-RPC port Binary RPC port
Production https://n0r0c0.core.sensaru.net, https://n1r0c0.core.sensaru.net, https://n2r0c0.core.sensaru.net 4001 4000
Staging https://n0r0c0.core-dev.sensaru.net, https://n1r0c0.core-dev.sensaru.net, https://n2r0c0.core-dev.sensaru.net 3001 3000
Development https://n0r0c0.core-dev.sensaru.net, https://n1r0c0.core-dev.sensaru.net, https://n2r0c0.core-dev.sensaru.net 2001 2000

Redirect to login page

When the user is not logged in, he needs to be redirected to the login page.

The following GET parameters need to be passed to the login page:

Parameter Description
client_id The client ID assigned to the module's UI. The client ID and client secret are randomly generated and must be registered to C1 Auth in order to work.
response_type Only code is supported.
state See description of state below.
code_challenge See description of code_challenge below.
code_challenge_method Only S256 is supported.
sp The ID of the system provider.
sd The ID of the system distributor.
bp The ID of the business partner.

state

The state variable enforces, that a login really is initiated by the module's UI. It prevents cross login requests. Either a random string can be used as state variable. This string needs to be stored and verified when the user is redirected back from the login page. Alternatively we can use a signed random value. This enables us to just check the signature when the user is redirected back and we don't have to store the state anywhere.

We can also sign additional metadata in the state string. The recommendation is:

Metadata Description
Time A time stamp to know when the state was generated. The state should only be valid for a short amount of time (e. g. 10 minutes)
Random data Random data to make hacking more difficult
Principal The principal ID to prevent logging in as a different principal than requested. The principal IDs must be passed to the module's UI as GET query parameters.

An example implementation in PHP might look like this:

function getState() : string
{
    global $systemProvider;
    global $systemDistributor;
    global $businessPartner;
    $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

    $pieces = [];
    $max = mb_strlen($keyspace, '8bit') - 1;
    for ($i = 0; $i < 64; ++$i)
    {
        $pieces[] = $keyspace[random_int(0, $max)];
    }
    $randomString = implode('', $pieces);
    $data = time().','.$randomString.','.$systemProvider.','.$systemDistributor.','.$businessPartner;

    $keyId = openssl_pkey_get_private('file://'.__DIR__.'/../stateCerts/key.pem');
    if($keyId === false) die('Could not read private key.');
    $signature = '';
    if(openssl_sign($data, $signature, $keyId, OPENSSL_ALGO_SHA512) === false) 
    {
        openssl_pkey_free($keyId);
        die('Could not generate state.');
    }
    openssl_pkey_free($keyId);
    return base64_encode($data.';'.base64_encode($signature));
}

code_challenge

Sensaru Cloud uses "Proof Key for Code Exchange" (PKCE) to mitigate the risk of interception attacks. Read https://datatracker.ietf.org/doc/html/rfc7636 for details. The code verifier is just a random string. Here's an example in PHP on how to generate a code verifier:

function getCodeVerifier()
{
    $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~';

    $pieces = [];
    $max = mb_strlen($keyspace, '8bit') - 1;
    for ($i = 0; $i < 128; ++$i)
    {
        $pieces[] = $keyspace[random_int(0, $max)];
    }
    return implode('', $pieces);
}

The generated code verifier is hashed and base64-encoded to generate code_challenge:

$codeChallenge = base64_encode(hash('sha256', $codeVerifier, true));

This hashed code challenge is sent to the login page. The code verifier needs to stored (e. g. in the session). When the user is redirected back and the authorization code is exchanged for the access token, the (unhashed) code verifier must be sent to the authorization server.

Process redirect from login page

When the user is redirected back to the module page, two GET parameters are passed: code and state. state matches the one you passed to the login page. When the data is signed, the signature needs to be verified. Otherwise it is required to check it state matches the stored one. It is also required to check if state has expired. Here's an example on how to check the signature and for expiration in PHP:

list($data, $signature) = explode(';', base64_decode(urldecode($_GET['state'])));
    if(strlen($data) > 0 && strlen($signature) > 0)
    {
        list($time, $randomString, $stateSystemProvider, $stateSystemDistributor, $stateBusinessPartner) = explode(',', $data);
        if($stateSystemProvider === '') $stateSystemProvider = '0';
        if($stateSystemDistributor === '') $stateSystemDistributor = '0';
        if($stateBusinessPartner === '') $stateBusinessPartner = '0';
        $timeDifference = time() - $time;
        if($timeDifference >= 0 && $timeDifference < 600)
        {
            $keyId = openssl_pkey_get_public('file://'.__DIR__.'/../stateCerts/cert.pem');
            $result = openssl_verify($data, base64_decode($signature), $keyId, OPENSSL_ALGO_SHA512);
            openssl_pkey_free($keyId);

            if($result === 1)
            {
                .
                .
                .
            }
        }
    }
}

When the signature is valid, the access token can be requested. For this a HTTP POST needs to be sent to C1 Auth to path /api/v1/oauth/token with the following data using application/x-www-form-urlencoded as content type:

Parameter Description
grant_type authorization_code
code The authorization code just received as GET parameter code.
client_id The client ID to identify the module.
client_secret The client secret to authenticate the module.
code_verifier The previously generated code verifier which was sent as a hashed value to the login page.

On success the access and refresh tokens are returned as a JSON. Both need to be stored in the user's session. The validity typically is 1 hour. Before this, the access token should be refreshed using the refresh token.

The access and refresh token contain information that must be checked. With PHP this looks like this:

$keyParts = explode(',', base64_decode($accessToken));
$keyParts2 = explode(',', base64_decode($refreshToken));
if(count($keyParts) >= 8 &&
   count($keyParts2) >= 8 &&
   $keyParts[7] === $clientId &&
   $keyParts[2] === $stateSystemProvider &&
   $keyParts[3] === $stateSystemDistributor &&
   $keyParts[4] === $stateBusinessPartner &&
   $keyParts2[7] === $clientId &&
   $keyParts2[2] === $stateSystemProvider &&
   $keyParts2[3] === $stateSystemDistributor &&
   $keyParts2[4] === $stateBusinessPartner)
{
    //Save REST object state to session.
    $rest->sessionSave();
    header('Location: '. $_SERVER['PHP_SELF']);
    die();
}
else
{
    $rest->logout();
    die('Unauthorized');
}

In particular it is mandatory to verify that the following access token and refresh token fields match the information within the module:

Field name Field index Must match
Client ID 7 The client ID of the module
System provider 2 The system provider in state
System distributor 3 The system distributor in state
Business partner 4 The business partner in state

Warning

These checks are mandatory to have a secure system! Otherwise the system might be hacked.

Verify with C1 Core that user is authenticated and access token belongs to your module

At last the access token must be used to query the user from C1 Core. The response must be checked for validity.

To query the user information, send a GET request to /user on one of the C1 Core endpoints.

An example check in PHP looks like this:

isset($result['success']) && $result['success'] === true &&
isset($result['result']['authenticated']) && $result['result']['authenticated'] === true && //Check if the access token is valid
isset($result['result']['clientId']) && $result['result']['clientId'] === $clientId //Check if the access token belongs to our service

When the response is valid, the user is successfully logged in.