Microsoft Entra ID (Azure AD)

How to set up Microsoft Entra ID, formerly Azure Active Directory, as an external identity provider for Stroom.

This page covers using Microsoft Entra ID as Stroom’s Identity Provider (IDP) Identity Provider (IDP) An Identity Provider is a system or service that can authenticate a user and assert their identity. Identity providers can support single sign on (SSO), which allows the user to sign in once to the Identity Provider so they are then authenticated to all systems using that IDP.Click to see more details.... Entra ID is the current name for what was Azure Active Directory, and much of the tooling and documentation still says Azure AD.

Entra ID has two generations of endpoint, v1.0 and v2.0, which issue tokens with different issuers and different formats. Two of the three things most likely to go wrong here come from mixing them up, so it is worth being deliberate: use the v2.0 endpoints throughout.

Creating the App Registration

In the Microsoft Entra admin centre , or the Azure portal under Microsoft Entra ID:

  1. Go to App registrations => New registration.
  2. Give it a name, e.g. Stroom.
  3. For Supported account types choose Accounts in this organizational directory only, i.e. single tenant, unless you have a specific reason not to. This restricts sign in to your own tenant.
  4. Under Redirect URI select a platform of Web and enter https://STROOM_FQDN/api/auth/flow/v1/signin-oidc.
  5. Click Register, then note the Application (client) ID and the Directory (tenant) ID from the overview page.

Where STROOM_FQDN is the public address of Stroom, i.e. what you have set as appConfig.publicUri.

Then, still in the app registration:

  1. Under Authentication, add a Front-channel logout URL of https://STROOM_FQDN/, and add the same value under Redirect URIs if your tenant requires post logout redirect URIs to be registered.
  2. Under Certificates & secrets => Client secrets, create a new secret and copy its Value immediately, as it is only shown once.

Entra ID supports PKCE, and Stroom always sends an S256 challenge, so there is nothing to configure for it.

Exposing an API for Access Tokens

This step is what makes API authentication work, and is the Entra ID equivalent of KeyCloak’s audience mapper.

If Stroom only ever asks for the openid, email and profile scopes, Entra ID issues an access token for Microsoft Graph rather than for Stroom. Those tokens are intended only for Graph, are not in a format a third party can validate, and will fail validation at Stroom. Interactive sign in still works throughout, because it uses the id_token.

To get an access token that Stroom can validate, the app registration has to expose an API of its own:

  1. Go to Expose an API => Add next to Application ID URI. Accept the default of api://CLIENT_ID, or set your own.
  2. Click Add a scope, name it something like user_impersonation, and choose who can consent.
  3. Under Manifest, set accessTokenAcceptedVersion to 2.

Callers then request that scope, e.g. api://CLIENT_ID/user_impersonation, and the resulting access token carries an aud claim that Stroom can be configured to accept.

Configuring Stroom

  receive:
    # Set to true to require authentication for /datafeed requests
    authenticationRequired: true
    # Set to true to allow authentication using an Open ID token
    tokenAuthenticationEnabled: true
  security:
    authentication:
      authenticationRequired: true
      openId:
        identityProviderType: EXTERNAL_IDP
        # Note the '/v2.0' path part. Without it you get the v1.0 endpoints and a different issuer.
        openIdConfigurationEndpoint: "https://login.microsoftonline.com/TENANT_ID/v2.0/.well-known/openid-configuration"
        # The Application (client) ID from the app registration overview
        clientId: "CLIENT_ID"
        clientSecret: "CLIENT_SECRET"
        logoutEndpoint: "https://login.microsoftonline.com/TENANT_ID/oauth2/v2.0/logout"
        # Accept both the id_token audience (the client id) and the access token audience
        # (the Application ID URI). Adjust to match what your tokens actually carry.
        allowedAudiences:
          - "CLIENT_ID"
          - "api://CLIENT_ID"
        # Accept tokens from both the v2.0 and the v1.0 endpoints. Setting this replaces
        # Stroom's default issuer check, so the v2.0 issuer must be listed too.
        validIssuers:
          - "https://login.microsoftonline.com/TENANT_ID/v2.0"
          - "https://sts.windows.net/TENANT_ID/"
        # Entra ID only issues the 'name' and 'preferred_username' claims when 'profile'
        # is requested, and Stroom's default scopes do not include it.
        requestScopes:
          - "openid"
          - "email"
          - "profile"
        # 'oid' is stable across app registrations; the default of 'sub' is not.
        uniqueIdentityClaim: "oid"

Replace TENANT_ID with the Directory (tenant) ID, CLIENT_ID with the Application (client) ID, and CLIENT_SECRET with the value of the client secret, all from the app registration’s Overview and Certificates & secrets pages.

Each of validIssuers, requestScopes and uniqueIdentityClaim is explained in the sections that follow.

Issuers

