Skip to main content

Microsoft Graph API

RoundTrip uses the Microsoft Graph API to manage users in the Entra External ID CIAM tenant (roundtripapp.onmicrosoft.com). This includes creating user accounts, sending invitations, assigning app roles, and setting custom extension attributes that link Entra users to RoundTrip tenants.


What Graph API Does

OperationMethodPurpose
Invite userPOST /invitationsCreates a guest invitation for a new tenant user
Create member userPOST /usersCreates a local user account directly in the CIAM tenant
Set tenant ID attributePATCH /users/{id}Sets the tid custom extension attribute linking the user to their RoundTrip tenant
Assign app rolePOST /users/{id}/appRoleAssignmentsAssigns a role (TenantAdmin, Dispatcher, Technician, etc.) to the user

Critical Configuration

:::danger Use b2c-extensions-app Credentials — Not RoundTrip API GraphUserService must be configured with credentials from the b2c-extensions-app registration — not the RoundTrip API registration. The b2c-extensions-app owns the custom extension attributes (like _tid) and has the required Graph permissions. Using the wrong app registration causes user creation and role assignment to fail. :::

The b2c-extensions-app Registration

PropertyValue
Display Nameb2c-extensions-app. Do not modify.
Client ID74ae24d6-6fa9-4110-9571-2cfddae0db04
Tenant ID08f09e72-40f3-4443-8fec-3a77c4c0d1ee
Tenantroundtripapp.onmicrosoft.com
Client SecretBitwarden → "Entra b2c-extensions-app Client Secret"
Secret rotationEvery 24 months — see Rotating the Client Secret

Required Graph Permissions (Application)

PermissionPurpose
User.Invite.AllSend user invitations
User.ReadWrite.AllCreate users, set extension attributes
Directory.ReadWrite.AllRead/write directory data
AppRoleAssignment.ReadWrite.AllAssign app roles to users

All permissions are Application type (not Delegated) and require admin consent granted in the roundtripapp tenant.


Configuration

Key Vault Secrets

Secret NameConfig KeyValue
GraphApi--TenantIdGraphApi:TenantId08f09e72-40f3-4443-8fec-3a77c4c0d1ee
GraphApi--ClientIdGraphApi:ClientId74ae24d6-6fa9-4110-9571-2cfddae0db04
GraphApi--ClientSecretGraphApi:ClientSecretBitwarden → "Entra b2c-extensions-app Client Secret"
GraphApi--ExtensionAttributePrefixGraphApi:ExtensionAttributePrefixextension_74ae24d66fa9411095712cfddae0db04

Extension Attribute Prefix

The tid custom attribute is stored as an Entra extension property. The attribute name is:

extension_{b2c-extensions-app-client-id-no-hyphens}_tid

Which resolves to:

extension_74ae24d66fa9411095712cfddae0db04_tid

The Client ID with hyphens removed is 74ae24d66fa9411095712cfddae0db04. This prefix must match exactly — if it is wrong, the tid attribute will not be found on users and tenant scoping will fail.


App Roles

App roles are defined on the RoundTrip API app registration and assigned via Graph API during user onboarding.

RoleApp Role IDPermissions
TenantAdminc626f6f7-8399-46aa-b10f-dab79d8befbbFull tenant access
Dispatchere92ce80e-3223-469c-bbde-19f64cc5283dDispatch board, ticket management
Technician5a01e722-b83d-401a-80d1-7b793e485142PWA access, ticket updates
BillingStaffb7a6b1fc-74b3-4770-b68d-ac1eeb74b21fInvoice and billing access
ReadOnly837257a4-cda0-406e-93c0-b816b19e81d7Read-only access across the app

Service Principal Object ID (used for role assignment target): 5be28949-5ad8-4409-8fdd-e2db74327c54


User Invitation Flow

When a TenantAdmin invites a new user through the RoundTrip admin panel:

1. InviteUserCommand → InviteUserHandler

├─ GraphUserService.InviteUserAsync(email, displayName)
│ └─ POST /invitations (SendInvitationMessage = false)
│ └─ Returns Entra Object ID of invited user

├─ GraphUserService.SetTenantIdAsync(entraUserId, tenantId)
│ └─ PATCH /users/{id}
│ └─ Sets extension_..._tid = tenantId

├─ GraphUserService.AssignAppRoleAsync(entraUserId, role)
│ └─ POST /users/{id}/appRoleAssignments
│ └─ Links user to their role in Entra

└─ Hangfire.Enqueue<SendInviteEmailJob>(entraUserId, email)
└─ SendGridEmailService.SendWelcomeEmailAsync()
└─ "Welcome to RoundTrip" email delivered via SendGrid

:::info Entra Invitation Email is Suppressed SendInvitationMessage is set to false in the Graph invitation call. Entra does not send its own invitation email. The welcome email is sent by SendGrid instead, giving full control over the design and content. :::


Troubleshooting

"GraphApi:ClientSecret is not configured"

The App Service environment variable is missing or the Key Vault reference is not resolving.

  1. Check Azure Portal → app-roundtrip-production → Environment variables
  2. Verify GraphApi__ClientSecret, GraphApi__ClientId, GraphApi__TenantId, and GraphApi__ExtensionAttributePrefix all exist with green Key Vault checkmarks
  3. If any are missing — add them (see Configuration above)
  4. Full stop → start the App Service after adding Key Vault references
  5. Requeue failed Hangfire jobs from the dashboard

User invited but cannot log in

  1. Check Entra External ID portal → Users — confirm the user exists
  2. Check the user's extension_..._tid attribute is set correctly
  3. Check the user has the correct app role assigned
  4. If the tid attribute is missing, the SetTenantIdAsync step failed — check Hangfire logs for the invite job

App role not appearing in JWT claims

App role assignments can take a few minutes to propagate in Entra. If a user logs in immediately after being assigned a role, their JWT may not yet include the role claim. Ask them to sign out and sign back in.


Rotating the Client Secret

The b2c-extensions-app client secret expires every 24 months. Rotate it before it expires to avoid a production outage.

  1. Go to Azure Portal → roundtripapp tenant → App registrations → b2c-extensions-app
  2. Certificates & secrets → New client secret
  3. Name it roundtrip-prod-{year}, set expiry to 24 months
  4. Copy the Value immediately — it is only shown once
  5. Update GraphApi--ClientSecret in kv-roundtrip-production with + New version
  6. Update Bitwarden → "Entra b2c-extensions-app Client Secret"
  7. Full stop → start app-roundtrip-production
  8. Delete the old secret version from the app registration
  9. Update the expiry date in this document

Current secret expires: _fill_in_from_Entra_portal_