By integrating Azure B2C with OwnID, you can implement the full set of OwnID features to streamline your user authentication experience.

Prerequisites

  • Azure B2C tenant
  • Azure application with Microsoft Graph API permissions

How it Works

Integrate OwnID with Azure B2C by completing these four basic steps:

  • Step 1 - Configure your Azure B2C tenant.
  • Step 2 - Build 3 server endpoints
  • Step 3 - Create an OwnID application in the OwnID Console.
  • Step 4 - Integrate with your frontend.

Step 1 - Configure Azure B2C

Use Your Existing Azure B2C Tenant

If you already have an Azure B2C tenant set up:

  1. Sign in to the Azure Portal
  2. Switch to your Azure B2C tenant by clicking on your account in the top right corner
  3. Select your B2C directory from the dropdown

Note: If you don’t have an existing B2C tenant, you can create one by:

  1. Navigating to Create a resource > Identity > Azure Active Directory B2C
  2. Selecting Create a new Azure AD B2C Tenant
  3. Filling in the required information (Organization name, Domain name, Country/Region, etc.)
  4. Clicking Review + create and then Create

Register an Application for OwnID Integration

You’ll need to register a specific application for OwnID integration, even if you have other applications already registered in your tenant:

  1. In your B2C tenant, navigate to App registrations
  2. Click New registration
  3. Fill in the required information:
    • Name: OwnID Integration (or any name you prefer)
    • Supported account types: Accounts in this organizational directory only (default directory only - single tenant)
  4. Click Register
  5. After registration, note down the following values:
    • Application (client) ID
    • Directory (tenant) ID
    • Object ID
This application will be used specifically for OwnID integration and user management. It’s separate from any applications you use for user sign-in flows.

Create Client Secret

  1. In your registered app, navigate to Certificates & secrets
  2. Click New client secret
  3. Add a description and select expiration period
  4. Click Add
  5. Important: Note down the client secret value as it will be shown only once

Configure API Permissions

  1. In your registered app, navigate to API permissions
  2. Click Add a permission
  3. Select Microsoft Graph
  4. Select Application permissions
  5. Add the following permissions:
    • User.ReadWrite.All (to manage users)
    • Directory.ReadWrite.All (to manage directory)
  6. Click Add permissions
  7. Important: Click Grant admin consent for [your-tenant-name] button at the top of the API permissions page. This step is critical - without admin consent, you will get “Insufficient privileges” errors when accessing the Graph API.

Find Your B2C Extension App ID

Every Azure B2C tenant has a special application called the b2c-extensions-app that’s automatically created:

  1. In your B2C tenant, go to App registrations
  2. Switch to All applications if you don’t see it immediately
  3. Look for b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.
  4. Note down its Application (client) ID - you’ll need this for your environment variables

This Extension App ID is crucial for working with custom attributes:

  1. Your existing B2C setup likely already uses this app for custom user attributes
  2. The OwnID integration will store its data as custom attributes using this same app
  3. No manual attribute creation is needed - attributes are created automatically when first used
  4. Custom attributes in B2C follow a specific naming convention that our code handles automatically

Configure User Flow (Policy)

You need to use or create an appropriate user flow (policy) in your Azure B2C tenant that includes the necessary claims:

  1. Go to your Azure AD B2C tenant in the Azure Portal

  2. In the left menu, select User flows

  3. Either use an existing user flow or create a new one:

    • To create a new flow, click + New user flow
    • Select Sign in as the type (or Sign up and sign in)
    • Choose Recommended version
    • Enter a name (e.g., signin) - it will be prefixed with B2C_1_
  4. Important: Configure the correct return claims

    • In your user flow, go to Application claims
    • Ensure these claims are selected as Return claims:
      • Email Addresses (required for user identification)
      • User’s Object ID (required for user operations)
      • Display Name (recommended)
  5. Save your user flow configuration

  6. Note the full name of your user flow (e.g., B2C_1_signin)

Step 2 - Build 3 Server Endpoints

In this step, you need to expose the following API endpoints for OwnID to use in server-to-server calls:

Set OwnID Data for a User

Endpoint: POST /setOwnIDDataByLoginId

Description: Stores OwnID authentication data (account.ownIdData) for an existing user in Azure B2C.

Request Body from OwnID:

{"loginId": "user@example.com", "ownIdData": "base64string"}

Endpoint implementation example:

