Tutorials 18 September 2020

How to Load Test OAuth secured APIs with k6?

Mostafa Moradian, Developer Advocate

The outline of this article is as follows:

  1. Overview
  2. Concepts
    1. OAuth 2.0 authentication flows
    2. OAuth 2.0 grant types
  3. Microsoft Azure Active Directory's Example OAuth 2.0 Application
  4. Testing OAuth Authentication with Curl
    1. Client credentials grant
    2. Password grant
  5. JavaScript Functions for OAuth Authentication in a k6 Script
  6. Test Script Template with OAuth Authentication
  7. Conclusion

Overview

Most systems are designed to treat users differently based on their identity. Therefore, users are authenticated via various mechanisms and it's often equally important to test the performance of anonymous and authenticated endpoints. One of the most widespread authentication standards used by today's applications is OAuth.

In OAuth, we use a trusted third party to verify the identity of the user. The process starts by redirecting the user from your website to the website of the trusted party. The third party then allows the user to login using either a traditional login form or some other method (for instance, via an API).

Upon successful authentication and authorization, the user gets redirected back to your website with a valid OAuth token, which is passed to every other endpoint on your system, allowing the system under test (SUT) to verify the authentication.

In this article, we explore two simple ways to use OAuth to authenticate the user, using either Microsoft Azure Active Directory (AAD) with the Microsoft Identity Platform or Okta.

While there are many ways to authenticate through OAuth platforms, we will explore authentication using client credentials and username/password. The provided authentication code and use-case template also help demystify the OAuth authentication flow.

Thus, this is the idea behind this article:

  1. You want to load test your system with k6.
  2. Your endpoints are secured with the OAuth protocol.
  3. The OAuth is making your load-testing more complex, which I am going to explain here.

Concepts

OAuth 2.0 authentication flows

There are two ways to authenticate with OAuth protocol using either Microsoft AAD or Okta:

  • Authorization code flow is the recommended approach. In this case, the user is sent to the sign-in page to authenticate and give consent to the application's requested permissions, the /authorize endpoint on Microsoft AAD or Okta is then used to retrieve the authorization code. Using the returned authorization code, the user requests access and refresh tokens via the /token endpoint on Microsoft AAD or Okta. For more information, please visit either Microsoft or Okta guides about the OAuth 2.0 authorization code flow.
  • Implicit flow just skips the authorization step and authenticates the user directly via the /token endpoint on Microsoft AAD or Okta. This is not secure, and not recommended in any way, yet for the sake of simplicity and testing, we'll explain this method. For more information, please visit either Microsoft or Okta guides about the OAuth 2.0 implicit grant flow.

OAuth 2.0 grant types

There are many ways to grant access to users, but in this article, we'll focus on two major implicit flow approaches:

A) Client credentials grant is used for authentication between servers, microservices, or daemons:

On Microsoft AAD, refer to their client credentials flow. The admin should give consent to the permissions requested in advance. This can be achieved either by requesting permissions from a directory admin or by having the admin give the consent via application's API permissions.

On Okta, refer to their client credentials flow. The consent method is trusted by default, so you usually needn't give consent in advance. For more information, please refer to their docs on how to enable consent for scopes and how to request user consent during authentication. This grant flow is demonstrated in the following flowchart from Microsoft documentation.

Client credentials grant flow

B) Resource owner password credentials grant is used for authenticating users by directly requesting a token using the user's credentials. For individual implementations, refer to either Microsoft or Okta password flow. This grant flow is demonstrated in the following flowchart from Microsoft documentation.

Password grant flow

Microsoft Azure Active Directory's Example OAuth 2.0 Application

I've created an App for testing purposes on the Microsoft AAD using a tutorial on how to register an app with Azure Active Directory. Then I granted admin consent to the application using the article on how to grant tenant-wide admin consent to an application and configure the admin consent workflow. I've also defined some default permissions, namely email, openid, profile, https://graph.microsoft.com/User.Read and https://graph.microsoft.com/User.ReadBasic.All.

