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
| Operation | Method | Purpose |
|---|---|---|
| Invite user | POST /invitations | Creates a guest invitation for a new tenant user |
| Create member user | POST /users | Creates a local user account directly in the CIAM tenant |
| Set tenant ID attribute | PATCH /users/{id} | Sets the tid custom extension attribute linking the user to their RoundTrip tenant |
| Assign app role | POST /users/{id}/appRoleAssignments | Assigns 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
| Property | Value |
|---|---|
| Display Name | b2c-extensions-app. Do not modify. |
| Client ID | 74ae24d6-6fa9-4110-9571-2cfddae0db04 |
| Tenant ID | 08f09e72-40f3-4443-8fec-3a77c4c0d1ee |
| Tenant | roundtripapp.onmicrosoft.com |
| Client Secret | Bitwarden → "Entra b2c-extensions-app Client Secret" |
| Secret rotation | Every 24 months — see Rotating the Client Secret |
Required Graph Permissions (Application)
| Permission | Purpose |
|---|---|
User.Invite.All | Send user invitations |
User.ReadWrite.All | Create users, set extension attributes |
Directory.ReadWrite.All | Read/write directory data |
AppRoleAssignment.ReadWrite.All | Assign 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 Name | Config Key | Value |
|---|---|---|
GraphApi--TenantId | GraphApi:TenantId | 08f09e72-40f3-4443-8fec-3a77c4c0d1ee |
GraphApi--ClientId | GraphApi:ClientId | 74ae24d6-6fa9-4110-9571-2cfddae0db04 |
GraphApi--ClientSecret | GraphApi:ClientSecret | Bitwarden → "Entra b2c-extensions-app Client Secret" |
GraphApi--ExtensionAttributePrefix | GraphApi:ExtensionAttributePrefix | extension_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.
| Role | App Role ID | Permissions |
|---|---|---|
TenantAdmin | c626f6f7-8399-46aa-b10f-dab79d8befbb | Full tenant access |
Dispatcher | e92ce80e-3223-469c-bbde-19f64cc5283d | Dispatch board, ticket management |
Technician | 5a01e722-b83d-401a-80d1-7b793e485142 | PWA access, ticket updates |
BillingStaff | b7a6b1fc-74b3-4770-b68d-ac1eeb74b21f | Invoice and billing access |
ReadOnly | 837257a4-cda0-406e-93c0-b816b19e81d7 | Read-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.
- Check Azure Portal →
app-roundtrip-production→ Environment variables - Verify
GraphApi__ClientSecret,GraphApi__ClientId,GraphApi__TenantId, andGraphApi__ExtensionAttributePrefixall exist with green Key Vault checkmarks - If any are missing — add them (see Configuration above)
- Full stop → start the App Service after adding Key Vault references
- Requeue failed Hangfire jobs from the dashboard
User invited but cannot log in
- Check Entra External ID portal → Users — confirm the user exists
- Check the user's
extension_..._tidattribute is set correctly - Check the user has the correct app role assigned
- If the
tidattribute is missing, theSetTenantIdAsyncstep 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.
- Go to Azure Portal →
roundtripapptenant → App registrations →b2c-extensions-app - Certificates & secrets → New client secret
- Name it
roundtrip-prod-{year}, set expiry to 24 months - Copy the Value immediately — it is only shown once
- Update
GraphApi--ClientSecretinkv-roundtrip-productionwith + New version - Update Bitwarden → "Entra b2c-extensions-app Client Secret"
- Full stop → start
app-roundtrip-production - Delete the old secret version from the app registration
- Update the expiry date in this document
Current secret expires: _fill_in_from_Entra_portal_