Extending Azure AD with Custom User Attributes via Microsoft Graph API

Entra ID Sep 14, 2025

This post walks you through adding custom attributes to Azure Active Directory users via Microsoft Graph API and a certificate-based authentication flow. We’ll also examine why it's needed, where to attach these extensions, and how to fetch them securely inside tokens.

Why You Might Need Custom Attributes in Azure AD

Azure AD is great out of the box, but it doesn’t always cover your application-specific needs. Sometimes you need to store metadata like BusinessArea, CompanyID, or internal employeeType flags—data for role-based access control, personalization, or downstream automation.

Azure AD doesn’t allow you to arbitrarily modify its schema for built-in user properties. That’s where directory schema extensions come into play: you define your own fields, attach them to users, and they live in your tenant under your app’s namespace.

The Gotcha: Custom Attributes Are Tied to Applications

You can't just drop custom attributes directly onto a user object and be done with it. These attributes must be registered under an application registration in Azure AD. That app acts as a namespace container. This:

  • will scopes your extension attributes to your solution
  • Allows Microsoft Graph to associate and manage them securely
  • Prevents attribute name collisions with other extensions

Once registered, they show up in user objects like this:

"extension_<appClientId>_<attributeName>": "value"

Script Walkthrough: Certificate-Based Graph API Call

Let’s walk through a production-grade Python script that does all the heavy lifting (script will be listed at the end):

1. Authenticate with Microsoft Graph

We authenticate using a certificate (pfx), not a client secret. This is safer in enterprise environments.

# Build and sign a JWT with RS256
# Exchange it for an OAuth2 access token

Why it matters:

  • Client secrets are static and vulnerable.
  • Certificates are time-bound and revocable.
  • JWT assertions let you go passwordless.

2. Define and Register Extension Properties

Once authenticated, we hit this Graph endpoint:

POST https://graph.microsoft.com/beta/applications/{app_id}/extensionProperties

Each property looks like:

{
  "name": "Company",
  "dataType": "String",
  "targetObjects": ["User"]
}

You can define up to 100 extension properties per app.

3. Example Payload:

custom_properties = [
    {"name": "BusinessArea", "dataType": "String", "targetObjects": ["User"]},
    {"name": "Company", "dataType": "String", "targetObjects": ["User"]},
    ...
]

Each one gets registered, stored under the app, and becomes available for read/write on user objects.

How to Fetch Extension Attributes in an Access Token

Once the attributes are set on a user, you probably want them inside your JWT access token to use in downstream apps.

  1. In your app registration, configure the token configuration.
  2. Add a directory schema extension claim:
    • Name: extension_<appId>_<propertyName>
    • Source: Directory schema extension
    • Emit in token type: ID token or Access token (depends on your use case)

Example output inside the token:

{
  "extension_2d5a6f22f9a14321b23b5226531dc607_Company": "Contoso",
  "extension_2d5a6f22f9a14321b23b5226531dc607_EmployeeType": "Contractor"
}

TL;DR

  • Azure AD lacks app-specific user attributes by default.
  • Custom attributes let you fill that gap, but they must be registered under an app.
  • Certificate-based auth > client secret for automation.
  • You can surface the attributes in tokens by modifying the app registration.

This is not optional if you're building identity-aware apps that depend on enriched user context. It’s essential. So, have fun using this script as a template:

import json
import base64
import uuid
import time
import requests
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.backends import default_backend

# Define your Azure AD tenant ID, client ID, and certificate path
tenant_id = "....-ae7f-d420adbc7dff"
client_id = "....-437e61ad1f82"

cert_path = "C:/Path/to/Your/Cert/certificate.pfx"
cert_password = b"SUPER_SECRET_PASSWORD"

# Define the application object ID (as container)
app_id = "...-5226531dc607"

# Load the certificate and private key
from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates
with open(cert_path, "rb") as cert_file:
    private_key, certificate, additional_certs = load_key_and_certificates(cert_file.read(), cert_password, default_backend())

# Create a JWT token for authentication
now = int(time.time())
exp = now + 3600  # Token valid for 1 hour
jwt_header = {
    "alg": "RS256",
    "typ": "JWT",
    "x5t": base64.urlsafe_b64encode(certificate.fingerprint(hashes.SHA1())).decode().rstrip("=")
}
jwt_payload = {
    "aud": f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
    "iss": client_id,
    "sub": client_id,
    "jti": str(uuid.uuid4()),
    "nbf": now,
    "exp": exp
}

# Encode the JWT token
header_encoded = base64.urlsafe_b64encode(json.dumps(jwt_header).encode()).decode().rstrip("=")
payload_encoded = base64.urlsafe_b64encode(json.dumps(jwt_payload).encode()).decode().rstrip("=")
token_to_sign = f"{header_encoded}.{payload_encoded}"

# Sign the JWT token using the certificate's private key
signature = private_key.sign(
    token_to_sign.encode(),
    padding.PKCS1v15(),
    hashes.SHA256()
)
signature_encoded = base64.urlsafe_b64encode(signature).decode().rstrip("=")
client_assertion = f"{token_to_sign}.{signature_encoded}"

# Define the resource and scope
scope = "https://graph.microsoft.com/.default"

# Define the URL for obtaining the access token
url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"

# Define the body for the API request
body = {
    "client_id": client_id,
    "scope": scope,
    "client_assertion": client_assertion,
    "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    "grant_type": "client_credentials"
}

# Send the API request and store the response
response = requests.post(url, data=body)
response.raise_for_status()
access_token = response.json()["access_token"]

# Display the access token
print(f"Access Token: {access_token}")


# Define the Graph API URL for extensionProperties
url = f"https://graph.microsoft.com/beta/applications/{app_id}/extensionProperties"

# Define the headers for the API request
headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json"
}

# Define the body for the API request
custom_properties = [
    {"name": "CompanyId", "dataType": "String", "targetObjects": ["User"]},
]

for body in custom_properties:
    response = requests.post(url, headers=headers, json=body)
    response.raise_for_status()
    print(response.json())

# Send the API request and store the response
#response = requests.get(url, headers=headers, json=body)
response = requests.post(url, headers=headers, json=body)
response.raise_for_status()

# Display the response
print(response.json())

Tags