Graph API Voorbeeldcode
Herbruikbare C# snippets voor de automatisering van de B2C → Entra External ID migratie via Microsoft Graph API.
Gebruik Microsoft.Graph en Azure.Identity NuGet-packages. Configureer een App Registration in Entra met de rechten Application.ReadWrite.All en Directory.ReadWrite.All.
// NuGet: dotnet add package Microsoft.Graph
// NuGet: dotnet add package Azure.Identity
using Azure.Identity;
using Microsoft.Graph;
// App Registration gegevens (sla op in appsettings of Key Vault)
var tenantId = "YOUR_TENANT_ID"; // bijv. crowssodevelopment.onmicrosoft.com
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var graphClient = new GraphServiceClient(credential);
// Test: haal de tenant-info op
var org = await graphClient.Organization.GetAsync();
Console.WriteLine($"Verbonden met: {org?.Value?.FirstOrDefault()?.DisplayName}");
{
"GraphApi": {
"TenantId": "crowssodevelopment.onmicrosoft.com",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientSecret": "~uw~secret~hier~"
},
"B2C": {
"TenantId": "uwtenant.onmicrosoft.com",
"ClientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"ClientSecret": "~b2c~secret~hier~"
}
}
Maakt een Enterprise Application aan in Entra External ID op basis van de SAML-configuratie van een klant (Entity ID, ACS URL, claims). Dit is het kernscript dat bij elke golf 20–50 keer wordt uitgevoerd.
using Microsoft.Graph;
using Microsoft.Graph.Models;
/// Maakt een Enterprise Application met SAML 2.0 SSO aan in Entra External ID.
/// Gebaseerd op de configuratie uit B2C per klant.
public async Task<ServicePrincipal> CreateSamlEnterpriseAppAsync(
GraphServiceClient graph,
string displayName,
string entityId, // SP Entity ID van de klant
string acsUrl, // Assertion Consumer Service URL
string logoutUrl = "")
{
// Stap 1: Maak de Application Registration aan
var app = await graph.Applications.PostAsync(new Application
{
DisplayName = displayName,
SignInAudience = "AzureADMyOrg",
Web = new WebApplication
{
RedirectUris = new List<string> { acsUrl }
},
IdentifierUris = new List<string> { entityId }
});
// Stap 2: Maak de bijbehorende Service Principal (= Enterprise App)
var sp = await graph.ServicePrincipals.PostAsync(new ServicePrincipal
{
AppId = app!.AppId,
DisplayName = displayName,
PreferredSingleSignOnMode = "saml",
ReplyUrls = new List<string> { acsUrl },
LogoutUrl = logoutUrl,
Tags = new List<string> { "WindowsAzureActiveDirectoryIntegratedApp" }
});
// Stap 3: Stel de SAML-identifiers in (Entity ID en ACS URL)
await graph.ServicePrincipals[sp!.Id].PostAsync(new ServicePrincipal
{
PreferredSingleSignOnMode = "saml"
});
Console.WriteLine($"✓ Enterprise App aangemaakt: {displayName} (AppId: {app.AppId})");
return sp!;
}
// Gebruik:
var sp = await CreateSamlEnterpriseAppAsync(
graph,
displayName: "Klant BV — SAML SSO",
entityId: "https://klant.nl/saml/metadata",
acsUrl: "https://klant.nl/saml/acs",
logoutUrl: "https://klant.nl/saml/logout"
);
Na het aanmaken van de Enterprise App worden de claims geconfigureerd die in de SAML-assertion worden opgenomen (NameID, email, rollen).
/// Voegt een claimsmapping policy toe aan een Service Principal.
/// Zorg dat de policy al bestaat, of maak 'm hieronder aan.
public async Task SetClaimsMappingAsync(
GraphServiceClient graph,
string servicePrincipalId)
{
// Stap 1: Maak een ClaimsMappingPolicy aan
var policy = await graph.Policies.ClaimsMappingPolicies.PostAsync(
new ClaimsMappingPolicy
{
DisplayName = "SAML Kennisbank Claims",
Definition = new List<string>
{
"""
{
"ClaimsMappingPolicy": {
"Version": 1,
"IncludeBasicClaimSet": true,
"ClaimsSchema": [
{
"Source": "user",
"ID": "userprincipalname",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"
},
{
"Source": "user",
"ID": "mail",
"SamlClaimType": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
},
{
"Source": "user",
"ID": "displayname",
"SamlClaimType": "http://schemas.microsoft.com/identity/claims/displayname"
}
]
}
}
"""
},
IsOrganizationDefault = false
});
// Stap 2: Koppel de policy aan de Service Principal
await graph.ServicePrincipals[servicePrincipalId]
.ClaimsMappingPolicies.Ref
.PostAsync(new ReferenceCreate
{
OdataId = $"https://graph.microsoft.com/v1.0/policies/claimsMappingPolicies/{policy!.Id}"
});
Console.WriteLine($"✓ Claimsmapping gekoppeld aan SP {servicePrincipalId}");
}
Haalt alle SAML-applicaties op uit de B2C-tenant en exporteert de configuratie naar JSON. Gebruik als startpunt voor de bulk-import naar Entra External ID.
using System.Text.Json;
/// Exporteert alle Enterprise Apps met SAML SSO uit B2C naar een lijst objecten.
public async Task<List<SamlAppExport>> ExportB2CSamlAppsAsync(GraphServiceClient b2cGraph)
{
var result = new List<SamlAppExport>();
// Filter op SAML SSO (PreferredSingleSignOnMode = "saml")
var sps = await b2cGraph.ServicePrincipals.GetAsync(req =>
{
req.QueryParameters.Filter = "preferredSingleSignOnMode eq 'saml'";
req.QueryParameters.Select = new[]
{
"id", "appId", "displayName",
"replyUrls", "logoutUrl",
"samlSingleSignOnSettings"
};
req.QueryParameters.Top = 999;
});
foreach (var sp in sps?.Value ?? [])
{
// Haal bijbehorende Application op voor de identifierUris (= Entity ID)
var apps = await b2cGraph.Applications.GetAsync(req =>
req.QueryParameters.Filter = $"appId eq '{sp.AppId}'");
var identifierUri = apps?.Value?.FirstOrDefault()?.IdentifierUris?.FirstOrDefault();
result.Add(new SamlAppExport
{
DisplayName = sp.DisplayName ?? "",
AppId = sp.AppId ?? "",
EntityId = identifierUri ?? "",
AcsUrl = sp.ReplyUrls?.FirstOrDefault() ?? "",
LogoutUrl = sp.LogoutUrl ?? "",
SsoUrl = sp.SamlSingleSignOnSettings?.RelayState ?? ""
});
}
// Schrijf naar JSON-bestand
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync("b2c-saml-export.json", json);
Console.WriteLine($"✓ {result.Count} SAML-apps geëxporteerd naar b2c-saml-export.json");
return result;
}
// DTO voor de export
public record SamlAppExport(
string DisplayName,
string AppId,
string EntityId,
string AcsUrl,
string LogoutUrl,
string SsoUrl
)
{
public SamlAppExport() : this("", "", "", "", "", "") { }
public string DisplayName { get; set; } = DisplayName;
public string AppId { get; set; } = AppId;
public string EntityId { get; set; } = EntityId;
public string AcsUrl { get; set; } = AcsUrl;
public string LogoutUrl { get; set; } = LogoutUrl;
public string SsoUrl { get; set; } = SsoUrl;
}
Leest het JSON-exportbestand in en maakt alle Enterprise Applications automatisch aan in de Entra External ID tenant. Vervangt 200+ handmatige handelingen. Gebruik paginering en rate-limit-afhandeling bij grote aantallen.
using System.Text.Json;
/// Importeert alle SAML-apps uit het exportbestand naar Entra External ID.
public async Task BulkImportAsync(
GraphServiceClient externalIdGraph,
string exportFilePath = "b2c-saml-export.json")
{
var json = await File.ReadAllTextAsync(exportFilePath);
var apps = JsonSerializer.Deserialize<List<SamlAppExport>>(json) ?? [];
Console.WriteLine($"▶ {apps.Count} apps importeren naar Entra External ID...");
var success = 0;
var failed = new List<string>();
foreach (var app in apps)
{
try
{
await CreateSamlEnterpriseAppAsync(
externalIdGraph,
displayName: app.DisplayName,
entityId: TransformEntityId(app.EntityId),
acsUrl: app.AcsUrl,
logoutUrl: app.LogoutUrl
);
success++;
// Respecteer Graph API rate limit (max ~4 req/s per tenant)
await Task.Delay(300);
}
catch (Exception ex)
{
Console.WriteLine($"✗ Fout bij {app.DisplayName}: {ex.Message}");
failed.Add(app.DisplayName);
}
}
Console.WriteLine($"\n✓ Klaar: {success}/{apps.Count} geslaagd.");
if (failed.Any())
Console.WriteLine($"✗ Mislukt: {string.Join(", ", failed)}");
}
/// Transformeer B2C-specifieke URLs naar External ID equivalenten.
/// Vervang login.microsoftonline.com → ciamlogin.com endpoints.
private string TransformEntityId(string entityId) =>
entityId
.Replace("login.microsoftonline.com", "ciamlogin.com")
.Replace("sts.windows.net", "ciamlogin.net");
Zelfstandig .NET 8 console-project dat de volledige migratiepipeline uitvoert: exporteren uit B2C, transformeren, importeren in External ID.
// dotnet new console -n SamlMigrator
// dotnet add package Microsoft.Graph
// dotnet add package Azure.Identity
using Azure.Identity;
using Microsoft.Graph;
var b2cTenantId = args.ElementAtOrDefault(0) ?? throw new Exception("Geef B2C tenant ID mee als argument");
var externalTenantId = args.ElementAtOrDefault(1) ?? throw new Exception("Geef External ID tenant ID mee");
var clientId = args.ElementAtOrDefault(2) ?? throw new Exception("Geef Client ID mee");
var clientSecret = args.ElementAtOrDefault(3) ?? throw new Exception("Geef Client Secret mee");
// Twee Graph-clients: één voor B2C (bron), één voor External ID (doel)
var b2cGraph = new GraphServiceClient(
new ClientSecretCredential(b2cTenantId, clientId, clientSecret));
var externalGraph = new GraphServiceClient(
new ClientSecretCredential(externalTenantId, clientId, clientSecret));
var migrator = new SamlMigrator();
// Stap 1: Exporteer uit B2C
Console.WriteLine("=== Stap 1: Export uit Azure AD B2C ===");
await migrator.ExportB2CSamlAppsAsync(b2cGraph);
// Stap 2: Importeer in External ID
Console.WriteLine("\n=== Stap 2: Import in Entra External ID ===");
await migrator.BulkImportAsync(externalGraph);
Console.WriteLine("\n✓ Migratie voltooid.");
// Uitvoeren:
// dotnet run -- <b2c-tenant-id> <external-tenant-id> <client-id> <client-secret>
Exporteert alle gebruikers inclusief extensie-attributen uit B2C. Wachtwoorden kunnen niet worden gemigreerd — gebruikers ontvangen een password-reset e-mail bij eerste inlog (SSPR).
/// Exporteert alle gebruikers met relevante attributen uit B2C.
public async Task ExportUsersAsync(GraphServiceClient b2cGraph, string b2cExtensionAppId)
{
// Extensie-attributen hebben een prefix op basis van de App Registration ID
// zonder koppeltekens: b2c_extension_<appId>_<attribuutNaam>
var prefix = $"extension_{b2cExtensionAppId.Replace("-", "")}_";
var users = await b2cGraph.Users.GetAsync(req =>
{
req.QueryParameters.Select = new[]
{
"id", "displayName", "givenName", "surname",
"mail", "userPrincipalName", "identities",
$"{prefix}organisatieId", // voorbeeldattribuut
$"{prefix}rolCode" // voorbeeldattribuut
};
req.QueryParameters.Top = 999;
});
var exportList = users?.Value?.Select(u => new
{
u.Id,
u.DisplayName,
u.GivenName,
u.Surname,
u.Mail,
u.UserPrincipalName,
Identities = u.Identities?.Select(i => new { i.SignInType, i.IssuerAssignedId }),
AdditionalData = u.AdditionalData
}).ToList();
var json = JsonSerializer.Serialize(exportList, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync("b2c-users-export.json", json);
Console.WriteLine($"✓ {exportList?.Count ?? 0} gebruikers geëxporteerd.");
}
Importeert gebruikers in External ID met een tijdelijk wachtwoord. Bij de eerste inlog wordt de gebruiker doorgestuurd naar SSPR voor wachtwoordherstel.
/// Importeert een gebruiker in Entra External ID.
/// Wachtwoord wordt als 'must change at next sign-in' ingesteld.
public async Task ImportUserAsync(GraphServiceClient externalGraph, UserExport source)
{
var newUser = new User
{
DisplayName = source.DisplayName,
GivenName = source.GivenName,
Surname = source.Surname,
Mail = source.Mail,
UserPrincipalName = $"{source.Mail?.Replace("@", "_")}@uwtenant.onmicrosoft.com",
AccountEnabled = true,
PasswordProfile = new PasswordProfile
{
Password = GenerateTempPassword(),
ForceChangePasswordNextSignIn = true
},
Identities = new List<ObjectIdentity>
{
new()
{
SignInType = "emailAddress",
Issuer = "uwtenant.onmicrosoft.com",
IssuerAssignedId = source.Mail
}
}
};
var created = await externalGraph.Users.PostAsync(newUser);
Console.WriteLine($"✓ Gebruiker aangemaakt: {created?.DisplayName} ({created?.Id})");
}
// Genereer een veilig tijdelijk wachtwoord (16 tekens, voldoet aan complexiteitseisen)
private static string GenerateTempPassword()
{
const string chars = "ABCDEFGHJKMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#";
var rng = System.Security.Cryptography.RandomNumberGenerator.Create();
var bytes = new byte[16];
rng.GetBytes(bytes);
return new string(bytes.Select(b => chars[b % chars.Length]).ToArray());
}
Application.ReadWrite.All, Directory.ReadWrite.All, User.ReadWrite.All.
Verleen consent via: Azure Portal → App Registration → API permissions → Grant admin consent.