Node.js
router.post('/setOwnIDDataByLoginId', async (req, res) => {
  try {
    // Verify the OwnID request signature
    try {
      verifyOwnIdRequest(req);
    } catch (verificationError) {
      console.error('Request verification failed:', verificationError.message);
      return res.status(401).json({ error: verificationError.message });
    }

    const { loginId: email, ownIdData } = req.body;
    const user = await userOps.findByEmail(email);

    if (!user) return res.status(404).json({ error: 'User not found' });

    await userOps.setData(user.id, ownIdData);
    return res.sendStatus(204);
  } catch (error) {
    console.error('Error in setOwnIDDataByLoginId:', error);
    return res.status(500).json({ error: 'Internal server error', details: error.message });
  }
});

Expected Responses:

StatusDescriptionReturn
204Successno content returned
404User not foundno content returned

Get OwnID Data for a User

Endpoint: POST /getOwnIDDataByLoginId

Description: Retrieves OwnID authentication data (account.ownIdData) for an existing user in Azure B2C.

Request Body from OwnID:

{"loginId": "user@example.com"}

Endpoint Implementation Example:

Node.js
router.post('/getOwnIDDataByLoginId', async (req, res) => {
    try {
    // Verify the OwnID request signature
    try {
        verifyOwnIdRequest(req);
    } catch (verificationError) {
        console.error('Request verification failed:', verificationError.message);
        return res.status(401).json({ error: verificationError.message });
    }

    const { loginId: email } = req.body;
    const user = await userOps.findByEmail(email);

    // Return 404 if the user doesn't exist.
    if (!user) return res.json({ errorCode: 404 });

    const userData = await userOps.getData(user.id);

    // Return 204 No Content if the user exists but has no OwnID data.
    if (!userData?.ownIdData) return res.sendStatus(204);

    let ownIdData;
    try { 
        ownIdData = JSON.parse(userData.ownIdData); 
    } catch (e) {
        console.error('Error parsing OwnID data:', e);
        // Optionally handle the error here if needed
    }

    return res.json({ ownIdData });
    } catch (error) {
    return res.status(500).json({ error: 'Internal server error', details: error.message });
    }

Expected Responses:

StatusDescriptionReturn
200User found and has ownIdDatareturn ownIdData String
204User found but doesn’t have ownIdDatano content returned
404User not foundno content returned

Generate Session for a User

Endpoint: POST /getSessionByLoginId

Description: Generates Azure B2C authentication tokens for an existing user.

Request Body:

{"loginId": "user@example.com"}

Endpoint Implementation Example:

Node.js
router.post('/getSessionByLoginId', async (req, res) => {
  try {
    // Verify the OwnID request signature
    try {
      verifyOwnIdRequest(req);
    } catch (verificationError) {
      console.error('Request verification failed:', verificationError.message);
      return res.status(401).json({ error: verificationError.message });
    }

    const { loginId: email } = req.body;
    const user = await userOps.findByEmail(email);

    if (!user) return res.sendStatus(404);

    try {
      const tokenResponse = await userOps.getTokens(user.id, email);
      return res.json(tokenResponse);
    } catch (tokenError) {
      return res.status(500).json({
        error: 'Error acquiring authentication tokens',
        details: tokenError.message
      });
    }
  } catch (error) {
    return res.status(500).json({ error: 'Internal server error', details: error.message });
  }
});

Expected Responses:

StatusDescriptionReturn
200SuccessAzure B2C tokens
404User not foundno content returned

Authentication:

This endpoint acquires standard tokens from Azure B2C that can be used with Microsoft’s authentication libraries. The frontend application should use MSAL.js to handle these tokens for proper authentication flow.

Secure Your Endpoints

Requests from the OwnID server include two headers that can be used to ensure the request has not been tampered with. The first one, ownid-signature, is a hash value that the OwnID server generates from a timestamp and the body of the request. The second one, ownid-timestamp can be used by your backend to calculate the signature that is based on the timestamp and request body, and then compare the result to the value of ownid-signature. If both signatures do not match, the request has been altered.

Because the signatures are generated using an HMAC with the SHA256 hash function, the OwnID server and your backend must use the same cryptographic key when calculating the hash value. You can obtain this key from the OwnID Console, and then add the code generates a hash and compares it to the signature in your backend.

Obtaining the HMAC Key

Before the backend can generate the HMACSHA256 value, you must obtain the secret cryptographic key used in the calculation. Simply open your OwnID application in the OwnID Console and copy the value from MyApp > Shared Secret.

Request Verification

Now that you have the cryptographic key, the backend can verify requests by generating each request’s expected signature and compare it to the one generated by the OwnID server. The backend code must:

  • Step 1: Extract the ownid-signature and ownid-timestamp headers from the request. These headers provide the HMAC code generated by the OwnID server and the timestamp it used to generate it.

  • Step 2: Validate the ownid-timestamp for expiration. To prevent replay attacks, check whether the provided timestamp is within an acceptable time window. Define a preferred expiration time, such as 1 minute, and validate against the current time. Requests with a timestamp older than this period should be rejected.

  • Step 3: Create the data string that will be used as an input to the hash function. To create it you need to concatenate:

    • The request body (in a JSON string format)
    • The character .
    • The timestamp (from the ownid-timestamp header)
  • Step 4: Use HMAC with SHA256 to calculate a hashed value from the body-timestamp data string. The cryptographic key used in the calculation is the shared secret for your OwnID application.

  • Step 5: Compare the hash value generated by your backend with the signature extracted from the ownid-signature header.

The following code snippets show how the backend might accomplish these steps:

const crypto = require('crypto');

// Step 1: Extract values from headers and request body
let key = "<your-shared-secret>"; // This is the shared secret from OwnID Console
let keyBuffer = Buffer.from(key, 'base64');
let body = JSON.stringify(req.body);
let ownIdSignature = req.headers['ownid-signature'];
let ownIdTimestamp = req.headers['ownid-timestamp'];
let dataToSign = `${body}.${ownIdTimestamp}`;

// Step 2: Validate the `ownid-timestamp` for expiration
const expirationTimeInSeconds = 60; // Example expiration period of 1 minute
const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds since epoch

if (Math.abs(currentTime - parseInt(ownIdTimestamp)) > expirationTimeInSeconds) {
    // The timestamp has expired
    throw new Error("Request rejected: Signature has expired.");
}

// Step 3: Generate HMAC signature
const hmac = crypto.createHmac('sha256', keyBuffer);
hmac.update(dataToSign);
let signature = hmac.digest('base64');

// Step 4: Compare the generated signature with the one from the header
if (signature !== ownIdSignature) {
    // The request has been tampered with
    throw new Error("Request rejected: Invalid signature.");
}

Complete Code Sample

Using ngrok for Development

During development, you can use ngrok to expose your local server to the internet, allowing OwnID services to communicate with your application:

Start your server with ngrok tunnel

npm run dev:tunnel
ownidEndpointsSample.js
const express = require('express');
const router = express.Router();
const { Client } = require('@microsoft/microsoft-graph-client');
const { ClientSecretCredential } = require('@azure/identity');
const crypto = require('crypto');

const { AZURE_TENANT_ID: tenantId, AZURE_CLIENT_ID: clientId,
  AZURE_CLIENT_SECRET: clientSecret, AZURE_B2C_EXTENSION_APP_ID: b2cExtensionAppId,
  OWNID_SHARED_SECRET: ownIdSharedSecret } = process.env;

const ownIdDataAttributeName = `extension_${b2cExtensionAppId.replace(/-/g, '')}_ownIdData`;

// Verify OwnID request signature to make sure the request came from OwnID
const verifyOwnIdRequest = (req) => {
  console.log('a');
  if (!ownIdSharedSecret) {
    console.warn('OWNID_SHARED_SECRET environment variable is not set. Request verification is disabled.');
    return true;
  }

  const keyBuffer = Buffer.from(ownIdSharedSecret, 'base64');
  const body = JSON.stringify(req.body);
  const ownIdSignature = req.headers['ownid-signature'];
  const ownIdTimestamp = req.headers['ownid-timestamp'];
  if (!ownIdSignature || !ownIdTimestamp) {
    throw new Error("Request rejected: Missing required OwnID headers.");
  }
  const expirationTimeInSeconds = 60000;
  if (Math.abs(Date.now() - parseInt(ownIdTimestamp)) > expirationTimeInSeconds) {
    throw new Error("Request rejected: Signature has expired.");
  }

  const dataToSign = `${body}.${ownIdTimestamp}`;
  const hmac = crypto.createHmac('sha256', keyBuffer);
  hmac.update(dataToSign);
  const signature = hmac.digest('base64');

  if (signature !== ownIdSignature) {
    throw new Error("Request rejected: Invalid signature.");
  }

  return true;
};

const getGraphClient = () => {
  const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
  return Client.initWithMiddleware({
    authProvider: { getAccessToken: async () => (await credential.getToken("https://graph.microsoft.com/.default")).token }
  });
};

// User operations with Graph API
const userOps = {
  findByEmail: async (email) => {
    try {
      const result = await getGraphClient().api('/users').filter(`mail eq '${email}'`).get();
      return result?.value?.[0] || null;
    } catch (error) {
      console.error('Error finding user:', error);
      throw error;
    }
  },

  setData: async (userId, data) => {
    const graphClient = getGraphClient();
    await graphClient.api(`/users/${userId}`).get(); // Verify user exists

    try {
      // Try to create new extension
      await graphClient.api(`/users/${userId}/extensions`).post({
        "@odata.type": "microsoft.graph.openTypeExtension",
        "extensionName": "com.ownid.data",
        "ownIdData": JSON.stringify(data)
      });
    } catch (extError) {
      // If extension exists (409 error), update it
      if (extError.statusCode === 409) {
        await graphClient.api(`/users/${userId}/extensions/com.ownid.data`)
          .patch({ "ownIdData": JSON.stringify(data) });
      } else {
        throw extError;
      }
    }
    return true;
  },

  getData: async (userId) => {
    const graphClient = getGraphClient();
    const user = await graphClient.api(`/users/${userId}`).select('id,displayName').get();

    try {
      const extensions = await graphClient.api(`/users/${userId}/extensions`).get();
      const ownIdExtension = extensions?.value?.find(ext => ext.id === 'com.ownid.data');
      if (ownIdExtension) user.ownIdData = ownIdExtension.ownIdData;
    } catch (extError) {
      // Extension not found, continue
    }

    return user;
  },

  getTokens: async (userId, email) => {
    const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
    const response = await credential.getToken("https://graph.microsoft.com/.default");

    return {
      accessToken: response.token,
      expiresOn: new Date(Date.now() + (response.expiresOnTimestamp - Date.now())),
      scopes: ["https://graph.microsoft.com/.default"],
      account: {
        homeAccountId: userId,
        environment: "login.microsoftonline.com",
        tenantId,
        username: email
      }
    };
  }
};

// API Endpoints
router.post('/setOwnIDDataByLoginId', async (req, res) => {
  try {
    // Verify the OwnID request signature
    try {
      verifyOwnIdRequest(req);
    } catch (verificationError) {
      console.error('Request verification failed:', verificationError.message);
      return res.status(401).json({ error: verificationError.message });
    }

    const { loginId: email, ownIdData } = req.body;
    const user = await userOps.findByEmail(email);

    if (!user) return res.status(404).json({ error: 'User not found' });

    await userOps.setData(user.id, ownIdData);
    return res.sendStatus(204);
  } catch (error) {
    console.error('Error in setOwnIDDataByLoginId:', error);
    return res.status(500).json({ error: 'Internal server error', details: error.message });
  }
});

router.post('/getOwnIDDataByLoginId', async (req, res) => {
  try {
    // Verify the OwnID request signature
    try {
      verifyOwnIdRequest(req);
    } catch (verificationError) {
      console.error('Request verification failed:', verificationError.message);
      return res.status(401).json({ error: verificationError.message });
    }

    const { loginId: email } = req.body;
    const user = await userOps.findByEmail(email);

    if (!user) return res.json({ errorCode: 404 });

    const userData = await userOps.getData(user.id);
    let ownIdData = null;

    if (userData?.ownIdData) {
      try { ownIdData = JSON.parse(userData.ownIdData); }
      catch (e) { console.error('Error parsing OwnID data:', e); }
    }

    return res.json({ ownIdData });
  } catch (error) {
    return res.status(500).json({ error: 'Internal server error', details: error.message });
  }
});

router.post('/getSessionByLoginId', async (req, res) => {
  try {
    // Verify the OwnID request signature
    try {
      verifyOwnIdRequest(req);
    } catch (verificationError) {
      console.error('Request verification failed:', verificationError.message);
      return res.status(401).json({ error: verificationError.message });
    }

    const { loginId: email } = req.body;
    const user = await userOps.findByEmail(email);

    if (!user) return res.status(404).json({ error: 'User not found' });

    try {
      const tokenResponse = await userOps.getTokens(user.id, email);
      return res.json(tokenResponse);
    } catch (tokenError) {
      return res.status(500).json({
        error: 'Error acquiring authentication tokens',
        details: tokenError.message
      });
    }
  } catch (error) {
    return res.status(500).json({ error: 'Internal server error', details: error.message });
  }
});

module.exports = router;

Step 3: Create OwnID Application

An OwnID application connects your backend with the OwnID widget in the front end. This OwnID application is assigned a unique appId that is then added to the website’s front end. To create an OwnID application:

  • Open the OwnID Console and create an account or log in to an existing account.
  • Select Create Application.
  • Define the name of your application, your backend language, and finish the onboarding.

Step 4 - Integrate with your Frontend

Choose your frontend integration path:

Next Steps

Ready to deploy?

YES!

Take me to the Deployment Checklist