diff --git a/README.md b/README.md index 6727a1b..3ff8e7f 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,11 @@ The code in this sample backs the [Manage Azure AD B2C user accounts with Micros ## Setup 1. Clone the repo or download and extract the [ZIP archive](https://github.com/Azure-Samples/ms-identity-dotnetcore-b2c-account-management/archive/master.zip) -1. Modify `./src/appsettings.json` with values appropriate for your environment: +2. Modify `./src/appsettings.json` with values appropriate for your environment: - Azure AD B2C **tenant ID** - Registered application's **Application (client) ID** - Registered application's **Client secret** -1. Build the application with `dotnet build`: +3. Build the application with `dotnet build`: ```console azureuser@machine:~/ms-identity-dotnetcore-b2c-account-management$ cd src @@ -61,6 +61,10 @@ The code in this sample backs the [Manage Azure AD B2C user accounts with Micros Time Elapsed 00:00:02.62 ``` +4. Add 2 custom attributes to your B2C instance in order to run all the sample operations with custom attributes involved. + Attributes to add: + - FavouriteSeason (string) + - LovesPets (boolean) ## Running the sample @@ -79,6 +83,8 @@ Command Description [4] Delete user by object ID [5] Update user password [6] Create users (bulk import) +[7] Create user with custom attributes and show result +[8] Get all users (one page) with custom attributes [help] Show available commands [exit] Exit the program ------------------------- diff --git a/src/Helpers/B2cCustomAttributeHelper.cs b/src/Helpers/B2cCustomAttributeHelper.cs new file mode 100644 index 0000000..245ed6f --- /dev/null +++ b/src/Helpers/B2cCustomAttributeHelper.cs @@ -0,0 +1,22 @@ +namespace b2c_ms_graph.Helpers +{ + internal class B2cCustomAttributeHelper + { + internal readonly string _b2cExtensionAppClientId; + + internal B2cCustomAttributeHelper(string b2cExtensionAppClientId) + { + _b2cExtensionAppClientId = b2cExtensionAppClientId.Replace("-", ""); + } + + internal string GetCompleteAttributeName(string attributeName) + { + if (string.IsNullOrWhiteSpace(attributeName)) + { + throw new System.ArgumentException("Parameter cannot be null", nameof(attributeName)); + } + + return $"extension_{_b2cExtensionAppClientId}_{attributeName}"; + } + } +} diff --git a/src/Helpers/PasswordHelper.cs b/src/Helpers/PasswordHelper.cs new file mode 100644 index 0000000..89aa932 --- /dev/null +++ b/src/Helpers/PasswordHelper.cs @@ -0,0 +1,37 @@ +using System; + +namespace b2c_ms_graph.Helpers +{ + public static class PasswordHelper + { + public static string GenerateNewPassword(int lowercase, int uppercase, int numerics) + { + string lowers = "abcdefghijklmnopqrstuvwxyz"; + string uppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + string number = "0123456789"; + + Random random = new Random(); + + string generated = "!"; + for (int i = 1; i <= lowercase; i++) + generated = generated.Insert( + random.Next(generated.Length), + lowers[random.Next(lowers.Length - 1)].ToString() + ); + + for (int i = 1; i <= uppercase; i++) + generated = generated.Insert( + random.Next(generated.Length), + uppers[random.Next(uppers.Length - 1)].ToString() + ); + + for (int i = 1; i <= numerics; i++) + generated = generated.Insert( + random.Next(generated.Length), + number[random.Next(number.Length - 1)].ToString() + ); + + return generated.Replace("!", string.Empty); + } + } +} diff --git a/src/Models/AppSettings.cs b/src/Models/AppSettings.cs index 091cfc6..15e3b04 100644 --- a/src/Models/AppSettings.cs +++ b/src/Models/AppSettings.cs @@ -35,6 +35,9 @@ public class AppSettings [JsonProperty(PropertyName = "ClientSecret")] public string ClientSecret { get; set; } + [JsonProperty(PropertyName = "B2cExtensionAppClientId")] + public string B2cExtensionAppClientId { get; set; } + [JsonProperty(PropertyName = "UsersFileName")] public string UsersFileName { get; set; } diff --git a/src/Program.cs b/src/Program.cs index 36e1f3e..1f01aef 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -55,6 +55,12 @@ static async Task Main(string[] args) case "6": await UserService.BulkCreate(config, graphClient); break; + case "7": + await UserService.CreateUserWithCustomAttribute(graphClient, config.B2cExtensionAppClientId, config.TenantId); + break; + case "8": + await UserService.ListUsersWithCustomAttribute(graphClient, config.B2cExtensionAppClientId); + break; case "help": Program.PrintCommands(); break; @@ -91,6 +97,8 @@ private static void PrintCommands() Console.WriteLine("[4] Delete user by object ID"); Console.WriteLine("[5] Update user password"); Console.WriteLine("[6] Create users (bulk import)"); + Console.WriteLine("[7] Create user with custom attributes and show result"); + Console.WriteLine("[8] Get all users (one page) with custom attributes"); Console.WriteLine("[help] Show available commands"); Console.WriteLine("[exit] Exit the program"); Console.WriteLine("-------------------------"); diff --git a/src/Services/UserService.cs b/src/Services/UserService.cs index 9008433..9aa1162 100644 --- a/src/Services/UserService.cs +++ b/src/Services/UserService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.Graph; @@ -32,6 +33,40 @@ public static async Task ListUsers(GraphServiceClient graphClient) } } + public static async Task ListUsersWithCustomAttribute(GraphServiceClient graphClient, string b2cExtensionAppClientId) + { + if (string.IsNullOrWhiteSpace(b2cExtensionAppClientId)) + { + throw new ArgumentException("B2cExtensionAppClientId (its Application ID) is missing from appsettings.json. Find it in the App registrations pane in the Azure portal. The app registration has the name 'b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.'.", nameof(b2cExtensionAppClientId)); + } + + // Declare the names of the custom attributes + const string customAttributeName1 = "FavouriteSeason"; + const string customAttributeName2 = "LovesPets"; + + // Get the complete name of the custom attribute (Azure AD extension) + Helpers.B2cCustomAttributeHelper helper = new Helpers.B2cCustomAttributeHelper(b2cExtensionAppClientId); + string favouriteSeasonAttributeName = helper.GetCompleteAttributeName(customAttributeName1); + string lovesPetsAttributeName = helper.GetCompleteAttributeName(customAttributeName2); + + Console.WriteLine($"Getting list of users with the custom attributes '{customAttributeName1}' (string) and '{customAttributeName2}' (boolean)"); + Console.WriteLine(); + + // Get all users (one page) + var result = await graphClient.Users + .Request() + .Select($"id,displayName,identities,{favouriteSeasonAttributeName},{lovesPetsAttributeName}") + .GetAsync(); + + foreach (var user in result.CurrentPage) + { + Console.WriteLine(JsonConvert.SerializeObject(user)); + + // Only output the custom attributes... + //Console.WriteLine(JsonConvert.SerializeObject(user.AdditionalData)); + } + } + public static async Task GetUserById(GraphServiceClient graphClient) { Console.Write("Enter user object ID: "); @@ -135,7 +170,7 @@ public static async Task SetPasswordByUserId(GraphServiceClient graphClient) var user = new User { - PasswordPolicies = "DisablePasswordExpiration,DisableStrongPassword", + PasswordPolicies = "DisablePasswordExpiration,DisableStrongPassword", PasswordProfile = new PasswordProfile { ForceChangePasswordNextSignIn = false, @@ -202,5 +237,95 @@ public static async Task BulkCreate(AppSettings config, GraphServiceClient graph } } } + + public static async Task CreateUserWithCustomAttribute(GraphServiceClient graphClient, string b2cExtensionAppClientId, string tenantId) + { + if (string.IsNullOrWhiteSpace(b2cExtensionAppClientId)) + { + throw new ArgumentException("B2C Extension App ClientId (ApplicationId) is missing in the appsettings.json. Get it from the App Registrations blade in the Azure portal. The app registration has the name 'b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.'.", nameof(b2cExtensionAppClientId)); + } + + // Declare the names of the custom attributes + const string customAttributeName1 = "FavouriteSeason"; + const string customAttributeName2 = "LovesPets"; + + // Get the complete name of the custom attribute (Azure AD extension) + Helpers.B2cCustomAttributeHelper helper = new Helpers.B2cCustomAttributeHelper(b2cExtensionAppClientId); + string favouriteSeasonAttributeName = helper.GetCompleteAttributeName(customAttributeName1); + string lovesPetsAttributeName = helper.GetCompleteAttributeName(customAttributeName2); + + Console.WriteLine($"Create a user with the custom attributes '{customAttributeName1}' (string) and '{customAttributeName2}' (boolean)"); + + // Fill custom attributes + IDictionary extensionInstance = new Dictionary(); + extensionInstance.Add(favouriteSeasonAttributeName, "summer"); + extensionInstance.Add(lovesPetsAttributeName, true); + + try + { + // Create user + var result = await graphClient.Users + .Request() + .AddAsync(new User + { + GivenName = "Casey", + Surname = "Jensen", + DisplayName = "Casey Jensen", + Identities = new List + { + new ObjectIdentity() + { + SignInType = "emailAddress", + Issuer = tenantId, + IssuerAssignedId = "casey.jensen@example.com" + } + }, + PasswordProfile = new PasswordProfile() + { + Password = Helpers.PasswordHelper.GenerateNewPassword(4, 8, 4) + }, + PasswordPolicies = "DisablePasswordExpiration", + AdditionalData = extensionInstance + }); + + string userId = result.Id; + + Console.WriteLine($"Created the new user. Now get the created user with object ID '{userId}'..."); + + // Get created user by object ID + result = await graphClient.Users[userId] + .Request() + .Select($"id,givenName,surName,displayName,identities,{favouriteSeasonAttributeName},{lovesPetsAttributeName}") + .GetAsync(); + + if (result != null) + { + Console.ForegroundColor = ConsoleColor.Blue; + Console.WriteLine($"DisplayName: {result.DisplayName}"); + Console.WriteLine($"{customAttributeName1}: {result.AdditionalData[favouriteSeasonAttributeName].ToString()}"); + Console.WriteLine($"{customAttributeName2}: {result.AdditionalData[lovesPetsAttributeName].ToString()}"); + Console.WriteLine(); + Console.ResetColor(); + Console.WriteLine(JsonConvert.SerializeObject(result, Formatting.Indented)); + } + } + catch (ServiceException ex) + { + if (ex.StatusCode == System.Net.HttpStatusCode.BadRequest) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Have you created the custom attributes '{customAttributeName1}' (string) and '{customAttributeName2}' (boolean) in your tenant?"); + Console.WriteLine(); + Console.WriteLine(ex.Message); + Console.ResetColor(); + } + } + catch (Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(ex.Message); + Console.ResetColor(); + } + } } -} \ No newline at end of file +} diff --git a/src/appsettings.json b/src/appsettings.json index 87f98f2..915fe96 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -3,6 +3,7 @@ "TenantId": "your-b2c-tenant.onmicrosoft.com", "AppId": "Application (client) ID", "ClientSecret": "Client secret", + "B2cExtensionAppClientId": "Find this Application (client) ID in the App registrations pane in the Azure portal. The app registration is named 'b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.'.", "UsersFileName": "users.json" } } diff --git a/src/b2c-ms-graph.csproj b/src/b2c-ms-graph.csproj index 2c0274a..c0e2b9a 100644 --- a/src/b2c-ms-graph.csproj +++ b/src/b2c-ms-graph.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.0 + netcoreapp3.1 b2c_ms_graph @@ -13,7 +13,7 @@ - +