Skip to main content

Microsoft Entra ID

This guide sets up federated OIDC access tokens with Microsoft Entra ID. Your backend uses the Entra On-Behalf-Of (OBO) flow to exchange a signed-in user's token for an access token issued for ABsmartly, and calls the ABsmartly REST API (/v1) or your instance's /mcp endpoint with it. Each call acts as the matching ABsmartly user, with exactly that user's permissions.

Read Federated OIDC access tokens first for how the feature works, how to call /v1 and /mcp, the errors and the security model. This page covers only the Entra ID setup.

Enabled on request

Federated tokens are off by default. ABsmartly support turns them on for your instance after you complete the setup on this page and send the details we need.

Prerequisites​

  • SAML SSO with Entra ID is already set up for your instance, in the same tenant. See SAML SSO - Azure.
  • Each user signs in once with SAML SSO after federated tokens are enabled, including users who already exist in ABsmartly. This links their Entra object ID to their ABsmartly user. The user must not be archived.
  • Your backend application is registered in Entra ID (or you can register it) and can keep a client secret or certificate.
  • You can grant admin consent in the tenant.

Step 1: Register an app that represents ABsmartly​

This app registration is the "API" that your backend requests tokens for. It is separate from the enterprise application you created for SAML SSO.

  1. Go to the Microsoft Entra admin center and head to Identity > Applications > App registrations > New registration.
  2. Name it, for example ABsmartly API.
  3. Under Supported account types, choose Accounts in this organizational directory only (Single tenant).
  4. Leave Redirect URI empty and click Register.
  5. On the Overview page, copy the Application (client) ID and the Directory (tenant) ID. You send both to ABsmartly later.

