Azure Identity

Configuring SAML SSO for an Enterprise App in Entra ID: A Field Guide

A vendor sends you a one-line request: “Please set up SAML SSO so our staff can sign into AcmeHR with their corporate accounts.” Four screenshots and two acronyms later you are staring at a field labelled Entity ID (SP) with no idea whether the value belongs there or in the box marked Identifier, and the vendor’s setup wizard wants something called an ACS URL that your Entra portal calls a Reply URL. Welcome to SAML federation, where two correct systems describe the same three values with five different names, and a single trailing slash mismatch produces a login page that bounces forever. This guide makes that wiring boring and repeatable.

SAML (Security Assertion Markup Language) 2.0 is the XML-based federation protocol that lets a third-party SaaS app — the service provider (SP) — delegate authentication to Microsoft Entra ID acting as the identity provider (IdP). The user lands on the app, the app redirects them to Entra, Entra authenticates them (password, MFA, whatever your Conditional Access says), and Entra hands back a signed XML assertion that says “this is jane@contoso.com, here are her attributes.” The app trusts that signature and logs her in. No password ever reaches the app. You configure this once per app as an Enterprise Application in your tenant, and the whole flow hinges on exactly three URLs, one signing certificate, and a handful of claims.

This is a step-by-step implementation field guide, not a protocol lecture. By the end you will have stood up a working SAML SSO integration three ways — clicking through the Entra admin center, scripting it with az and Microsoft Graph, and declaring it in Bicep — and you will be able to read a failed sign-in and name the exact misconfiguration behind every common AADSTS error. We keep the protocol theory to the minimum you need to make correct decisions, then spend the bulk of the article in the portal and at the command line doing the real thing, with the expected output at each step, validation, and teardown.

What problem this solves

Every organisation accumulates SaaS: an HR system, an expense tool, a learning platform, a ticketing app, a dozen more. Each one ships with its own username-and-password login. Left alone, that means a separate credential per app per employee — passwords that get reused, never rotated, and walk out the door the day someone resigns because nobody remembers the app exists. There is no central MFA, no Conditional Access, no audit trail of who signed in from where, and offboarding is a frantic spreadsheet exercise.

SAML SSO collapses all of that into one identity. The user authenticates once against Entra ID — under your password policy, your MFA, your device-compliance and risk-based rules — and the app simply trusts the result. When you disable the user in Entra, every federated app instantly stops letting them in. When you require MFA, every app gets it for free without the vendor writing a line of code. Sign-in logs for all apps land in one place. This is the entire point of an identity provider: authenticate in one trusted spot, assert the result everywhere.

Who hits this: anyone administering Entra ID for a real organisation. The pain shows up the first time you onboard a SaaS vendor and discover the SAML wizard speaks a different dialect than the Entra blade — SP versus Reply URL, NameID versus name, signed assertion versus signed response. It bites again at the audit, when security asks “show me every app a departing admin could still access” and you realise half of them never federated. Getting SAML SSO right, repeatably, is foundational identity hygiene — and it is shockingly easy to get almost right in a way that works for you and silently breaks for the user with a slightly different attribute.

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need a Microsoft Entra ID tenant and an account holding at least the Cloud Application Administrator or Application Administrator role (Global Administrator works but is overkill — least privilege matters here). You should be comfortable in the Entra admin center, running az in Cloud Shell, and reading JSON. Basic familiarity with HTTP redirects, X.509 certificates and XML helps but is not required — we explain the moving parts as we go. The target SaaS app must support SAML 2.0 (almost all enterprise SaaS does); for apps that only speak modern protocols you would use OIDC instead, covered in OIDC and OAuth2 on Entra ID: Choosing the Right Flow (Auth Code, PKCE, Client Credentials).

This sits in the Identity & Access track. It builds directly on the object model explained in App Registrations vs Enterprise Applications: The Service Principal Model Explained — the Enterprise Application you configure here is a service principal, the local representation of the app in your tenant. Once SSO works, you protect it with Deploying Conditional Access Safely: Report-Only Rollout to Enforcement and scope access with Entra ID Dynamic Groups: A Membership-Rule Cookbook That Actually Scales. Where this fits in the bigger identity picture:

Layer What it does Article
Object model App registration vs enterprise app vs service principal App Registrations vs Enterprise Applications
Protocol choice When SAML vs OIDC/OAuth2 OIDC and OAuth2 on Entra ID
This article Wiring SAML SSO end to end
Access control Who can sign in, under what conditions Deploying Conditional Access Safely
Assignment at scale Group-driven access Entra ID Dynamic Groups
Licensing App access via group-based licensing Group-Based Licensing Errors in Entra ID

Core concepts

Five ideas make every later step obvious. Internalise these and the portal stops being a maze of synonyms.

There are two parties and one direction of trust. The identity provider (IdP) — Entra ID — authenticates users and issues assertions. The service provider (SP) — the SaaS app — consumes assertions and grants access. Trust flows one way: the SP trusts the IdP’s signature. You configure both ends: you tell Entra who the SP is (its Entity ID and where to send the assertion), and you tell the SP who the IdP is (Entra’s login URL and signing certificate). Half of every “it doesn’t work” is that you configured one end and forgot the other.

The whole integration is three URLs plus one certificate. That is genuinely it. The Identifier (Entity ID) is the SP’s unique name — an opaque string, often a URL but never fetched. The Reply URL, also called the Assertion Consumer Service (ACS) URL, is where Entra POSTs the signed assertion back. The Sign on URL is where Entra sends the user to start an SP-initiated flow. The signing certificate is the X.509 cert whose private key Entra uses to sign the assertion and whose public key the SP uses to verify it. Get these four things right and exact, and SAML works.

