The Next Level3 Azure AD B2C integration is designed to be used for any of your existing applications or sites which are using Azure AD for authentication. This integration will allow you to add Account Protection for any application which leverages Azure AD B2C for authentication. 

Pre-requisites

Requirements:

– Application leveraging Azure AD B2C for authentication
– Next Level3 Company Account
– Signing Key created for an application in the Next Level3 Company Portal

Account Protection

ADDING NEXT LEVEL3 AZURE AD B2C CUSTOM POLICY ON SIGN IN

The first step to add an NL3 Account Protection Check to an existing application which is using Azure AD B2C for authentication is to create a custom policy to add the check to your existing login flow. Unless you are very familiar we Azure AD B2C policies and user journeys, we recommend downloading the starter packs from here (https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack). For most of you, assuming you have already implemented Azure AD B2C, you may already be familiar with the policies you are using and you can start with those policies.

For this integration, we used the standard ‘Local Accounts’ starter pack downloaded from the GitHub repository referenced above. The only change we made to the policies listed in the ‘Local Accounts’ folder of that repo was in the TrustFrameworkBase.xml policy. We added the following ‘ClaimsProvider’ to line 453 underneath the existing ‘Local Account SignIn’ ‘ClaimsProvider’:

 

				
					<ClaimsProvider>
    <DisplayName>Local Account NL3 Protection Check</DisplayName>
    <TechnicalProfiles>
        <TechnicalProfile Id="REST-NL3AccountProtectionCheck">
            <DisplayName>Perform NL3 Account Protection Check</DisplayName>
            <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
            <Metadata>
                <Item Key="ServiceUrl">https://[your-service-endpoint].azurewebsites.net/api/AccountProtectionCheck</Item>
                <Item Key="AuthenticationType">ApiKeyHeader</Item>
                <Item Key="SendClaimsIn">Body</Item>
            </Metadata>
            <CryptographicKeys>
                <Key Id="x-functions-key" StorageReferenceId="B2C_1A_RestApiKey" />
            </CryptographicKeys>
            <InputClaims>
                <InputClaim ClaimTypeReferenceId="signInName" />
            </InputClaims>
        </TechnicalProfile>
    </TechnicalProfiles>
</ClaimsProvider>
				
			

The ‘ServiceUrl’ points to an API endpoint that runs the code for performing the NL3 Account Protection Check. We have deployed this as an Azure Function, but as long as it is a RESTful API endpoint that validates the API key in the headers, performs the Protection Check, and returns the proper status codes and messages, the technology used is not important. We will use an Azure Function for this example. If you are not familiar with Azure Functions, the following guides can be helpful:

https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-function-app-portal

https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-csharp?tabs=azure-cli%2Cin-process

https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-node

The function in this example uses the Node.js 16 runtime and this is the code you can use within your function to perform the protection check:

				
					const https = require("https");
const nJwt = require("njwt");

function getLockStatus(jwt, apiHost, apiPath, requestHeaders) {
  return new Promise((resolve, reject) => {
    const postData = JSON.stringify({
      userIP: requestHeaders["x-forwarded-for"],
      userDevice: requestHeaders["user-agent"],
      userLocation: "",
      integrationType: "aadb2c",
      integrationData: {},
    });
    const options = {
      host: apiHost,
      port: "443",
      path: apiPath,
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
        "x-nl3-authorization-token": jwt,
        "Content-Length": Buffer.byteLength(postData),
      },
    };

    const req = https.request(options, (response) => {
      const chunksOfData = [];

      response.on("data", (fragments) => {
        chunksOfData.push(fragments);
      });

      response.on("end", () => {
        const responseBody = Buffer.concat(chunksOfData);
        resolve(responseBody.toString());
      });
      response.on("error", (error) => {
        console.log(`Error = ${error.message}`);
        reject(error);
      });
    });
    req.write(postData);
    req.end();
  });
}

module.exports = async function (context, req) {
  let responseMessage = "";
  let responseStatus = 200;
  const claims = {
    iss: process.env.APP_URI,
    aud: process.env.API_HOST,
    sub: req.body.signInName,
  };
  /* Ideally this Signing Key would be stored and retrieved from a secrets manager
     and not an environmental variable */
  const decodedDomainToken = Buffer.from(process.env.SIGNING_KEY, "base64");
  const jwt = nJwt.create(claims, decodedDomainToken);
  jwt.setExpiration(new Date().getTime() + 60 * 5 * 1000); // 5 minute expiration
  jwt.setNotBefore(new Date().getTime() - 60 * 1 * 1000); // 1 minute leeway
  const authToken = jwt.compact();

  const res = await getLockStatus(
    authToken,
    process.env.API_HOST,
    process.env.API_PATH,
    req.headers
  );
  const result = JSON.parse(res);
  let failed = false;

  if (result) {
    context.log(JSON.stringify(result));
    if (Object.prototype.hasOwnProperty.call(result, "locked")) {
      if (result.locked) {
        responseStatus = 409;
        responseMessage = process.env.LOCKED_MESSAGE;
      }
    } else {
      failed = true;
    }
  } else {
    failed = true;
  }
  if (failed) {
    if (process.env.FAIL_CLOSED == "true") {
      responseStatus = 409;
      responseMessage = "NL3 Account Protection Check failed and configuration is set to fail closed!";
    }
  }
  context.res = {
    status: responseStatus,
    body: responseMessage
  };
};
				
			

The environmental variables for API_HOST, API_PATH, APP_URI, FAIL_CLOSED, LOCKED_MESSAGE, and SIGNING_KEY can be set by opening your Function App in the Azure Portal, then clicking on ‘Configuration’ and adding each as a ‘New application setting’ under ‘Application settings’. We have added all values as environmental variables for simplicity, but it is recommended that the ‘SIGNING_KEY’ be stored in a secrets manager like Azure Key Vault instead of being stored as an environmental variable when possible which will require some minor updates to the code (please contact support for guidance). The URL to use for your ‘ServiceUrl’ in custom policy can be found in your ‘Function App’ under ‘Functions’, then click on your function’s name, then click on the ‘Get Function Url’ selection at the top (you can remove the ?code= . . . parameter at the end since we will be providing that code in the headers).

Once your function is created and you have updated the ‘ServiceUrl’ in your custom policy, you will need to deploy the custom policy in Azure. If you have already set up custom policy to support your application previously, you will only need to upload the modified ‘TrustFrameworkBase.xml’ policy. However, if you have not previously leveraged custom policy, guidance can be found here on how to set up the pre-requisites and upload a policy:

https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-custom-policy

Then, depending on the type of application you are integrating, you will need to update the corresponding settings to point to the custom policy. Examples are provided for a variety of application types are listed under ‘Next Steps’ in the above referenced tutorial.

Scroll to Top