Using OAuth2 for API authorization with DEX

https://img.shields.io/badge/AIMMS_4.90-Minimum_AIMMS_Version_WebUI-brightgreen https://img.shields.io/badge/AIMMS_2.90-Minimum_AIMMS_Version_PRO-brightgreen

OAuth example download

With the Data Exchange Library (DEX) you can use the OAuth2 protocol for the authentication procedure of API’s. On this how-to page you can find more details on how this type of authorization works and the flows that come with it. You can download the example project to replicate the examples described below.

In this article we will be demonstrating the two available options:

  1. the Authorization Code flow, meaning we are requesting access to the API on behalf of a user. We need a client to manage the authentication request, but the logged in user needs to perform the manual step of granting the permission for this request.

  2. the Client Credentials flow. This one is often used when it makes more sense to authenticate and authorize an app instead of a personal user login. This is the case for server-to-server/machine-to-machine communications where the client ID and client secret are used to authenticate and get access.

Prerequisites

  1. You need to have the Data Exchange Library installed. Visit this article for instructions on how to do this.

  2. You will need certain details from the API that needs authorization. This means either you are able to access/configure the API settings yourself, or you have access to someone who can do this for you. For the Client Credentials flow you’ll need the client id, client secret, token endpoint and the scope. For the Authorization Code flow, these are the client id, client secret, token endpoint, scope, authorization endpoint and path part of the redirect URL where the used identity platform will need to forward the result to.

Implementing the Authorization Code flow with Google

In this example we use Google’s OpenID Connect API, with the goal to obtain the logged-in Google user data within the AIMMS application. Because we are following the Authorization Code Flow, user consent is required.

Through the Google Cloud dashboard we have set up an account as to retrieve a client ID and client secret:

../../_images/google_step1.png

This provides us with some details we will need for the flow to work (see right top of the page):

../../_images/google_step2.png

You can see in the section at the bottom left that we’ve added two redirect URI’s; one for usage from the AIMMS Cloud (URI 1) and one for usage from a locally opened AIMMS PRO (URI 2), so the connection should work both from a local connection as well as from an AIMMS app uploaded to the cloud.

If we take a look at the setup within AIMMS we see the following:

!empty UserInfo_Data, just to make sure we start off clean
empty dex::oauth::UserInfo_Data;

!load data into an APIClient we name 'Google'
dex::oauth::APIClients := data { Google };

!set data for 'Google'
dex::oauth::APIClientStringData('Google',dex::oauth::apidata) :=$ data {
        authorizationEndpoint : "https://accounts.google.com/o/oauth2/v2/auth",
        tokenEndpoint : "https://oauth2.googleapis.com/token",
        openIDEndpoint : "https://www.googleapis.com/oauth2/v3/userinfo",
        clientId : "xxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com",
        clientSecret : "xxxxxx-xxxxxxxxxxxxxxx",
        scope: "openid profile";
}

The authorization endpoint, token endpoint and open ID endpoint should be provided by the API that requires authentication. The client ID and client secret are, in this example, provided by Google’s OAuth 2.0 authentication system (as shown in the screenshot above).

Now, when we open the example project either locally or as an app uploaded on our cloud, we are able to run the procedure (in the WebUI by clicking the related button). The underlying procedure in AIMMS is:

InitializeOAuthClients;
dex::oauth::GetUserInfo('Google');

This will first send us to the Google authentication screen, where we will have to select the profile to authenticate with:

../../_images/google_step3.png

After that we will receive the message:

../../_images/google_step4.png

When this request has processed, you will see the requested data is provided:

../../_images/google_step5.png

Implementing the Authorization Code flow with Azure

For Azure, the OAuth 2.0 authentication flow is kind of similar to the one from Google, but of course set up from a different context. In this case, we can find the App Registrations in the Azure Active Directory within the Azure Portal. Once you’ve created the registration of the app, you will receive the necessary details:

../../_images/azure_step1.png

