The default external collaboration posture in a fresh Entra tenant is “anyone can invite anyone, and every external tenant is trusted equally.” That is fine for a startup and a liability for an enterprise. This is how I lock external access down to named partners, trust their MFA so guests stop re-registering authenticators every week, and stand up a self-service sign-up funnel for customer-facing apps — without losing the audit trail.
Terminology note: Microsoft has consolidated its external identity story under Entra External ID. The same product covers two distinct scenarios: B2B collaboration / direct connect in your existing workforce tenant, and external-facing apps in a dedicated external tenant (the successor to Azure AD B2C). Settings and APIs differ between the two. I call out which one applies at each step.
1. Pick the right External ID model
Get this decision right first; it dictates everything downstream.
| Model | Where guests live | Auth surface | Use it for |
|---|---|---|---|
| B2B collaboration | Guest objects in your workforce tenant | Guest authenticates in their home tenant, redeems in yours | Partners, vendors, contractors using Teams, SharePoint, your line-of-business apps |
| B2B direct connect | No guest object; trust at the tenant boundary | Shared channels / Teams Connect only | Deep Teams collaboration with a trusted org, no guest sprawl |
| External-facing tenant | Local consumer/customer accounts in a separate external tenant | Email OTP, social, or federated IdPs via user flows | Customer and partner-facing SaaS apps you build |
The big trap: people reach for an external tenant to handle partner B2B, or bolt customer sign-up onto their corporate workforce tenant. Don’t. Partner collaboration belongs in your workforce tenant via B2B. Customer-facing apps belong in a dedicated external tenant so consumer accounts never pollute your employee directory or land in scope of workforce Conditional Access.
Everything in sections 2-8 is workforce-tenant B2B unless the heading says external tenant.
2. Cross-tenant access settings: default vs per-org
Cross-tenant access settings (XTAP) are the control plane for B2B. There is one default policy that applies to every external Entra tenant, plus organizational overrides keyed by partner tenant ID. Each policy has an inbound side (external users coming into your tenant) and an outbound side (your users going to theirs).
Inspect the current default before you touch anything:
# Default inbound/outbound posture for all external tenants
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/default"
I keep the default restrictive and open access per partner. Block inbound B2B collaboration by default, then explicitly allow named tenants:
{
"inboundTrust": {
"isMfaAccepted": false,
"isCompliantDeviceAccepted": false,
"isHybridAzureADJoinedDeviceAccepted": false
},
"b2bCollaborationInbound": {
"usersAndGroups": {
"accessType": "blocked",
"targets": [{ "target": "AllUsers", "targetType": "user" }]
},
"applications": {
"accessType": "blocked",
"targets": [{ "target": "AllApplications", "targetType": "application" }]
}
}
}
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/default" \
--headers "Content-Type=application/json" \
--body @default-xtap.json
A blocked default does not break existing redeemed guests immediately, but it stops new inbound B2B from any tenant you have not allowlisted. Communicate before flipping it, and stage it with a single pilot partner.
3. Author an inbound policy for a named partner
Create a per-organization policy for the partner’s tenant ID, then set its inbound side. Two API calls: one to create the org entry, one to configure inbound collaboration.
PARTNER_TENANT="22223333-cccc-4444-dddd-5555eeee6666"
# 1. Register the partner org (creates default-allow placeholders you then tighten)
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/partners" \
--headers "Content-Type=application/json" \
--body "{\"tenantId\": \"$PARTNER_TENANT\"}"
Now scope inbound access to a specific group in the partner tenant (their object IDs) and to specific apps in yours. Targeting a group instead of AllUsers is the single highest-leverage control here — it means only the partner’s project team can be invited, not their entire company.
{
"b2bCollaborationInbound": {
"usersAndGroups": {
"accessType": "allowed",
"targets": [
{ "target": "9a9a8b8b-7c7c-6d6d-5e5e-4f4f3a3a2b2b", "targetType": "group" }
]
},
"applications": {
"accessType": "allowed",
"targets": [
{ "target": "11112222-aaaa-bbbb-cccc-333344445555", "targetType": "application" }
]
}
}
}
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/partners/$PARTNER_TENANT" \
--headers "Content-Type=application/json" \
--body @partner-inbound.json
For the outbound direction — your employees being invited into the partner tenant — set b2bCollaborationOutbound on the same partner object. Outbound rarely needs to be as tight as inbound, but if you have data-exfiltration concerns, restrict which of your users can be guests elsewhere and pair it with tenant restrictions (section 8).
4. Trust external MFA and device claims
This is the setting that removes the most guest friction. By default, when a guest accesses your resources, Entra forces them to register and satisfy MFA in your tenant even though they already did MFA in their home tenant. Trusting their inbound claims means a partner who authenticated with a phishing-resistant method at home is honored as MFA-satisfied here.
{
"inboundTrust": {
"isMfaAccepted": true,
"isCompliantDeviceAccepted": true,
"isHybridAzureADJoinedDeviceAccepted": false
}
}
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/partners/$PARTNER_TENANT" \
--headers "Content-Type=application/json" \
--body @partner-trust.json
Decision guidance:
isMfaAccepted— turn on only for partners whose MFA bar you actually trust. You are accepting their assertion that the user did MFA. For a partner running phishing-resistant methods, this is strictly better security and far less friction. For an unknown org, leave itfalseand let your Conditional Access challenge them.isCompliantDeviceAccepted/isHybridAzureADJoinedDeviceAccepted— only meaningful if your Conditional Access requires compliant or Hybrid-joined devices for the app. Most orgs cannot enforce device compliance on a partner’s hardware, so trusting the partner’s compliance signal is the only way to let device-gated apps work for guests at all.
Trust settings are honored only when your Conditional Access policy actually requires MFA or device compliance. Trust does not weaken CA — it lets a guest satisfy a CA requirement using a claim from their home tenant instead of re-proving it in yours.
5. External collaboration restrictions: who can invite, who can be invited
Two independent knobs govern invitations. First, who in your tenant can invite guests (the authorization policy). Lock this down so random employees cannot sprinkle guests across the directory:
# allowInvitesFrom: everyone | adminsAndGuestInviters | adminsGuestInvitersAndAllMembers | none
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/policies/authorizationPolicy" \
--headers "Content-Type=application/json" \
--body '{"allowInvitesFrom": "adminsAndGuestInviters"}'
adminsAndGuestInviters is my default: assign the Guest Inviter role to the specific people who legitimately onboard partners (project managers, vendor coordinators) and nobody else can invite.
Second, which email domains may be invited at all (the B2B management policy). Use an allowlist of trusted partner domains, or a denylist to block consumer/competitor domains:
{
"invitationsAllowedAndBlockedDomainsPolicy": {
"allowedDomains": ["fabrikam.com", "contoso-partner.com"]
}
}
az rest --method PATCH \
--uri "https://graph.microsoft.com/v1.0/legacy/policies/<b2bPolicyId>" \
--headers "Content-Type=application/json" \
--body @allowed-domains.json
allowedDomainsandblockedDomainsare mutually exclusive — you configure one list or the other, never both. An allowlist is the safer posture; pair it with the per-partner XTAP policies so domain and tenant are both constrained.
6. Invite, redeem, and reset redemption
Send an invitation via Graph. Setting sendInvitationMessage: true emails the guest a redemption link:
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/invitations" \
--headers "Content-Type=application/json" \
--body '{
"invitedUserEmailAddress": "anil@fabrikam.com",
"inviteRedirectUrl": "https://myapps.microsoft.com",
"sendInvitationMessage": true,
"invitedUserType": "Guest"
}'
When a partner changes their identity provider — say they migrate from Google to their own Entra tenant — you do not delete and re-invite the guest (that orphans all their permissions). Instead, reset redemption status so they re-redeem against the new IdP while keeping the same object ID, group memberships, and app assignments:
GUEST_ID="<guest-object-id>"
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/users/$GUEST_ID/invalidateAllRefreshTokens"
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/invitations" \
--headers "Content-Type=application/json" \
--body "{
\"invitedUserEmailAddress\": \"anil@fabrikam.com\",
\"inviteRedirectUrl\": \"https://myapps.microsoft.com\",
\"sendInvitationMessage\": true,
\"resetRedemption\": true,
\"invitedUser\": { \"id\": \"$GUEST_ID\" }
}"
7. Custom user flows and self-service sign-up (external tenant)
Switching scenarios now. For customer-facing apps in a dedicated external tenant, you don’t invite guests — users self-register through a user flow you attach to the app. The user flow controls the sign-up/sign-in page, the attributes you collect, and the identity providers offered.
The high-leverage extension is an API connector (also called a custom authentication extension): Entra calls your HTTPS endpoint mid-flow to validate input or enrich the token. Two interception points exist — after federating with an IdP and before creating the user. Use the latter to reject disposable-email domains, look up an existing CRM record, or block sign-up unless the user is on an approved partner list.
Your endpoint receives a POST and must answer fast (single-digit seconds) with a continuation or a validation error:
{
"version": "1.0.0",
"action": "ValidationError",
"status": 400,
"userMessage": "Sign-up is limited to approved partner organizations.",
"code": "DOMAIN_NOT_ALLOWED"
}
To continue and optionally write claims into the token, return Continue:
{
"version": "1.0.0",
"action": "Continue",
"extension_partnerTier": "gold"
}
Secure the connector endpoint with Basic auth or, better, OAuth2 client credentials, and validate the bearer token Entra sends. An unauthenticated connector is an open door into your sign-up logic. Also build it idempotent and resilient — if the endpoint times out, the user’s sign-up fails.
Self-service sign-up in a workforce tenant exists too (for letting unmanaged partners onboard themselves into specific apps), and it reuses the same user flow + API connector machinery. The difference is the resulting object is a guest, not a local account.
8. Identity providers for guests
What a guest authenticates with depends on the email domain and which IdPs you have configured:
| Guest’s email | IdP used | Setup required |
|---|---|---|
| Belongs to an Entra tenant | That home Entra tenant | None |
| Gmail / Google Workspace | Google federation | Register OAuth client in Google Cloud, add to Entra external IdPs |
| Any other email | Email one-time passcode (OTP) | Enabled by default for B2B; verify it is on |
| Microsoft personal account | Microsoft account | Default |
| Federated domain (partner runs AD FS / Okta / Ping) | SAML/OIDC direct federation | Exchange metadata, configure per domain |
SAML/OIDC direct federation is what you set up when a partner uses a non-Entra IdP for their whole company. You federate at the domain level so every @partner.com guest authenticates at the partner’s IdP. The partner gives you their SAML metadata (issuer URI, passive endpoint, signing cert) or OIDC issuer + client credentials, and you register it:
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/directory/federationConfigurations/graph.microsoft.com%2FsamlOrWsFedExternalDomainFederation" \
--headers "Content-Type=application/json" \
--body '{
"domains": [{ "@odata.type": "microsoft.graph.externalDomainName", "id": "partner.com" }],
"issuerUri": "https://idp.partner.com/saml",
"passiveSignInUri": "https://idp.partner.com/saml/login",
"signingCertificate": "<base64-der-cert>",
"metadataExchangeUri": "https://idp.partner.com/saml/metadata"
}'
Confirm the partner does not already have an Entra tenant verifying that domain. If they do, B2B uses their Entra tenant and your direct-federation config is ignored. Direct federation is specifically for domains not backed by an Entra tenant.
9. Govern the guest lifecycle
A guest you invited two years ago for a one-week project is now an unmonitored standing credential. Two mechanisms keep the population honest.
Access reviews (Entra ID Governance / P2) — schedule a recurring review of all guests, or guests in specific groups, and auto-remove anyone whose reviewer doesn’t approve (or who doesn’t self-attest). The teeth are in autoApplyDecisionsEnabled plus a removeAccess default decision:
az rest --method POST \
--uri "https://graph.microsoft.com/v1.0/identityGovernance/accessReviews/definitions" \
--headers "Content-Type=application/json" \
--body '{
"displayName": "Quarterly guest access review",
"scope": {
"@odata.type": "#microsoft.graph.accessReviewQueryScope",
"query": "/users?$filter=userType eq '\''Guest'\''",
"queryType": "MicrosoftGraph"
},
"settings": {
"recurrence": { "pattern": { "type": "absoluteMonthly", "interval": 3 } },
"autoApplyDecisionsEnabled": true,
"defaultDecisionEnabled": true,
"defaultDecision": "Deny",
"instanceDurationInDays": 14
}
}'
Automatic guest expiration comes free with entitlement management access packages: assign guests to a resource bundle via an access package with an expiry, and when it lapses the guest’s access — and optionally the guest object itself — is removed. This is the cleanest pattern for time-boxed partner engagements because onboarding and offboarding are a single policy, not a manual checklist.
Enterprise scenario
A manufacturing group acquired a smaller competitor and needed their engineering team in our SharePoint and a CAD review app within a week — but legal forbade adding the acquired company’s people to our employee directory as full members, and security flagged that the acquired tenant’s MFA was SMS-only. We went B2B, not a tenant merge. The gotcha surfaced fast: with isMfaAccepted: true on their per-partner policy, guests sailed past our phishing-resistant CA policy on the strength of an SMS code we did not trust. Trust is binary per partner — you cannot say “accept their MFA but only the strong factors.”
The fix was to not trust their MFA and instead let our own Conditional Access re-challenge guests with a guest-scoped policy requiring an authenticator app, while keeping inbound collaboration tightly scoped to one security group in their tenant and our two apps:
{
"inboundTrust": {
"isMfaAccepted": false,
"isCompliantDeviceAccepted": false
},
"b2bCollaborationInbound": {
"usersAndGroups": {
"accessType": "allowed",
"targets": [
{ "target": "4d4d3e3e-2f2f-1a1a-0b0b-9c9c8d8d7e7e", "targetType": "group" }
]
}
}
}
Pairing that with an entitlement-management access package set to auto-expire at the integration deadline meant offboarding was automatic when the directory merge finally happened months later. The lesson: external MFA trust is a per-partner bet on their factor strength, not a friction toggle — when in doubt, keep it false and own the challenge yourself.
Verify
Confirm the controls actually took effect before declaring victory.
# 1. Per-partner XTAP reflects allow + trust
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/partners/$PARTNER_TENANT" \
--query "{mfa:inboundTrust.isMfaAccepted, inbound:b2bCollaborationInbound.usersAndGroups.accessType}"
# 2. Invitation policy is locked to inviters
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/policies/authorizationPolicy" \
--query "allowInvitesFrom"
# 3. Pending vs redeemed guests
az rest --method GET \
--uri "https://graph.microsoft.com/v1.0/users?\$filter=userType eq 'Guest'&\$select=displayName,externalUserState,createdDateTime" \
--query "value[].{name:displayName, state:externalUserState}"
Then validate behavior, not just config:
- Have a pilot guest from the trusted partner sign in to a CA-gated app and confirm they are not re-prompted for MFA (proves
isMfaAcceptedis honored). - Try inviting a
@gmail.comaddress while an allowlist is set — it should be rejected. - In Entra admin center -> Identity -> Monitoring & health -> Sign-in logs, filter by User type = Guest and inspect the Cross-tenant access type and Authentication requirement columns to confirm the home-tenant MFA claim flowed through.
Checklist
Pitfalls
- Leaving the default XTAP wide open. A permissive default silently trusts every tenant on the planet; per-partner policies only help if the default is locked.
- Trusting MFA from tenants you shouldn’t.
isMfaAcceptedaccepts the partner’s word that the user did MFA. Set it per trusted partner, never globally on the default policy. - Confusing the two External ID worlds. User flows, API connectors, and email-OTP-as-primary belong to the external (customer) tenant; cross-tenant access settings and guest objects belong to the workforce (B2B) tenant. Applying one model’s settings to the other wastes hours.
- Deleting guests to “fix” auth problems. It orphans every assignment. Use
resetRedemptionfor IdP changes and access reviews for cleanup. - Unsecured API connectors. An unauthenticated sign-up endpoint is remote-code-influence on your onboarding logic. Require a validated bearer token and make the handler idempotent and fast.
Next steps: wire B2B sign-ins into Conditional Access with a guest-specific policy (require compliant device or trusted MFA, block legacy auth), and ship sign-in and audit logs to Log Analytics so guest activity is queryable in KQL and retained beyond the default window.