While creating the application, Azure provided me with an example application that made use of MSAL Python library and was some boilerplate code for implementing the authentication. I tested it against my App setup, and it worked perfectly, as shown below.

Microsoft Identity Python Web App

Testing OAuth Authentication with Curl

First, let's test authentication endpoints with curl.

Client credentials grant

The endpoint parameters are documented in the guide about service to service calls using client credentials (shared secret or certificate).

$ curl --location --request POST 'https://login.microsoftonline.com/<tenantId>/oauth2/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=client_credentials' \
    --data-urlencode 'client_id=<clientId>' \
    --data-urlencode 'client_secret=<clientSecret>' \
    --data-urlencode 'resource=<resource>' \
    --data-urlencode 'scope=email openid profile https://graph.microsoft.com/User.Read https://graph.microsoft.com/User.ReadBasic.All'

Password grant

The endpoint parameters are documented in the guide about OAuth 2.0 resource-owner password credentials on Microsoft identity platform.

$ curl --location --request POST 'https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode 'grant_type=password' \
    --data-urlencode 'client_id=<clientId>' \
    --data-urlencode 'client_secret=<clientSecret>' \
    --data-urlencode 'username=<username>' \
    --data-urlencode 'password=<password>' \
    --data-urlencode 'scope=email openid profile https://graph.microsoft.com/User.Read https://graph.microsoft.com/User.ReadBasic.All'

JavaScript Functions for OAuth Authentication in a k6 Script

When all is said and done, now's the time to incorporate OAuth authentication into your k6 load-test script using the following functions. Save either of these files into a directory named oauth.

import http from 'k6/http';

/**
 * Authenticate using OAuth against Azure Active Directory
 * @function
 * @param  {string} tenantId - Directory ID in Azure
 * @param  {string} clientId - Application ID in Azure
 * @param  {string} clientSecret - Can be obtained from https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app#create-a-client-secret
 * @param  {string} scope - Space-separated list of scopes (permissions) that are already given consent to by admin
 * @param  {string} resource - Either a resource ID (as string) or an object containing username and password
 */
export function authenticateUsingAzure(tenantId, clientId, clientSecret, scope, resource) {
  let url;
  const requestBody = {
    client_id: clientId,
    client_secret: clientSecret,
    scope: scope,
  };

  if (typeof resource == 'string') {
    url = `https://login.microsoftonline.com/${tenantId}/oauth2/token`;
    requestBody['grant_type'] = 'client_credentials';
    requestBody['resource'] = resource;
  } else if (
    typeof resource == 'object' &&
    resource.hasOwnProperty('username') &&
    resource.hasOwnProperty('password')
  ) {
    url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
    requestBody['grant_type'] = 'password';
    requestBody['username'] = resource.username;
    requestBody['password'] = resource.password;
  } else {
    throw 'resource should be either a string or an object containing username and password';
  }

  let response = http.post(url, requestBody);

  return response.json();
}
import http from 'k6/http';

/**
 * Authenticate using OAuth against Okta
 * @function
 * @param  {string} oktaDomain - Okta domain to authenticate against (e.g. 'k6.okta.com')
 * @param  {string} authServerId - Authentication server identifier (default is 'default')
 * @param  {string} clientId - Generated by Okta automatically
 * @param  {string} clientSecret - Generated by Okta automatically
 * @param  {string} scope - Space-separated list of scopes
 * @param  {string|object} resource - Either a resource ID (as string) or an object containing username and password
 */