SAML field names are a Tower of Babel. Entra, the SAML spec, and each vendor’s wizard use different words for the same value. This single table resolves 90% of setup confusion — keep it open while you fill in both ends:

What it is Entra field name SAML-spec name Common vendor-wizard names
SP’s unique ID Identifier (Entity ID) <saml:Issuer> / SP EntityID Entity ID, SP Entity ID, Audience, Audience URI, SP Identifier
Where assertion is POSTed Reply URL AssertionConsumerService Location ACS URL, Assertion Consumer Service URL, Recipient URL, SAML POST URL, Reply URL
Where user starts SP-initiated Sign on URL — (app-specific) Login URL, App URL, Start URL
Carries app deep-link state Relay State RelayState RelayState, Default RelayState
Subject of the assertion Unique User Identifier (Name ID) <saml:Subject><NameID> NameID, Name Identifier, Username, Subject
IdP’s sign-in endpoint Login URL IdP SSO Service Location SAML 2.0 Endpoint, IdP SSO URL, SSO URL
IdP’s logout endpoint Logout URL SingleLogoutService Location SLO URL, Logout URL
IdP’s public signing cert Certificate (Base64 / Raw) IdP X.509 Certificate Signing Certificate, IdP Certificate, X.509 Cert

The assertion is signed XML, and the SP validates four things. When the user authenticates, Entra builds a SAML assertion — an XML document stating the subject (NameID), the attributes (claims), an Audience restriction (which must equal the SP’s Entity ID), and validity timestamps — then signs it with the signing cert’s private key. The SP, on receiving it at the ACS URL, checks: (1) the signature is valid against the IdP cert it trusts; (2) the Audience matches its own Entity ID; (3) the assertion is within its NotBefore/NotOnOrAfter time window; (4) the NameID/attributes identify a known user. A failure in any of these four is the source of most “logged in to Entra but the app rejected me” tickets.

SSO can start at either end. In an IdP-initiated flow the user starts at the My Apps portal (myapps.microsoft.com), clicks the app tile, and Entra POSTs an unsolicited assertion straight to the ACS URL. In an SP-initiated flow the user starts at the app, the app redirects them to Entra with a SAMLRequest, and Entra responds. IdP-initiated needs only the Identifier and Reply URL; SP-initiated additionally needs the Sign on URL and the app must support it. Knowing which flow you are testing tells you where to start the click-through and which URL is in play.

The SAML sign-in sequence, step by step

The flow itself, once you see it as numbered hops, demystifies every error. Here is the SP-initiated sequence with the Entra and SP responsibilities side by side:

# Step Who acts What goes over the wire Fails as
1 User opens the app Browser → SP App URL request
2 App builds an auth request SP HTTP redirect with SAMLRequest App-side config
3 Browser redirected to Entra Browser → IdP GET /saml2?SAMLRequest=… AADSTS750xx (bad request)
4 Entra authenticates the user IdP Password + MFA + Conditional Access AADSTS500011 (app not found), CA block
5 Entra checks user is assigned IdP Assignment lookup AADSTS50105 (not assigned)
6 Entra builds + signs the assertion IdP XML assertion, signed
7 Browser POSTs assertion to ACS Browser → SP POST to Reply URL with SAMLResponse AADSTS700016 / Reply URL mismatch
8 SP validates signature + audience SP Audience/signature mismatch (app-side)
9 SP maps NameID to a local user SP “user not found” (provisioning gap)
10 App session established SP Set cookie, redirect to app

Setting up the Enterprise Application

Everything starts with creating the app object in your tenant. There are two front doors, and choosing right saves rework.

Gallery vs non-gallery applications

Entra ships a gallery of thousands of pre-integrated SaaS apps (Salesforce, Workday, ServiceNow, etc.). Adding one from the gallery pre-fills protocol settings, sometimes pre-fills URL patterns (you supply your tenant-specific slug), and often unlocks automated SCIM provisioning and step-by-step vendor docs. If your app is in the gallery, use it. If it is a homegrown app or an obscure vendor, you create a non-gallery application (the tile literally named “Create your own application”). The two differ only in pre-fill convenience; the underlying object — a service principal — is identical.

Aspect Gallery app Non-gallery app
Setup speed Faster — settings pre-filled Manual — you enter every field
URL pre-fill Pattern provided, you fill the slug Blank — paste from vendor
SCIM provisioning Often built-in & documented Manual SCIM config if supported
Vendor SSO docs Linked from the tile You follow the vendor’s generic guide
When to use Listed SaaS vendors Custom / in-house / unlisted apps
Object created Service principal (enterprise app) Service principal (enterprise app)

SSO mode: pick SAML deliberately

Once the app exists, its Single sign-on blade offers several modes. Choosing the wrong one is a common first stumble. This table is the decision:

SSO mode What it means Use when
SAML Entra is IdP via SAML 2.0; you configure URLs + claims + cert The app supports SAML 2.0 (this guide)
OpenID Connect / OAuth App uses OIDC; configured via an app registration App is OIDC-native (modern apps, your own code)
Password-based Entra stores & replays form credentials (vaulting) App has only a username/password form, no SAML/OIDC
Linked Just a tile that deep-links elsewhere SSO is handled by another system; you only want the tile
Disabled No SSO App not yet configured

For this guide, you pick SAML. That reveals the five-section SAML configuration page — Basic SAML Configuration, Attributes & Claims, SAML Certificates, the app’s metadata/URLs, and Test — which we work through next.

Basic SAML configuration: the three URLs