The v2.0 discovery endpoint advertises an issuer of https://login.microsoftonline.com/TENANT_ID/v2.0, which is a parent path of the discovery endpoint itself, so Stroom’s issuer check is satisfied with no extra configuration.

The v1.0 endpoints are not so tidy. Their issuer is https://sts.windows.net/TENANT_ID/, which shares no base URI with the discovery endpoint, and Stroom will refuse to start with:

Issuer ‘X’ obtained from configuration endpoint Y does not share the same base URI.

If you must use v1.0, or you have v1.0 access tokens in circulation from an app registration you cannot change, list both issuers explicitly, as in the example above:

        validIssuers:
          - "https://login.microsoftonline.com/TENANT_ID/v2.0"
          - "https://sts.windows.net/TENANT_ID/"

Stroom then accepts tokens carrying either issuer, so v1.0 and v2.0 tokens can be in use at the same time.

Using the v2.0 endpoints and accessTokenAcceptedVersion: 2 is much the better answer, in which case validIssuers can be omitted altogether.

Audience Validation

An Entra ID id_token carries aud set to the Application (client) ID, so interactive sign in validates against clientId with no further configuration.

An access token for your exposed API carries aud set to either the Application ID URI or the client id, depending on accessTokenAcceptedVersion and how the scope was requested. Listing both in allowedAudiences, as above, covers either.

Leave validateAudience at its default of true.

Claims

Entra ID v2.0 issues preferred_username, normally the user principal name, which Stroom uses as the display name by default, and name, which satisfies the default fullNameClaimTemplate of ${name}.

Both of those claims are only issued when the profile scope is requested. Stroom’s default requestScopes is openid and email, which does not include it, so without the requestScopes shown in the example above users will sign in with no display name or full name. Setting requestScopes replaces the defaults rather than adding to them, so keep openid and email in the list.

For uniqueIdentityClaim you have a choice:

Claim Notes
sub The Stroom default. In Entra ID this is pairwise, i.e. a different value per application, and stable for the life of that app registration. Delete and recreate the app registration and every user’s sub changes, orphaning their Stroom user.
oid The user’s object id in the directory. Stable across applications and across app registrations, so it survives a re-registration. Unique within a tenant.

oid is the more robust choice for a single tenant deployment, and is what Microsoft’s own guidance points to as the durable identifier. sub is fine if you are confident the app registration will not be recreated. The example above uses oid; remove that line to keep the default of sub.

Group and Role Claims

Entra ID can be configured to emit groups and roles claims. Stroom does not consume them. All authorisation is done with Stroom’s own users, groups and permissions, so directory group membership has no effect on what a user can do in Stroom.

Access Token Type

Leave requiredAccessTokenType unset until you have decoded the header of a real access token from your tenant and confirmed what it contains. Setting it to a value your tokens do not use will refuse every API call.

Setting up the Admin User in Stroom

Find the identifier of the account that is to be the administrator, matching whatever you set uniqueIdentityClaim to.

If you are using oid, it is shown as the Object ID on the user’s page under Users in the Entra admin centre. If you are using sub, it is pairwise and not shown anywhere in the portal, so you will need to decode an id_token issued for that user, or have them sign in once and read it from the Stroom logs.

Then run the following, ideally before Stroom has been started for the first time:

subject_id="XXX"; \
java -jar /absolute/path/to/stroom-app-all.jar \
  manage_users \
  --createUser "${subject_id}" \
  --createGroup Administrators \
  --addToGroup "${subject_id}" Administrators \
  --grantPermission Administrators "Administrator" \
  ../local.yml

The command is repeatable and will skip anything that already exists, so running it against a user who has already signed in is fine. Restart Stroom afterwards if it was running, as permissions are cached.

Stroom-Proxy with Entra ID

A Stroom-Proxy obtains a token for its own service user using the client credentials grant.

Create a second app registration for the proxy, then grant it access to the API exposed by the Stroom app registration:

  1. In the proxy’s app registration, go to API permissions => Add a permission => My APIs and select the Stroom app registration.
  2. Choose Application permissions, which is the client credentials case, rather than delegated permissions.
  3. Have a directory administrator grant admin consent, without which the grant will fail.

Entra ID’s client credentials flow uses the .default scope of the target API:

  security:
    authentication:
      openId:
        identityProviderType: EXTERNAL_IDP
        openIdConfigurationEndpoint: "https://login.microsoftonline.com/TENANT_ID/v2.0/.well-known/openid-configuration"
        # The proxy's own app registration
        clientId: "PROXY_CLIENT_ID"
        clientSecret: "PROXY_CLIENT_SECRET"
        # The Application ID URI of the Stroom app registration, not the proxy's
        clientCredentialsScopes:
          - "api://CLIENT_ID/.default"

The destination the proxy forwards to must accept the audience these tokens carry, which will be the Application ID URI or client id of the Stroom app registration, so make sure it appears in that destination’s allowedAudiences.