export function authenticateUsingOkta(oktaDomain, authServerId, clientId, clientSecret, scope, resource) {
  if (authServerId === 'undefined' || authServerId == '') {
    authServerId = 'default';
  }
  let url = `https://${oktaDomain}/oauth2/${authServerId}/v1/token`;
  const requestBody = { scope: scope };
  let response;

  if (typeof resource == 'string') {
    requestBody['grant_type'] = 'client_credentials';

    const encodedCredentials = encoding.b64encode(`${clientId}:${clientSecret}`);
    const params = {
      auth: 'basic',
      headers: {
        Authorization: `Basic ${encodedCredentials}`,
      },
    };

    response = http.post(url, requestBody, params);
  } else if (
    typeof resource == 'object' &&
    resource.hasOwnProperty('username') &&
    resource.hasOwnProperty('password')
  ) {
    requestBody['grant_type'] = 'password';
    requestBody['username'] = resource.username;
    requestBody['password'] = resource.password;
    requestBody['client_id'] = clientId;
    requestBody['client_secret'] = clientSecret;

    response = http.post(url, requestBody);
  } else {
    throw 'resource should be either a string or an object containing username and password';
  }

  return response.json();
}

Test Script Template with OAuth Authentication

This template script is an example that uses the above-mentioned authentication functions. The following script uses the setup function to authenticate once and then the return value is reused in the VU code (export default function), which is used to authenticate against another endpoint. The point is that you usually don't need to authenticate multiple times, since you are not testing the performance of Azure or any other identity provider, unless you specifically want to.

import http from 'k6/http';
import { authenticateUsingAzure } from './oauth/azure.js';
import { authenticateUsingOkta } from './oauth/okta.js';

const AZURE_TENANT_ID = 'AZURE_APP_DIRECTORY_ID';
const AZURE_CLIENT_ID = 'AZURE_APP_APPLICATION_ID';
const AZURE_CLIENT_SECRET = 'AZURE_APP_CLIENT_SECRET';
const USERNAME = 'USERNAME';
const PASSWORD = 'PASSWORD';
const RESOURCE = 'RESOURCE_ID_URI';
const AZURE_SCOPES =
  'email openid profile https://graph.microsoft.com/User.Read https://graph.microsoft.com/User.ReadBasic.All';

const OKTA_CLIENT_ID = 'OKTA_CLIENT_ID';
const OKTA_CLIENT_SECRET = 'OKTA_CLIENT_SECRET';
const OKTA_DOMAIN = 'youcompanyaccount.okta.com';
const OKTA_SCOPES = 'profile email openid';

export function setup() {
  // Use either password authentication flow
  let passwordAuthResp = authenticateUsingAzure(AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SCOPES, {
    username: USERNAME,
    password: PASSWORD,
  });

  return passwordAuthResp;

  // Or client credentials authentication flow
  // let clientAuthResp = authenticateUsingAzure(
  //     AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SCOPES, RESOURCE
  // );
  // return clientAuthResp;

  // // Example of Okta OAuth password authentication flow
  // let oktaPassAuth = authenticateUsingOkta(OKTA_DOMAIN, 'default', OKTA_CLIENT_ID, OKTA_CLIENT_SECRET, OKTA_SCOPES,
  // {
  //     username: USERNAME,
  //     password: PASSWORD
  // });
  // // This should print the authentication tokens
  // console.log(JSON.stringify(oktaPassAuth));
  // return oktaPassAuth;
}

export default function (data) {
  // Then, use the access_token to access a protected resource (user profile)
  // NOTE: access_token from client credentials flow cannot be used to access the user profile
  let params = {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${data.access_token}`, // or `Bearer ${clientAuthResp.access_token}`
    },
  };
  let userProfileUrl = 'https://graph.microsoft.com/v1.0/me';
  let res = http.get(userProfileUrl, params);

  // Do something with the response
  // For example, this should print the user profile
  console.log(JSON.stringify(res.json()));
}

Conclusion

In this article, I have explained how to authenticate against Microsoft Azure AAD and Okta via OAuth. There exists the Microsoft Authentication Library (MSAL) for JS, yet it uses various browser APIs that are not supported in k6. There are other libraries, but they have similar issues. Instead, I used the Microsoft identity platform APIs.

I've also explained how OAuth works with different authentication flows. Then, I've used curl to test OAuth on Microsoft AAD. Afterward, I've shown two helper functions to be used in k6 load-test scripts and also shown examples on how to use them.

Hope you enjoyed reading this article. I'd be happy to hear your feedback and possible improvements.

< Back to all posts