This is Section 1 of the SAML page, and it is where the synonym confusion bites hardest. Each field, what it is, and the gotcha:

Field What to enter Required for Gotcha
Identifier (Entity ID) The SP’s unique ID from the vendor (e.g. https://acmehr.com/saml) Always Must match the assertion’s Audience exactly — protocol, case, trailing slash all count
Reply URL (ACS) Where Entra POSTs the assertion (vendor-supplied) Always Must be HTTPS; must exactly match the SP’s ACS; you can add several and mark one default
Sign on URL Where to send the user to start SP-initiated SSO SP-initiated only Leave blank for IdP-initiated-only apps
Relay State Default deep-link/state after login Optional Only set if the app needs a specific landing page
Logout URL SP’s Single Logout endpoint SLO only Needed only if you want federated sign-out

Three rules that prevent the most painful failures:

Attributes & claims: shaping the assertion

This is Section 2, and it is where “works for me, breaks for that one user” lives. Claims are the attributes Entra puts in the assertion. The single most important is the Name ID (the <NameID> in the subject) — the value the app uses as the user’s primary key. The rest are additional attributes the app may need (email, name, department, group memberships).

Name ID: format and source

The Name ID has two dimensions: its format (the XML Format attribute telling the SP how to interpret the value) and its source (which Entra user attribute supplies the value).

Name ID format When to use Notes
emailAddress App keys users by email (most common) Source must be a real, present email-shaped attribute
persistent App wants a stable opaque ID that never changes Survives email changes; good for privacy
unspecified App doesn’t care about format Default; safest when the vendor is vague
transient Anonymous, per-session ID Rare; for apps that don’t persist identity
Name ID source attribute Value it sends Watch out for
user.userprincipalname The UPN (often but not always = email) UPN may differ from primary SMTP — breaks email-keyed apps
user.mail The primary email Blank for users without a mailbox/mail set — the #1 silent failure
user.objectid The immutable Entra object GUID Great for persistent; opaque, never reused
user.employeeid HR identifier Only if populated for every assigned user

The classic production bug: the vendor keys on email, you set Name ID source to user.mail, and it works perfectly in your testing — because you have a mailbox. Then a contractor with no Exchange license, whose mail attribute is empty, signs in and the assertion carries an empty subject; the app throws “user not found.” If the app keys on email, user.userprincipalname is usually the more reliable source provided UPN equals the SMTP address in your tenant; otherwise populate mail for everyone first.

Additional claims and transformations

Beyond Name ID, Entra emits a default set of claims and lets you add custom ones. The defaults (emitted with long namespace URIs unless you shorten them):

Default claim (short name) Source Typical use
name user.userprincipalname Display/login hint
givenname user.givenname First name in the app
surname user.surname Last name in the app
emailaddress user.mail Email attribute
name identifier (NameID) configurable The subject / primary key

You can add claims sourced from any directory attribute, a constant, or a claim transformation (e.g. ToLower(), Join(), ExtractMailPrefix(), regex replace). Transformations solve real mismatches:

Transformation What it does Real use case
ExtractMailPrefix() Takes the part before @ App wants jdoe, not jdoe@contoso.com
ToLowercase() Lowercases the value App does case-sensitive username matching
Join() Concatenates with a separator Build a composite ID like region-username
RegexReplace() Pattern-based rewrite Strip a domain suffix or reformat an ID
IfEmpty() Falls back to another attribute Use mail, else fall back to userprincipalname

Group claims

Many apps drive in-app roles from group membership. The group claim emits the user’s groups in the assertion — but you must choose which groups and how they’re identified, because emitting all groups for a user in 500 groups blows past the SAML token size limit and breaks the login.

Group claim option Emits Caveat
All groups Every group the user belongs to Risks token bloat; can exceed ~150-group emit limit → overage claim instead
Security groups Security groups only Common choice; still subject to the limit
Groups assigned to the application Only groups assigned to this app Best practice — bounded, relevant, avoids bloat
Directory roles Entra role memberships For apps that map to admin roles

The group value can be the group Object ID (a GUID — stable, opaque) or a sAMAccountName / display name (human-readable, requires Entra Connect sync for on-prem groups). Apps that match on a name need the latter; most cloud apps take the Object ID. Choose “Groups assigned to the application” unless the app genuinely needs the full set — it is the single most effective guard against the token-too-large failure.

The signing certificate and metadata

This is Section 3. Entra generates a self-signed X.509 signing certificate (default validity 3 years) whose private key signs every assertion. The SP must trust the public key. Two ways to give it to the SP, and you should always prefer the first:

Delivery method What you hand the SP Why prefer it
Federation Metadata XML One XML file with cert + URLs + entity ID The SP can re-fetch on rollover automatically (if it supports metadata refresh); fewer manual steps
Certificate (Base64 / Raw) + URLs The .cer plus the Login/Logout URLs typed in Use only when the SP can’t consume metadata; manual cert rollover required

The signing options control what gets signed:

Signing option What is signed Use when
Sign SAML assertion (default) The assertion element only Default and recommended for most apps
Sign SAML response The outer response only App requires response-level signature
Sign SAML response and assertion Both App demands both signed (some strict SPs)

The signing algorithm defaults to SHA-256; only drop to SHA-1 if a legacy SP cannot do SHA-256 (and treat that as tech debt to remove). The cert is the thing that expires and takes the app down if you ignore it — covered in detail in the certificate-rollover section below.

Assigning users and provisioning

A configured SAML app that no one is assigned to will reject every login with AADSTS50105 (“the signed in user is not assigned to a role for the application”). Assignment is a separate, mandatory step.

Who can sign in

By default an enterprise app requires user assignment — only explicitly assigned users/groups can sign in. You can flip “Assignment required?” to No to let any user in the tenant authenticate (useful for broadly-available internal tools, dangerous for sensitive apps). Assign individual users for pilots; assign groups for anything real, ideally dynamic groups so membership self-maintains.

Setting Effect Recommendation
Assignment required = Yes Only assigned users/groups can sign in Default; keep on for all but trivial apps
Assignment required = No Any tenant user can sign in Only for genuinely all-staff internal tools
Assign users Named individuals get access Pilots and small apps
Assign groups All group members get access Production; pair with dynamic groups

JIT provisioning vs SCIM

Authenticating is not the same as existing in the app. Two patterns close that gap:

Pattern How users get created in the app Deprovisioning Use when
JIT (just-in-time) The app creates the user on first successful SSO, from the assertion’s claims App-specific; often manual App supports SAML JIT; you accept manual offboarding
SCIM provisioning Entra pushes create/update/disable via the SCIM API on a schedule Automatic — disabling in Entra disables/deletes in the app App exposes a SCIM endpoint; you want full lifecycle

JIT is “free” — it rides on the SSO assertion — but only handles creation; it does not disable users when you remove them. SCIM is the grown-up answer: Entra synchronises the full lifecycle (joiners, movers, leavers) to the app, so disabling a user in Entra disables them everywhere. For any app holding real data, aim for SCIM; use JIT only as a stopgap or where SCIM isn’t available.

Architecture at a glance

Follow the request left to right. A user starts at the AcmeHR SaaS app (or the My Apps portal for IdP-initiated). The app, being the service provider, has no idea who the user is, so it issues a SAMLRequest and redirects the browser to Entra ID — the identity provider. Entra authenticates the user against your directory: it checks credentials, runs MFA and Conditional Access, and crucially confirms the user is assigned to this enterprise app (skip the assignment and you stop dead here with AADSTS50105). Once satisfied, Entra’s token service builds an XML assertion stating the subject (NameID), the email/group claims, and an Audience equal to the app’s Entity ID, then signs it with the signing certificate private key.

The browser POSTs that signed SAMLResponse to the app’s Reply URL (ACS). The service provider now validates four things — signature against the IdP cert it trusts, Audience equals its own Entity ID, the timestamps are current, and the NameID maps to a known user — and only then drops a session cookie and lets the user in. The numbered badges below mark exactly where each common failure bites: a Reply URL that doesn’t match, a Name ID source that’s empty for some users, an expired signing cert, a missing assignment, and an unprovisioned user the SP can’t find.

Left-to-right SAML 2.0 SSO architecture in Entra ID showing the service provider app redirecting to Entra as identity provider, Entra running MFA, Conditional Access and app assignment, the token service signing the assertion with the signing certificate, and the browser POSTing the signed SAMLResponse to the app Reply URL where signature, audience, timestamp and user mapping are validated, with five numbered failure badges

Real-world scenario

Contoso Retail (a fictional 4,000-employee chain) is rolling out AcmeHR, a cloud HR and payroll SaaS, to all store and head-office staff. The mandate from security: every login must be SSO with MFA, no local AcmeHR passwords, and a departing employee must lose access within minutes. Priya, the identity admin, owns the integration.

AcmeHR is in the Entra gallery, so Priya adds it from there, which pre-fills the SAML pattern and links AcmeHR’s own setup doc. She pastes AcmeHR’s tenant-specific Identifier (https://contoso.acmehr.com/saml) and Reply URL (https://contoso.acmehr.com/saml/acs) into Basic SAML Configuration, copying directly from the AcmeHR admin console to avoid a typo. The pilot — herself and three HR managers — works on the first try via My Apps. She assigns the HR-Managers security group, requires MFA via a Conditional Access policy, and calls it done.

Then she expands to the first store. Half the cashiers can sign into Entra but AcmeHR shows “employee record not found.” Priya pulls the sign-in logs: the SAML sign-in succeeds on the Entra side every time — so the failure is on AcmeHR’s side, in step 9, mapping the assertion to a user. She inspects the assertion with the Test SAML experience and spots it: the Name ID source is user.mail, and the cashiers — hired part-time, no Exchange license — have an empty mail attribute. The assertion subject is blank, so AcmeHR can’t match them. AcmeHR keys users by email, and in Contoso’s tenant UPN is the SMTP address, so Priya switches the Name ID source to user.userprincipalname. Every cashier now logs in.

A week later AcmeHR asks Priya to drive job-role-based access (manager vs cashier vs payroll) from groups. She adds a group claim — but scoped to “Groups assigned to the application”, having read that emitting all groups risks the token-size limit. AcmeHR maps the group Object IDs to its internal roles. Finally, to satisfy the “lose access in minutes” requirement, Priya enables SCIM provisioning so that disabling a worker in Entra automatically disables them in AcmeHR — JIT alone would have created users but never removed them. The integration that started as “paste three URLs” ends as a full joiner-mover-leaver lifecycle, and the only two real bugs were both in the claims, not the URLs — which is exactly where SAML bugs usually live.

Advantages and disadvantages

SAML SSO is the right default for federating SaaS, but it has sharp edges worth naming explicitly:

Advantages Disadvantages
One identity, one MFA, one set of Conditional Access rules across all apps Per-app manual configuration; no global “turn on SSO” switch
No app-local passwords to leak, reuse or rotate Synonym confusion (Entity ID vs Reply URL vs ACS) causes setup errors
Instant offboarding — disable in Entra, lose access everywhere (with SCIM) JIT alone doesn’t deprovision; you need SCIM for true lifecycle
Centralised sign-in audit across the whole estate Signing certificate expiry causes a hard outage if unmanaged
Works with thousands of gallery SaaS apps out of the box SAML is XML-heavy and dated vs OIDC for modern/native apps
Group/role claims drive in-app authorization Token-size limit bites on large group claims

When SAML wins: established enterprise SaaS (HR, finance, ITSM, LMS) that supports it — which is most of it. When to prefer OIDC instead: your own modern web/mobile/SPA apps, anything needing fine-grained API authorization, or apps where the lighter JSON token and PKCE flow fit better — see OIDC and OAuth2 on Entra ID. The disadvantages are all manageable (claims discipline, cert rollover hygiene, SCIM for lifecycle) rather than disqualifying — which is why SAML SSO remains the workhorse of enterprise federation.

Hands-on lab

This is the centrepiece. You will configure a working SAML SSO integration three ways and validate it. Because the service provider is a third-party app, the lab uses a free, public SAML test SP so you can complete the full loop end to end without owning a SaaS vendor. We use samltest.id (a community SAML test service) as the SP. The same steps apply identically to a real vendor — you simply substitute the vendor’s Entity ID and ACS URL.

Estimated time: 30–40 minutes. Cost: ₹0 / $0 — enterprise apps and SAML SSO are free in every Entra tier; this lab uses no paid features.

Prerequisites for the lab

Requirement How to verify
Entra tenant + Application Administrator (or higher) az ad signed-in-user show then check role assignments
az CLI ≥ 2.55 with you signed in az version then az login
A test user with a populated mail or UPN = email az ad user show --id <upn> --query mail
Browser for the portal and the Test SAML flow

Part A — Portal walkthrough

Step 1 — Create the enterprise application. In the Entra admin center, go to Entra ID → Enterprise applications → New application → Create your own application. Name it SAML-Lab-SP, choose “Integrate any other application you don’t find in the gallery (Non-gallery)”, and click Create. Expected: the app’s Overview page opens; an enterprise application (service principal) now exists in your tenant.

Step 2 — Choose SAML SSO. On the app, open Single sign-on and select SAML. Expected: the five-section SAML configuration page appears, with Basic SAML Configuration at the top showing empty Identifier and Reply URL fields.

Step 3 — Enter the three URLs. Click the edit (pencil) on Basic SAML Configuration and enter the test SP’s values:

Field Value
Identifier (Entity ID) https://samltest.id/saml/sp
Reply URL (ACS) https://samltest.id/idp/profile/Metadata/SAML
Sign on URL https://samltest.id/upload.php

Click Save. Expected: the section now shows your three values; a warning about testing may appear — that’s fine.

Step 4 — Review attributes & claims. Open the Attributes & Claims edit pane. Confirm the Unique User Identifier (Name ID) is set, and change its source to user.userprincipalname with format emailAddress (most SPs key on email). Expected: the claims list shows Unique User Identifier (Name ID) plus the default givenname, surname, emailaddress, name claims.

Step 5 — Download the Federation Metadata. In SAML Certificates, copy the App Federation Metadata Url, and also note the Login URL and Logout URL from the app’s setup section. Download the Federation Metadata XML. Expected: an XML file downloads containing the signing certificate and Entra’s SSO/SLO endpoints. This is what you hand a real SP.

Step 6 — Configure the SP side. Go to https://samltest.id, choose Upload Metadata, and upload the Federation Metadata XML from Step 5 (or paste the metadata URL). Expected: samltest.id confirms it ingested your IdP metadata and now trusts your Entra tenant as an IdP.

Step 7 — Assign yourself. Back on the app, go to Users and groups → Add user/group, select your own account, and Assign. Expected: your user appears in the assignment list. Skipping this step is the #1 cause of AADSTS50105 in the next step.

Step 8 — Test the SSO. On the Single sign-on page, scroll to Test single sign-on and click Test. Choose Sign in as current user. Expected: you authenticate against Entra (MFA if your policy requires it), the assertion is POSTed to samltest.id, and samltest.id displays the decoded attributes — your UPN as the NameID and the email/name claims. This is a successful end-to-end SAML SSO.

Step 9 — Validate from the sign-in logs. Go to Entra ID → Sign-in logs, filter by the application SAML-Lab-SP. Open the latest entry. Expected: a Success sign-in for your user against the app. If you ever see a failure, this entry’s error code names the cause (cross-reference the troubleshooting table below).

Part B — az CLI + Microsoft Graph

The portal is fine for one app; for repeatability you script it. Enterprise-app SAML config spans the az ad sp commands and direct Microsoft Graph calls (some SAML-specific properties are only on Graph). Here is the full sequence.

Step 10 — Create the application + service principal.

# Create the app registration (the blueprint)
appId=$(az ad app create --display-name "SAML-Lab-SP-CLI" --query appId -o tsv)

# Create the enterprise app (service principal) from it
spId=$(az ad sp create --id "$appId" --query id -o tsv)

echo "appId=$appId  spId=$spId"

Expected: both IDs print. appId is the client/application ID; spId is the service principal object ID you’ll patch.

Step 11 — Set the SSO mode to SAML. The preferredSingleSignOnMode lives on the service principal and is set via Graph:

az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spId" \
  --headers "Content-Type=application/json" \
  --body '{ "preferredSingleSignOnMode": "saml" }'

Expected: HTTP 204 No Content. The app’s SSO mode is now SAML (verify in the portal — the SAML page is now active).

Step 12 — Set the Identifier (Entity ID) and Reply URL (ACS). The Entity ID is the app’s identifierUris; the Reply URL is web.redirectUris on the application object:

# Identifier (Entity ID) and Reply URL on the application object
az ad app update --id "$appId" \
  --identifier-uris "https://samltest.id/saml/sp" \
  --web-redirect-uris "https://samltest.id/idp/profile/Metadata/SAML"

Expected: command returns silently (success). Confirm with az ad app show --id "$appId" --query "{ent:identifierUris, acs:web.redirectUris}".

Step 13 — Set the SAML sign-on URL and notification email. These SAML-specific properties are patched on the service principal via Graph:

az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spId" \
  --headers "Content-Type=application/json" \
  --body '{
    "loginUrl": "https://samltest.id/upload.php",
    "notificationEmailAddresses": ["admin@contoso.com"]
  }'

Expected: HTTP 204. The Sign on URL is set, and cert-expiry notifications will go to that address.

Step 14 — Create the SAML signing certificate. Generate the token-signing cert and mark it active in one call:

az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spId/addTokenSigningCertificate" \
  --headers "Content-Type=application/json" \
  --body '{ "displayName": "CN=SAML-Lab-SP-CLI", "endDateTime": "2029-06-24T00:00:00Z" }'

Expected: JSON describing the new certificate (thumbprint, key ID, start/end dates). The signing cert now exists; Entra will sign assertions with it.

Step 15 — Customise the Name ID claim (claims mapping policy). SAML claim customisation in Graph uses a claimsMappingPolicy assigned to the SP. Create a policy that sets the NameID source, then assign it:

# Create a claims mapping policy: NameID = userprincipalname, emailAddress format
polId=$(az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies" \
  --headers "Content-Type=application/json" \
  --body '{
    "displayName": "saml-lab-nameid",
    "definition": ["{\"ClaimsMappingPolicy\":{\"Version\":1,\"IncludeBasicClaimSet\":\"true\",\"ClaimsSchema\":[{\"Source\":\"user\",\"ID\":\"userprincipalname\",\"SamlClaimType\":\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier\"}]}}"]
  }' --query id -o tsv)

# Assign the policy to the service principal
az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spId/claimsMappingPolicies/\$ref" \
  --headers "Content-Type=application/json" \
  --body "{ \"@odata.id\": \"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$polId\" }"

Expected: the policy ID prints, then the assignment returns HTTP 204. (Note: claims mapping policies require the app’s acceptMappedClaims or a custom signing key for some scenarios; for the default basic claim set this works as-is.)

Step 16 — Assign a user via an app role assignment. Assignment in Graph is an appRoleAssignment. Use the default access role (00000000-0000-0000-0000-000000000000):

# Get your user object ID
userId=$(az ad signed-in-user show --query id -o tsv)

az rest --method POST \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spId/appRoleAssignedTo" \
  --headers "Content-Type=application/json" \
  --body "{
    \"principalId\": \"$userId\",
    \"resourceId\": \"$spId\",
    \"appRoleId\": \"00000000-0000-0000-0000-000000000000\"
  }"

Expected: JSON describing the assignment. Your user can now sign into the app (otherwise: AADSTS50105).

Step 17 — Validate. Confirm the SAML wiring took:

az rest --method GET \
  --uri "https://graph.microsoft.com/v1.0/servicePrincipals/$spId?\$select=preferredSingleSignOnMode,loginUrl,replyUrls" \
  --query "{mode:preferredSingleSignOnMode, login:loginUrl, reply:replyUrls}"

Expected: mode: saml, your loginUrl, and the reply URL — proving the CLI build matches the portal one. Then run the same Test SAML browser flow from Step 8.

Part C — Bicep (declarative)

For repeatable, source-controlled app onboarding, declare the application and service principal in Bicep using the Microsoft Graph Bicep extension. This captures the SAML shape as code:

extension microsoftGraphV1

@description('Display name of the SAML enterprise app')
param appDisplayName string = 'SAML-Lab-SP-Bicep'

@description('SP Entity ID (Identifier)')
param entityId string = 'https://samltest.id/saml/sp'

@description('SP Assertion Consumer Service (Reply) URL')
param acsUrl string = 'https://samltest.id/idp/profile/Metadata/SAML'

@description('SP sign-on URL for SP-initiated SSO')
param signOnUrl string = 'https://samltest.id/upload.php'

// The application object (blueprint)
resource samlApp 'Microsoft.Graph/applications@v1.0' = {
  uniqueName: appDisplayName
  displayName: appDisplayName
  identifierUris: [ entityId ]
  web: {
    redirectUris: [ acsUrl ]
  }
}

// The service principal (enterprise app) with SAML SSO
resource samlSp 'Microsoft.Graph/servicePrincipals@v1.0' = {
  appId: samlApp.appId
  preferredSingleSignOnMode: 'saml'
  loginUrl: signOnUrl
  appRoleAssignmentRequired: true
  notificationEmailAddresses: [ 'admin@contoso.com' ]
}

output enterpriseAppObjectId string = samlSp.id
output applicationId string = samlApp.appId

Step 18 — Deploy the Bicep.

az deployment group create \
  --resource-group rg-identity-lab \
  --template-file saml-app.bicep

Expected: a successful deployment whose outputs print the enterprise app object ID and application ID. The app appears under Enterprise applications with SAML pre-selected and appRoleAssignmentRequired (assignment required) set to true. You still create the signing certificate and assign users/claims as in Part B — the Bicep captures the durable shape; the cert and assignments are operational state.

Note: the Microsoft.Graph Bicep extension is the supported path for declaring Entra objects as code. Confirm your tooling has the extension enabled (bicepconfig.json with the Graph extension) before deploying.

Part D — Teardown

Remove everything you created so the lab leaves no trace and incurs no risk:

# Delete the CLI-created app (cascades to its service principal)
az ad app delete --id "$appId"

# Delete the claims mapping policy
az rest --method DELETE --uri "https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/$polId"

# Delete the Bicep-deployed app
az ad app delete --id "$(az ad app list --display-name 'SAML-Lab-SP-Bicep' --query '[0].appId' -o tsv)"

For the portal-created app: Enterprise applications → SAML-Lab-SP → Properties → Delete. Deleted enterprise apps and app registrations go to a soft-deleted state for 30 days; purge with az ad app delete (already permanent for app registrations) or leave them to age out. Expected: the apps disappear from the Enterprise applications list. Confirm with az ad sp list --display-name 'SAML-Lab-SP-CLI' returning empty.

Common mistakes & troubleshooting

This is the section you return to mid-incident. Every SAML failure surfaces as either an AADSTS error on the Entra side (visible in the sign-in logs and on the error page) or a silent rejection on the SP side (the user authenticated fine but the app says “no”). The fork: if the sign-in log shows Success but the app rejects you, the problem is on the SP — almost always Audience, signature, or user-mapping. If the sign-in log shows a failure, the AADSTS code names it.

The AADSTS error-code reference

Code Meaning Likely cause How to confirm Fix
AADSTS50105 User not assigned to a role for the app App requires assignment; user/group not assigned Sign-in logs show 50105 Assign the user/group, or set “Assignment required = No”
AADSTS700016 App not found in directory / reply mismatch Wrong Identifier, or app not in tenant Error page names the identifier Fix Identifier; confirm the app exists in this tenant
AADSTS650056 Misconfigured application App registration/permission/SSO mode wrong Full error text in sign-in log details Re-check SSO mode = SAML and required config
AADSTS75005 SAML request not compliant Malformed SAMLRequest from the SP Sign-in log → request details Fix the SP’s SAML request settings
AADSTS750xx Bad SAML request (binding/format) SP sent a request Entra can’t parse Error page binding detail Align SP binding (HTTP-Redirect/POST) with Entra
AADSTS500011 Resource principal not found in tenant Identifier doesn’t match any service principal Error names the resource Make the Identifier exactly match the app’s Entity ID
AADSTS90100 Invalid SAML parameter A SAML request parameter is invalid Request details Correct the offending parameter on the SP
AADSTS7000112 App disabled The enterprise app is disabled for sign-in App → Properties → Enabled for users to sign-in Re-enable the app

The setup-mistake playbook

The errors above are symptoms; these are the configuration mistakes that cause them, plus the SP-side failures that never produce an AADSTS code at all:

# Symptom Root cause Confirm Fix
1 Login loops / “reply URL mismatch” Reply URL registered ≠ ACS the SP uses (trailing slash, http vs https) Compare registered Reply URL with SP’s ACS byte-for-byte Make them identical; HTTPS only
2 AADSTS50105 for everyone No users/groups assigned to the app Users and groups blade is empty Assign the user/group
3 Works for you, “user not found” for some Name ID source (user.mail) empty for users without mailbox Test SAML shows blank NameID for that user Switch source to user.userprincipalname or populate mail
4 App rejects assertion: “audience invalid” Identifier (Entity ID) ≠ the app’s expected Audience Decode assertion; compare <Audience> to SP config Set Identifier to match exactly
5 App rejects assertion: “signature invalid” SP has the wrong/old IdP certificate SP cert thumbprint ≠ current Entra signing cert Re-upload current metadata/cert to the SP
6 Login worked, suddenly all users fail Signing certificate expired SAML Certificates blade shows expiry past Roll cert over (see below); re-share with SP
7 Login fails only for users in many groups Group claim emits too many groups, token too large Group claim set to “All groups” for users in 150+ groups Switch to “Groups assigned to the application”
8 Authenticated but bounced back to login SP-initiated but no Sign on URL set Flow starts at app, Sign on URL blank Set the Sign on URL; or use IdP-initiated
9 AADSTS7000112 / can’t reach the app Enterprise app disabled for sign-in Properties → “Enabled for users to sign-in?” = No Set to Yes
10 MFA prompt loops or is denied Conditional Access policy blocks/requires unmet control Sign-in log → Conditional Access tab shows the policy Adjust the CA policy or satisfy the control
11 New users can SSO but aren’t in the app JIT not configured (or app has no JIT) and no SCIM App has no provisioning configured Enable SCIM provisioning or app-side JIT
12 Claims missing in the assertion Claim not added, or source attribute empty Test SAML assertion lacks the claim Add the claim; populate the source attribute

Three reading notes:

Best practices

Security notes

SAML SSO is a security feature, but a sloppy setup can weaken posture. The essentials:

Cost & sizing

The good news on cost: SAML SSO and enterprise applications are free in every Entra ID tier, including the free tier bundled with any Azure subscription or Microsoft 365 plan. You can configure unlimited SAML apps at no charge. What you pay for is the surrounding governance:

Capability Tier required Why you’d pay
Basic SAML SSO + assignment Free Always free — no limit on SAML apps
Conditional Access (MFA, device, risk) Entra ID P1 To gate SAML apps with MFA/device rules
Dynamic groups for assignment Entra ID P1 Self-maintaining group-based access
Identity Protection (risk-based CA) Entra ID P2 Sign-in/user risk signals feeding CA
Access Reviews for app assignment Entra ID P2 Periodic recertification of who has access
SCIM provisioning Free for most apps Lifecycle sync (gallery apps’ provisioning is generally included)

Rough figures: Entra ID P1 is on the order of ₹500 / ~$6 per user/month and P2 around ₹750 / ~$9 per user/month (often bundled in Microsoft 365 E3/E5). For a 4,000-seat org like Contoso, the SAML config costs nothing; the P1 licensing to gate every app with Conditional Access is the real line item — and it is the licence you almost certainly already own if you run Microsoft 365. There is nothing to “size” on the SAML side itself: no compute, no throughput, no quota beyond the per-token group-emit limit (~150 groups before overage). The sizing decision is purely which Entra tier you need for the access controls you want around SSO, not the SSO itself.

Interview & exam questions

Q1. What are the three core URLs in an Entra SAML SSO configuration, and what does each do? The Identifier (Entity ID) uniquely names the service provider and must equal the assertion’s Audience. The Reply URL (ACS) is where Entra POSTs the signed assertion. The Sign on URL is where Entra sends the user to begin an SP-initiated flow. Get all three exact and SAML works. (Maps to SC-300.)

Q2. A user authenticates to Entra successfully but the SaaS app says “user not found.” Where is the bug? On the service provider side, in the user-mapping step — the sign-in log showing success proves Entra’s IdP role worked. Most often the Name ID source attribute is empty for that user (e.g. user.mail blank for a no-mailbox account), so the assertion subject can’t be matched. Fix the Name ID source.

Q3. What does AADSTS50105 mean and how do you fix it? The signed-in user is not assigned to the application. Either assign the user/group under Users and groups, or set “Assignment required” to No. It is the single most common first-test failure. (SC-300.)

Q4. Difference between an app registration and an enterprise application? The app registration is the global blueprint (app object) defining the app’s identity and config; the enterprise application is the service principal — the local instance in your tenant where you configure SSO, assignment and Conditional Access. SAML SSO is configured on the enterprise app. See App Registrations vs Enterprise Applications.

Q5. When would you choose SAML over OIDC for an app? Choose SAML for established enterprise SaaS that supports it (HR, finance, ITSM). Choose OIDC for your own modern web/mobile/SPA apps, fine-grained API authorization, or where the lighter JSON token and PKCE flow fit. The app’s supported protocols usually decide it.

Q6. What’s the risk of emitting “All groups” as a group claim, and the better option? For users in many groups (150+), the assertion exceeds the token-size limit, the claim is dropped to an overage reference, and logins or in-app authorization break. Scope the group claim to “Groups assigned to the application” — bounded and relevant.

Q7. How do you roll the SAML signing certificate without an outage? Make before break: add a new certificate, distribute it to the SP (ideally via metadata it re-fetches), then mark the new cert active. Because the SP trusts the new cert before Entra starts signing with it, there’s no gap. Cert expiry with no rollover is a hard, all-users outage.

Q8. JIT provisioning vs SCIM — what’s the operational difference? JIT creates the user in the app on first SSO from the assertion’s claims but does not deprovision. SCIM synchronises the full lifecycle (create/update/disable) from Entra to the app on a schedule, so disabling a user in Entra disables them in the app. For real lifecycle and instant offboarding, use SCIM.

Q9. The app rejects the assertion with “invalid audience.” What’s wrong? The Identifier (Entity ID) you set in Entra does not exactly match the Audience the SP expects — usually a trailing slash, case, or protocol mismatch. Decode the assertion, compare <Audience> to the SP’s config, and make them byte-identical.

Q10. Where do you confirm whether a SAML failure is on Entra’s side or the app’s side? The Entra sign-in logs, filtered to the app. A Success entry means Entra issued the assertion correctly — the failure is on the SP (Audience/signature/mapping). A failure entry carries the AADSTS code naming the IdP-side cause.

Q11. What licence do you need to require MFA on a SAML app via Conditional Access? Entra ID P1 (often bundled in Microsoft 365 E3/E5). Basic SAML SSO is free; Conditional Access — the mechanism to require MFA, device compliance or risk controls on the app — needs P1. Risk-based CA needs P2.

Q12. How do you make SAML SSO repeatable across many apps? Capture the durable config — application object, Entity ID, Reply URL, SSO mode — as Bicep (Microsoft Graph extension) or script it with az + Microsoft Graph, so onboarding the next vendor is a parameter change. Keep the signing cert and assignments as operational state on top.

Quick check

  1. Which Entra field maps to the vendor’s “ACS URL”?
  2. A user with no Exchange mailbox can’t log into a SAML app keyed on email; what’s the most likely cause and fix?
  3. What does setting “Assignment required = No” do, and why is it risky?
  4. Name the four things the service provider validates when it receives the assertion.
  5. Why prefer Federation Metadata XML over handing the SP a raw .cer?

Answers

  1. The Reply URL (Assertion Consumer Service URL) — where Entra POSTs the signed assertion.
  2. The Name ID source is user.mail, which is empty for that user, so the assertion subject is blank. Switch the source to user.userprincipalname (when UPN equals the SMTP address) or populate the mail attribute for all users.
  3. It lets any user in the tenant (including guests) sign into the app without explicit assignment — risky because the assignment list is your access boundary, and removing it exposes the app broadly.
  4. (1) Signature valid against the trusted IdP certificate; (2) Audience equals the SP’s own Entity ID; (3) the assertion is within its time window (NotBefore/NotOnOrAfter); (4) the NameID/attributes map to a known user.
  5. With metadata, the SP can re-fetch the certificate automatically on rollover, avoiding the manual cert-swap that causes the most common expiry outage; a raw .cer must be re-uploaded by hand every rotation.

Glossary

Next steps

Entra IDSAMLSSOEnterprise ApplicationsIdentityMicrosoft GraphConditional AccessAuthentication
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading