> ## Content Index
> Fetch the complete content index at: https://blog.bajonczak.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Extending Azure AD with Custom User Attributes via Microsoft Graph API
- URL: https://blog.bajonczak.com/extending-azure-ad-with-custom-user-attributes-via-microsoft-graph-api/
- Published: 2025-09-14T09:15:34.000Z
- Updated: 2026-06-15T16:21:17.000Z
- Description: 🔐 Extend Azure AD with custom user attributes using Python & certificates! Learn why you need them, how to add them via Graph API, and fetch them in tokens for smarter apps 🚀 #Azure #GraphAPI #Identity
- Author: Sascha Bajonczak
- Tags: Microsoft 365, Identity

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:

```json
"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.

```python
# 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:

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

```

You can define up to 100 extension properties per app.

### 3\. Example Payload:

```python
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:

```json
{
  "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:

```python
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())

```