Set the Application ID URI and expose the scopes​

  1. In the inner left pane, click Expose an API.

  2. Next to Application ID URI, click Add and keep the suggested value (api://<application-client-id>) or enter your own. Click Save.

  3. Click + Add a scope and fill in:

    • Scope name: mcp.access
    • Who can consent?: Admins only
    • Admin consent display name: Access ABsmartly MCP as the signed-in user
    • Admin consent description: Allows the app to use the ABsmartly MCP server on behalf of the signed-in user.
    • State: Enabled

    Click Add scope.

  4. If you want your backend to call the REST API directly, click + Add a scope again and fill in:

    • Scope name: api.access
    • Who can consent?: Admins only
    • Admin consent display name: Access the ABsmartly API as the signed-in user
    • Admin consent description: Allows the app to call the ABsmartly REST API on behalf of the signed-in user.
    • State: Enabled

    Click Add scope.

mcp.access grants access to /mcp and api.access grants access to /v1. See REST API and MCP access. These are the scope names we use by convention. ABsmartly configures the scopes for your instance, so if you need different names, agree them with ABsmartly support and use them everywhere this page says mcp.access or api.access.

Use version 2 access tokens​

  1. In the inner left pane, click Manifest.

  2. Set the access token version to 2 and click Save. The property name depends on which manifest format the portal shows:

    • Microsoft Graph app manifest (the current default): under api, set requestedAccessTokenVersion.

      "api": {
      "requestedAccessTokenVersion": 2
      }
    • AAD Graph app manifest (legacy): set the top-level accessTokenAcceptedVersion.

      "accessTokenAcceptedVersion": 2

ABsmartly checks the token's issuer exactly against the version 2 issuer, https://login.microsoftonline.com/<tenant-id>/v2.0. Version 1 tokens have a different issuer (https://sts.windows.net/<tenant-id>/) and are rejected.

In version 2 access tokens, the audience (aud) is the ABsmartly app's Application (client) ID, a GUID, not its Application ID URI. ABsmartly expects that GUID, which is why you send it to support in Step 4.

ABsmartly links the Entra oid claim (the user's stable object ID) to the user when they sign in through your existing SAML SSO provider. It does not match on email, upn or preferred_username. Entra access tokens already contain oid and tid, so you don't need an email optional claim on the API app.

In the enterprise application used for SAML SSO (not the new API app), open Single sign-on > Attributes & Claims. Ensure these single-valued attributes are included in the assertion:

Attribute nameSource attribute
http://schemas.microsoft.com/identity/claims/objectidentifieruser.objectid
http://schemas.microsoft.com/identity/claims/tenantidYour Directory (tenant) ID

The objectidentifier must equal the access token's oid, and tenantid must match its tid. If you use different assertion attribute names, send those names to ABsmartly support so they can configure the mapping. Keep the existing SAML Name ID and other claims used for Web Console sign-in.

ABsmartly records the user's oid and tenant at their next SAML sign-in. After that, it only accepts tokens whose tid matches the recorded tenant.

Every user must sign in once through SAML after federated tokens are enabled, even if they already have an account or browser session. There is no bulk import. The first sign-in decides the link, so check that every user's email in Entra ID maps to the right ABsmartly user before you enable it. Links are never reassigned automatically; see Link users through SAML SSO for details and for how to fix a wrong link.

Step 2: Give your backend app permission to call ABsmartly​

  1. In App registrations, open the app registration of your backend application (or create one with New registration).
  2. Click Certificates & secrets and add a client secret or a certificate, if the app doesn't have one yet. Your backend needs it for the OBO request.
  3. Click API permissions > + Add a permission > APIs my organization uses, and select the ABsmartly app from Step 1 (for example ABsmartly API).
  4. Choose Delegated permissions, tick mcp.access, and api.access if you added it, and click Add permissions.
  5. Click Grant admin consent for <your tenant> and confirm. The permission's status changes to Granted.
  6. On the Overview page, copy the backend app's Application (client) ID. ABsmartly only accepts tokens requested by the client IDs you send.
tip

You can also pre-authorize your backend app on the ABsmartly app registration: in Expose an API > Authorized client applications, add the backend app's client ID and tick the mcp.access scope, and api.access if you added it. Users are then never asked to consent to them.

Step 3: Check who can use it​

Any user in your tenant that your backend can get a token for can reach ABsmartly, but only if they also exist and are active in ABsmartly. To limit this further, open the ABsmartly app's enterprise application (Identity > Applications > Enterprise applications), set Assignment required? to Yes under Properties, and assign the users or groups that may use it under Users and groups.

Step 4: Send the details to ABsmartly support​

Send these values to ABsmartly support, for example through your Slack channel or by email:

ValueWhere to find it
Directory (tenant) IDOverview page of either app registration
Application ID URI of the ABsmartly appExpose an API (for example api://…)
Application (client) ID of the ABsmartly appOverview page of the ABsmartly app
Application (client) ID of your backend app (one or more)Overview page of your backend app
Capabilities and scope namesThe scopes you added in Step 1: mcp.access for MCP access, and api.access if you want REST API access
SAML SSO provider and assertion attributesThe existing SAML application, its objectidentifier and tenantid attribute names; the token subject is oid
Your instance URLFor example https://your-instance.absmartly.com

None of these values is secret. Never send a client secret or a token.

ABsmartly confirms when federated tokens are enabled for your instance.

Request a token with the On-Behalf-Of flow​

Your backend exchanges the user's incoming access token (the one issued for your backend app) for a token issued for ABsmartly:

POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&client_id=<backend-app-client-id>
&client_secret=<backend-app-client-secret>
&assertion=<the user's access token for your backend app>
&scope=api://<absmartly-app-client-id>/mcp.access profile
&requested_token_use=on_behalf_of

Request the scope for the endpoint you call:

You callscope
/mcpapi://<absmartly-app-client-id>/mcp.access
/v1api://<absmartly-app-client-id>/api.access
Both, with one tokenapi://<absmartly-app-client-id>/mcp.access api://<absmartly-app-client-id>/api.access

Both scopes belong to the same ABsmartly app, so the token's audience is the same either way, and a token requested with both scopes carries both in scp. Add profile to whichever scope you request, as in the example above, so the token carries the oid and tid claims ABsmartly needs. MSAL adds it for you. Use your own Application ID URI in scope if you changed it in Step 1. The access_token in the response is the token you send to ABsmartly. It expires after 60 to 90 minutes, so request a new one when it expires.

The same exchange with MSAL for Python:

import msal

app = msal.ConfidentialClientApplication(
client_id="<backend-app-client-id>",
client_credential="<backend-app-client-secret>",
authority="https://login.microsoftonline.com/<tenant-id>",
)

result = app.acquire_token_on_behalf_of(
user_assertion=incoming_user_access_token,
# Use .../api.access for /v1, or list both scopes for a token that works on both.
scopes=["api://<absmartly-app-client-id>/mcp.access"],
)

if "access_token" not in result:
raise RuntimeError(result.get("error_description"))

absmartly_token = result["access_token"]

acquire_token_on_behalf_of always calls Entra ID for a new token. It doesn't return a cached one. Keep the token for each user and scope set, and use it again until it is close to the time given in expires_in. Then request a new one. Don't send one user's token on another user's requests.

note

Don't send the user's ID token, the token for your own backend app, or a Microsoft Graph token. ABsmartly only accepts access tokens issued for the ABsmartly app registration.

Call ABsmartly with the token​

Send the access_token from the OBO response as Authorization: Bearer <token> on every request to https://your-instance.absmartly.com/v1/... or https://your-instance.absmartly.com/mcp. See Call the REST API with the token and Call the MCP server with the token for full examples.

What ABsmartly checks in Entra ID tokens​

ABsmartly applies all the token requirements. For Entra ID they mean:

CheckToken claimMust be
IssuerissExactly https://login.microsoftonline.com/<tenant-id>/v2.0
AudienceaudThe ABsmartly app's Application (client) ID
TenanttidYour Directory (tenant) ID. Tokens from other tenants are rejected.
Calling appazpOne of the backend app client IDs you sent to ABsmartly
ScopescpIncludes mcp.access for /mcp, or api.access for /v1. App roles alone are not accepted.
User identityoidThe object ID linked to an active ABsmartly user during SAML sign-in
Token typeidtypNot app. App-only tokens from the client credentials flow are rejected, because they don't act for a user.

Revoking access​

Entra ID access tokens can't be revoked early. To cut a user off, archive the user in ABsmartly (Settings > Users), which takes effect on the next request, and disable the user in Entra ID or remove their assignment so no new tokens are issued. A token that was already issued stays valid in Entra ID until it expires (60 to 90 minutes), but ABsmartly rejects it once the user is archived.

Troubleshooting​

Rejected tokens come back with an error_description that says why. See Error reasons for the full list. These problems are specific to Entra ID:

SymptomLikely causeFix
audience not accepted, and aud is your backend app's client IDYou sent the user's ID token or the token for your backend app. Sending an Entra ID token instead of the OBO result usually shows up this way.Exchange it with the OBO flow and send the resulting access token
audience not accepted, and aud is 00000003-0000-0000-c000-000000000000 or https://graph.microsoft.comThe token is a Microsoft Graph tokenRequest the token with scope api://<absmartly-app>/mcp.access (or api.access), not User.Read or .default for Graph
audience not accepted, and aud is api://… instead of a GUIDThe ABsmartly app issues version 1 tokensSet the access token version to 2 in the ABsmartly app's manifest, then request a new token
issuer not accepted, and iss is https://sts.windows.net/<tenant-id>/A version 1 token with the wrong issuer.Set the access token version to 2 in the manifest, then request a new token
tenant not acceptedThe token comes from another tenant, for example a guest user's home tenantRequest the token from your own tenant's endpoint
required scope missing (403)scp doesn't contain the scope named in the WWW-Authenticate header (mcp.access on /mcp, api.access on /v1): the scope wasn't requested, or consent wasn't grantedRequest api://<absmartly-app>/<scope> and grant admin consent
federated tokens are not accepted on this endpoint on /v1REST API access isn't enabled for your instanceAsk ABsmartly support to enable it with your API scope, or call /mcp
token subject claim missingThe token has no oidSend a delegated access token issued for the ABsmartly app
no active ABsmartly user for this identityThe user hasn't signed in with SAML since federated tokens were enabled, the SAML assertion lacks the object ID attribute, the linked user is archived, or the token is from a different tenant than the one recorded at SAML sign-in (or has no tid)Have the user sign in with SAML SSO once, and request the token from the same tenant the user signs in to ABsmartly with. If the link is wrong, fix it.
app-only tokens are not acceptedThe token comes from the client credentials flow, not OBOUse the OBO flow with a signed-in user's token
The OBO request fails with AADSTS65001Admin consent wasn't granted to your backend appGrant admin consent in API permissions
The OBO request fails with AADSTS50013 or AADSTS50027The assertion isn't a valid access token for your backend appPass the user's access token for your backend app, not an ID token