Microsoft Entra ID (Azure AD)
Pulls directory, sign-in, and Conditional Access data from Microsoft Graph, deriving KRIs for MFA enrollment, privileged-account hygiene, stale users, CA-policy coverage, guest sprawl, failed-login pressure, and role-assignment change velocity.
At a glance
| Vendor | Microsoft Entra ID (formerly Azure Active Directory) |
|---|---|
| Source type | identity |
| Vendor ID (slug) | entra-id |
| Base URL | https://graph.microsoft.com/v1.0 — commercial cloud. US Gov / China / Germany customers override this manually. |
| Auth method | oauth2 — client-credentials flow. Draxis mints an Authorization: Bearer <access_token> per run via https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token. |
| Schedule default | daily — override to hourly if you want tighter response on failed-login and role-change KRIs. |
| Licensing | Entra ID Free / P1 / P2 all work for most KRIs. Sign-in logs and Conditional Access APIs require P1 or higher; the connector tolerates 403 on those and records the affected KRIs as max-gap (fail-safe). |
| Availability | New in 2026.04. |
Required scopes & roles
Draxis authenticates as an Entra ID app registration using the client-credentials flow — no user is impersonated. The app needs these Microsoft Graph Application permissions (all Read.All, all requiring admin consent):
User.Read.All— list users withuserType,accountEnabled, andsignInActivity. BacksGET /users.Directory.Read.All— list activated directory roles and their members (privileged-account enumeration). BacksGET /directoryRolesandGET /directoryRoles/{id}/members.AuditLog.Read.All— read sign-in logs and directory audits (failed sign-ins, role-management events). Required even forsignInActivityon/users. BacksGET /auditLogs/signInsandGET /auditLogs/directoryAudits.Policy.Read.All— read Conditional Access policies to measure coverage gaps. BacksGET /identity/conditionalAccess/policies.Reports.Read.All— read the aggregate authentication-methods registration report. BacksGET /reports/authenticationMethods/userRegistrationDetails(used instead of per-user calls for MFA enrollment — one paginated call vs. N calls).
Do not grant the app any ReadWrite.All variant, Directory.AccessAsUser.All, or Global Administrator role membership. The connector never writes to your directory.
Setup steps
- Register the app. In the Entra admin center go to Identity → Applications → App registrations → New registration. Name it
Draxis Entra Reader. Supported account types: Single tenant. Redirect URI: leave blank (client-credentials flow doesn’t use one). Click Register. - Copy the three IDs you’ll need. On the new app’s Overview page, copy:
- Application (client) ID — you’ll paste this into Draxis’s Client ID field.
- Directory (tenant) ID — you’ll paste this into Draxis’s Extra Config JSON.
- (The third, Object ID, is not needed by Draxis.)
- Create a client secret. Go to Certificates & secrets → Client secrets → New client secret. Set a descriptive name (
draxis-connector) and a rotation window that matches your policy (Microsoft caps at 24 months). Copy the secret Value — not the Secret ID. You cannot retrieve this value later; if you lose it, create a new secret. - Add the Graph permissions. Go to API permissions → Add a permission → Microsoft Graph → Application permissions. Search for and add each permission exactly:
Do not add the Delegated variants — client-credentials flows can only use Application permissions.User.Read.All Directory.Read.All AuditLog.Read.All Policy.Read.All Reports.Read.All - Grant admin consent. Back on the API permissions page, click Grant admin consent for <your tenant>. The status column should flip to a green checkmark for all five permissions. If this button is disabled, ask a Global Administrator or Privileged Role Administrator to perform the consent.
- (Optional) Restrict by IP. For extra hardening, go to Authentication → Conditional Access on the app and require requests to come from your corporate network or from the Draxis egress range. Skip this on first setup and add it after you’ve confirmed the connector works.
Wire it into Draxis
- Open Settings → Integrations in your tenant.
- Click Add integration and pick Identity Provider as the source type.
- Pick Microsoft Entra ID (Azure AD) from the vendor dropdown. Draxis auto-fills the Graph base URL, the OAuth auth method, the daily schedule, and seeds
extra_config_jsonwith{"tenant_id":""}. - In Client ID, paste the Application (client) ID from step 2 above.
- In Client Secret, paste the secret Value from step 3. Draxis encrypts it server-side with
encryption.keybefore storage. - In the Extra Config JSON field, replace the empty
tenant_idwith your Directory (tenant) ID from step 2, e.g.{"tenant_id":"00000000-0000-0000-0000-000000000000"}. - Click Test. Green means Draxis exchanged the credentials for an access token and read your tenant’s organization record successfully.
- Under KRIs to import, tick the KRIs you want Draxis to manage. All seven
entra_*KRIs are checked by default; uncheck any you don’t need. Selected rows are created on save with the seeded thresholds (tunable later in the KRIs tab). Unchecking a previously-imported KRI deletes it on save. - Save. The connector runs
dailyby default; use Run now from run history to trigger the first sync immediately.
KRIs produced
| Slug | Meaning | Derivation |
|---|---|---|
entra_mfa_enrollment_pct |
% of enabled, non-guest users registered for MFA | round(count(enabled_members where userRegistrationDetails.isMfaRegistered == true) / count(enabled_members) * 100, 1) |
entra_privileged_no_mfa_count |
Members of any activated directory role who are not MFA-registered | count(distinct directoryRole_members where isMfaRegistered != true) — deduped across roles |
entra_stale_accounts_90d |
Enabled, non-guest users with no sign-in in the last 90 days | count(enabled_members where signInActivity.lastSignInDateTime < now - 90d OR signInActivity is null) |
entra_ca_coverage_gaps |
Approximate count of users not covered by a universal MFA policy | 0 if there exists at least one CA policy where state == 'enabled' AND conditions.users.includeUsers contains 'All' AND no excludeUsers AND grantControls.builtInControls contains 'mfa'; else count(enabled_members) |
entra_guest_account_count |
Count of users with userType == 'Guest' |
count(users[].userType == 'Guest') |
entra_failed_signins_24h |
Sign-ins with non-zero errorCode in the last 24 hours |
count(signIns where status.errorCode ne 0 and createdDateTime ge now - 24h) |
entra_priv_role_changes_24h |
Role-management audit events in the last 24 hours | count(directoryAudits where category == 'RoleManagement' and activityDateTime ge now - 24h) |
Each row is a slug the connector writes to. Draxis creates the matching kri rows automatically when you check them in the KRIs to import section of the integration form — no manual API call or seed script needed. Thresholds shown in the table are the seeded defaults; you can edit them freely in the KRIs tab afterwards.
Vendor quirks
- Licensing gates two KRIs.
entra_failed_signins_24handentra_ca_coverage_gapsrequire Entra ID P1 or P2 — Graph returns 403 on those APIs for Free tenants. The connector catches 403/404 and recordsfailed_signins_24h = 0(nothing to measure) andca_coverage_gaps = count(enabled_members)(max gap — fail-safe). A run log warning tells you this happened. - Conditional Access coverage is approximated, not exhaustive. A truly exhaustive coverage check would resolve every policy’s
includeUsers/includeGroups/includeRolesagainstexcludeUsers/excludeGroupsper user — a small policy engine. Draxis’s v1 heuristic answers the 95% question: do you have at least one enabled policy that applies to all users and requires MFA? If yes, coverage is treated as complete. If your posture depends on finer-grained CA modeling, open a support request — a full resolver is on the roadmap. signInActivityneedsAuditLog.Read.All. Even though you’re only reading the/usersendpoint, Graph hidessignInActivityunless the caller holds that permission. Missing it makesentra_stale_accounts_90dspike to the full enabled-member count on the first run.- Tenant ID goes in Extra Config, not the URL. The Graph base URL is shared; the tenant ID only parameterizes the token-endpoint URL. Keeping it in
extra_config_json(seeded as{"tenant_id":""}) lets the same connector serve many Entra tenants without code changes. - Privileged-role enumeration uses activated roles only.
GET /directoryRolesreturns roles that have at least one member; roles no one is assigned don’t appear. That’s the desired behavior here — an unassigned role isn’t a risk. - Client secrets expire. Microsoft caps client-secret lifetime at 24 months. Schedule a rotation in your calendar — when the secret expires, every run starts failing with 401 at token exchange, and the Entra portal will not warn you.
- Non-commercial clouds. For US Government, China (21Vianet), or Germany clouds, override the base URL to the right sovereign endpoint before saving.
Troubleshooting
- HTTP 401 on Test with
invalid_client— the client secret is wrong (you copied the Secret ID instead of the Value) or has expired. Generate a new secret and update Draxis. - HTTP 401 on Test with
AADSTS90002— the tenant ID in Extra Config is wrong. Copy the Directory (tenant) ID from the app registration’s Overview page. - HTTP 403 with
Insufficient privileges— a required Graph permission is missing or admin consent hasn’t been granted. Re-open API permissions and confirm all five are green-checked. - HTTP 403 on
/auditLogs/signInsor/identity/conditionalAccess— tenant doesn’t have Entra ID P1 or higher. Expected — see Quirks above. entra_stale_accounts_90dequals the full enabled-member count —AuditLog.Read.Allis not granted, so Graph stripssignInActivityfrom every user record. The connector treats missing last-sign-in as stale (fail-safe); fix the permission and the number will stabilize on the next run.rowsSkipped > 0androwsWritten = 0— your tenant hasn’t imported any KRIs for this integration yet. Open the integration in Settings → Integrations, tick the KRIs under KRIs to import, and save.- Still stuck? Open a support ticket with the run ID (from Run history) and we’ll dig in.