The secret can be found (or created, if none exists yet) under ‘Certificates & secrets’, or by simply clicking on the link next to ‘Client credentials’ in the above screenshot. Redirect URI’s should be added under ‘Authentication’:

../../_images/azure_step2a.png

The correct scope(s) for the request should be added in the ‘API permissions’ section. Since for the Authentication Code Flow we will retrieve the user data from the logged in user, we don’t need admin consent and the User.Read permission should be sufficient:

../../_images/azure_step2.png

In the request we’ll also add the ‘offline_access’ scope as defined by the documentation so we get a refresh token for extended access to resources. If we take a look at the setup within AIMMS we see the following:

!empty UserInfo_Data, just to make sure we start off clean
empty dex::oauth::UserInfo_Data;

!load data into an APIClient we name 'MSACF'
dex::oauth::APIClients := data { MSACF };

!set data for 'MSACF'
dex::oauth::APIClientStringData('MS',dex::oauth::apidata) :=$ data {
        authorizationEndpoint : "https://login.microsoftonline.com/[tenantID]/oauth2/v2.0/authorize",
        tokenEndpoint : "https://login.microsoftonline.com/[tenantID]/oauth2/v2.0/token",
        openIDEndpoint : "https://graph.microsoft.com/v1.0/me",
        clientId : "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
        clientSecret : "xxxxxxxxxxxxxxxxxxxx",
        scope: "offline_access https://graph.microsoft.com/User.Read"
};

The same arguments as the previous example should be provided, but of course with different data. Note that the tenantID should be provided in both the authorizationEndpoint and tokenEndpoint. We also perform the same request but with a different argument because we changed the name of the client:

InitializeOAuthClients;
dex::oauth::GetUserInfo('MSACF');

Now, when we open the example project either locally or as an app uploaded on our cloud, we are able to run the procedure and/or use the button in the WebUI to retrieve the requested user data.

Implementing the Client Credentials flow with Azure

The Client Credentials Code flow requires a slightly different setup to work. You can reuse the client that was set up for the Authorization Code Flow, but we need an additional API Permission within the Azure portal:

../../_images/azure_step2c.png

In AIMMS, we will work with the dex::client::NewRequest functionality. We first create the client:

!read mappings
dex::ReadAllMappings;

!empty UserInfo_Data, just to make sure we start off clean
empty dex::oauth::UserInfo_Data;

!create client
dex::oauth::APIClients := data { MS };
dex::oauth::APIClientStringData('MS',dex::oauth::apidata) :=$ data {
        tokenEndpoint : "https://login.microsoftonline.com/[tenantID]/oauth2/v2.0/token",
        clientId : "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx",
        clientSecret : "xxxxxxxxxxxxxxxxxxxx",
        scope: "https://graph.microsoft.com/.default"
};

Note that you should input the tenant ID into to tokenEndpoint. The scope has changed to the .default graph scope. We also left out the authorizationEndpoint (as we will now use a bearer) and the openIDEndpoint. Now we can create the request and add the bearer token:

!first create the request
dex::client::NewRequest(
        "getUser",
        "https://graph.microsoft.com/v1.0/users/[identifier]",
        'Callback',
        responsefile:"Output.json",
        tracefile:"Trace.xml"
);

!add bearer token
dex::oauth::AddBearerToken('MS', "getUser");

As you can see we’ve added a reference to a Callback procedure, necessary for the request to be handled properly but which will also be used to map the retrieved results onto a string parameter (or catch any possible error and show the related message). We are also tracing the request of which we store the results in a file called Trace.xml. The actual response will be in Output.json. Both of these files can be accessed if you run the procedure(s) locally. Now we are ready to perform the request:

!perform the request
dex::client::PerformRequest(
        "getUser"
);

!wait for response
dex::client::WaitForResponses(
        1000
);

!close request properly
dex::client::CloseRequest(
        "getUser"
);

If the request was performed successfully, the response data is now in Output.json. Then we use a DEX-mapping to map the retrieved data onto the same parameters that we used for the previous requests as to be able to show it correctly in the WebUI.