Obtaining a Token to Send Data

When a Stroom-Proxy, or Stroom itself, is configured with Entra ID as above and has receive.authenticationRequired and receive.tokenAuthenticationEnabled both set to true, anything sending data to /datafeed must present an Entra ID access token, or a certificate, with each request. See Token Authentication for how the token is attached.

This section covers how a sending system gets that token. It is the mirror image of Stroom-Proxy with Entra ID: the sender is a client of the API exposed by the Stroom app registration in exactly the same way the proxy is, and uses the same client credentials grant.

Registering the Sender

Each sending system needs an app registration of its own, so that it has a client id and secret to authenticate with and can be granted, or revoked, individually.

  1. Create an app registration for the sender, e.g. Stroom Sender - Widget Service. No redirect URI is needed as it will never sign in interactively.
  2. Under Certificates & secrets, create a client secret and record its value.
  3. Under API permissions => Add a permission => My APIs, select the Stroom app registration.
  4. Choose Application permissions, not delegated permissions.
  5. Have a directory administrator grant admin consent, without which the token request will fail.

The Stroom app registration must already expose an API with accessTokenAcceptedVersion set to 2. If it does not, the token comes back as a v1.0 token with an issuer of https://sts.windows.net/TENANT_ID/ and is refused unless that issuer is listed in validIssuers, see Issuers.

Requesting the Token

The sender asks the v2.0 token endpoint for a token using the client credentials grant. The scope is the .default scope of the Stroom app registration’s Application ID URI, not openid or email; asking for those returns a token for Microsoft Graph that Stroom cannot validate.

TENANT_ID="<TENANT_ID>"; \
SENDER_CLIENT_ID="<SENDER_CLIENT_ID>"; \
SENDER_CLIENT_SECRET="<SENDER_CLIENT_SECRET>"; \
STROOM_APP_ID_URI="api://<CLIENT_ID>"; \
TOKEN="$( \
  curl \
    --silent \
    --request POST \
    --header "Content-Type: application/x-www-form-urlencoded" \
    --data "grant_type=client_credentials" \
    --data "client_id=${SENDER_CLIENT_ID}" \
    --data "client_secret=${SENDER_CLIENT_SECRET}" \
    --data "scope=${STROOM_APP_ID_URI}/.default" \
    "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token" \
  | jq -r '.access_token' \
)"

Where:

  • TENANT_ID is the Directory (tenant) ID.
  • SENDER_CLIENT_ID and SENDER_CLIENT_SECRET are from the sender’s app registration.
  • STROOM_APP_ID_URI is the Application ID URI of the Stroom app registration, as shown under Expose an API. The default form is api://<CLIENT_ID>, where CLIENT_ID is the Stroom app registration’s client id, not the sender’s, and it is the same value used in the proxy’s clientCredentialsScopes above.

The response is a JSON document containing access_token, token_type and expires_in; the command above extracts just the token. A 400 response instead carries error and error_description fields, the latter with an AADSTS code and a message that says what is wrong. The usual causes are a wrong or expired client secret, a scope that is not the .default scope of an API the Stroom app registration actually exposes, or admin consent not having been granted.

Sending Data with the Token

The token goes in the Authorization header as a bearer token, alongside the usual header arguments:

curl \
  --silent \
  --request POST \
  --header "Authorization: Bearer ${TOKEN}" \
  --header "Feed: <FEED_NAME>" \
  --header "System: <SYSTEM_NAME>" \
  --header "Environment: <ENVIRONMENT>" \
  --data-binary @events.log \
  "https://stroom-proxy.example.com/stroom/datafeed"

Tokens are short lived, typically around an hour, so a sender must request a fresh token when expires_in has elapsed or a request is refused with a 401, rather than caching one indefinitely.

Checking the Token

If the proxy rejects the token, decode its payload before assuming the proxy is at fault:

echo "${TOKEN}" \
  | cut -d '.' -f 2 \
  | tr '_-' '/+' \
  | base64 --decode 2>/dev/null \
  | jq '.'

Check that:

  • iss is https://login.microsoftonline.com/TENANT_ID/v2.0. If it is https://sts.windows.net/TENANT_ID/ the Stroom app registration is still issuing v1.0 tokens, see Exposing an API for Access Tokens.
  • aud is a value that appears in the proxy’s allowedAudiences. It will be the Application ID URI or the client id of the Stroom app registration.
  • exp is in the future.

Once accepted, the sender’s identity is recorded against the received data in the UploadUserId meta attribute, taken from the claim named in uniqueIdentityClaim. For a client credentials token that is the oid of the sender’s service principal, so it is that value, not the app registration’s display name, that you match on in Data Receipt Rules.

Last modified September 23, 2026: #140 Add auth flow sequence diagrams (860d6f4)