菜单
文档breadcrumb arrow Grafana k6breadcrumb arrow 示例breadcrumb arrow OAuth 身份验证
开源

OAuth 身份验证

关于如何在负载测试中使用 OAuth 身份验证的脚本示例。

OAuth 身份验证

以下示例接受函数文档中显示的一组参数,并以 JSON 格式返回响应体,以便从中提取令牌。

Azure Active Directory

JavaScript
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';
  }

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

  return response.json();
}

Azure B2C

以下示例展示了如何使用客户端凭据流程 (Client Credentials Flow) 通过 Azure B2C 进行身份验证。

此示例基于 azure-ad-b2c/load-tests 存储库中的 JMeter 示例。

要使用此脚本,你需要

  1. 设置你自己的 Azure B2C 租户
    • 复制租户名称,它将在你的测试脚本中使用。
  2. 注册一个 Web 应用程序
    • 注册一个单页应用程序,重定向 URL 为: https://jwt.ms。这是流程接收令牌所必需的。
    • 创建后,你可以获取应用程序(客户端)ID 和目录(租户)ID。复制这两个 ID,它们将在你的测试脚本中使用。
  3. 创建一个用户流程,以便你可以注册和创建用户
    • 创建一个新用户,并复制用户名和密码。它们将在测试脚本中使用。

如果之后需要参考,可以在 Azure 门户的 B2C 设置中找到这些设置。请确保填写 B2CGraphSettings 对象的所有变量,并在 export default function 中替换 USERNAMEPASSWORD

JavaScript
import http from 'k6/http';
import crypto from 'k6/crypto';
import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js';

const B2CGraphSettings = {
  B2C: {
    client_id: '', // Application ID in Azure
    user_flow_name: '',
    tenant_id: '', // Directory ID in Azure
    tenant_name: '',
    scope: 'openid',
    redirect_url: 'https://jwt.ms',
  },
};

/**
 * Authenticate using OAuth against Azure B2C
 * @function
 * @param  {string} username - Username of the user to authenticate
 * @param  {string} password
 * @return {string} id_token
 */
export function GetB2cIdToken(username, password) {
  const state = GetState();
  SelfAsserted(state, username, password);
  const code = CombinedSigninAndSignup(state);
  return GetToken(code, state.codeVerifier);
}

/**
 * @typedef {object} b2cStateProperties
 * @property {string} csrfToken
 * @property {string} stateProperty
 * @property {string} codeVerifier
 *
 */

/**
 * Get the id token from Azure B2C
 * @function
 * @param {string} code
 * @returns {string} id_token
 */
const GetToken = (code, codeVerifier) => {
  const url =
    `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com/${B2CGraphSettings.B2C.tenant_id}` +
    `/oauth2/v2.0/token` +
    `?p=${B2CGraphSettings.B2C.user_flow_name}` +
    `&client_id=${B2CGraphSettings.B2C.client_id}` +
    `&grant_type=authorization_code` +
    `&scope=${B2CGraphSettings.B2C.scope}` +
    `&code=${code}` +
    `&redirect_uri=${B2CGraphSettings.B2C.redirect_url}` +
    `&code_verifier=${codeVerifier}`;

  const response = http.post(url, '', {
    tags: {
      b2c_login: 'GetToken',
    },
  });

  return JSON.parse(response.body).id_token;
};

/**
 * Signs in the user using the CombinedSigninAndSignup policy
 * extraqct B2C code from response
 * @function
 * @param {b2cStateProperties} state
 * @returns {string} code
 */
const CombinedSigninAndSignup = (state) => {
  const url =
    `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com/${B2CGraphSettings.B2C.tenant_name}.onmicrosoft.com` +
    `/${B2CGraphSettings.B2C.user_flow_name}/api/CombinedSigninAndSignup/confirmed` +
    `?csrf_token=${state.csrfToken}` +
    `&rememberMe=false` +
    `&tx=StateProperties=${state.stateProperty}` +
    `&p=${B2CGraphSettings.B2C.user_flow_name}`;

  const response = http.get(url, '', {
    tags: {
      b2c_login: 'CombinedSigninAndSignup',
    },
  });
  const codeRegex = '.*code=([^"]*)';
  return response.url.match(codeRegex)[1];
};

/**
 * Signs in the user using the SelfAsserted policy
 * @function
 * @param {b2cStateProperties} state
 * @param {string} username
 * @param {string} password
 */
const SelfAsserted = (state, username, password) => {
  const url =
    `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com/${B2CGraphSettings.B2C.tenant_id}` +
    `/${B2CGraphSettings.B2C.user_flow_name}/SelfAsserted` +
    `?tx=StateProperties=${state.stateProperty}` +
    `&p=${B2CGraphSettings.B2C.user_flow_name}` +
    `&request_type=RESPONSE` +
    `&email=${username}` +
    `&password=${password}`;

  const params = {
    headers: {
      'X-CSRF-TOKEN': `${state.csrfToken}`,
    },
    tags: {
      b2c_login: 'SelfAsserted',
    },
  };
  http.post(url, '', params);
};

/**
 * Calls the B2C login page to get the state property
 * @function
 * @returns {b2cStateProperties} b2cState
 */
const GetState = () => {
  const nonce = randomString(50);
  const challenge = crypto.sha256(nonce.toString(), 'base64rawurl');

  const url =
    `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com` +
    `/${B2CGraphSettings.B2C.tenant_id}/oauth2/v2.0/authorize?` +
    `p=${B2CGraphSettings.B2C.user_flow_name}` +
    `&client_id=${B2CGraphSettings.B2C.client_id}` +
    `&nonce=${nonce}` +
    `&redirect_uri=${B2CGraphSettings.B2C.redirect_url}` +
    `&scope=${B2CGraphSettings.B2C.scope}` +
    `&response_type=code` +
    `&prompt=login` +
    `&code_challenge_method=S256` +
    `&code_challenge=${challenge}` +
    `&response_mode=fragment`;

  const response = http.get(url, '', {
    tags: {
      b2c_login: 'GetCookyAndState',
    },
  });

  const vuJar = http.cookieJar();
  const responseCookies = vuJar.cookiesForURL(response.url);

  const b2cState = {};
  b2cState.codeVerifier = nonce;
  b2cState.csrfToken = responseCookies['x-ms-cpim-csrf'][0];
  b2cState.stateProperty = response.body.match('.*StateProperties=([^"]*)')[1];
  return b2cState;
};

/**
 * Helper function to get the authorization header for a user
 * @param {user} user
 * @returns {object} httpsOptions
 */
export const GetAuthorizationHeaderForUser = (user) => {
  const token = GetB2cIdToken(user.username, user.password);

  return {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + token,
    },
  };
};

export default function () {
  const token = GetB2cIdToken('USERNAME', 'PASSWORD');
  console.log(token);
}

Okta

JavaScript
import http from 'k6/http';
import encoding from 'k6/encoding';

/**
 * 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';
  }
  const 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();
}

有关详细示例,请访问此文章:如何使用 k6 对 OAuth 安全的 API 进行负载测试?