├── Authorization-AppRoles ├── AppRoles.WebApp │ ├── Pages │ │ ├── _ViewStart.cshtml │ │ ├── _ViewImports.cshtml │ │ ├── AdminOnly.cshtml │ │ ├── Index.cshtml.cs │ │ ├── Identity.cshtml.cs │ │ ├── AdminOnly.cshtml.cs │ │ ├── Error.cshtml.cs │ │ ├── Error.cshtml │ │ ├── Index.cshtml │ │ ├── Shared │ │ │ ├── _LoginPartial.cshtml │ │ │ └── _Layout.cshtml │ │ └── Identity.cshtml │ ├── Services │ │ ├── AppRolesOptions.cs │ │ ├── IAppRolesProvider.cs │ │ ├── AzureADAppRolesProviderOptions.cs │ │ ├── StringSplitClaimTransformation.cs │ │ └── AzureADAppRolesProvider.cs │ ├── appsettings.Development.json │ ├── wwwroot │ │ ├── js │ │ │ └── site.js │ │ └── css │ │ │ └── site.css │ ├── Dockerfile │ ├── AppRoles.WebApp.csproj │ ├── Properties │ │ └── launchSettings.json │ ├── Program.cs │ ├── appsettings.json │ ├── SameSiteCookieExtensions.cs │ ├── Controllers │ │ └── AppRolesController.cs │ └── Startup.cs ├── .vscode │ ├── extensions.json │ ├── settings.json │ ├── tasks.json │ └── launch.json ├── LICENSE ├── .devcontainer │ ├── Dockerfile │ ├── library-scripts │ │ └── azcli-debian.sh │ └── devcontainer.json ├── azuredeploy.json └── README.md ├── InvitationCodeDelegatedUserManagement ├── DelegatedUserManagement.WebApp │ ├── Pages │ │ ├── _ViewStart.cshtml │ │ ├── _ViewImports.cshtml │ │ ├── Identity.cshtml.cs │ │ ├── Index.cshtml.cs │ │ ├── Identity.cshtml │ │ ├── Index.cshtml │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── Shared │ │ │ ├── _LoginPartial.cshtml │ │ │ └── _Layout.cshtml │ │ ├── User.cshtml │ │ ├── User.cshtml.cs │ │ ├── UserInvitation.cshtml │ │ └── UserInvitation.cshtml.cs │ ├── appsettings.Development.json │ ├── wwwroot │ │ ├── js │ │ │ └── site.js │ │ └── css │ │ │ └── site.css │ ├── Models │ │ ├── UserInvitationRequest.cs │ │ ├── User.cs │ │ └── UserInvitation.cs │ ├── Dockerfile │ ├── Program.cs │ ├── Services │ │ ├── IUserInvitationRepository.cs │ │ ├── FileStorageUserInvitationRepository.cs │ │ └── B2cGraphService.cs │ ├── appsettings.json │ ├── DelegatedUserManagement.WebApp.csproj │ ├── Constants.cs │ ├── Properties │ │ └── launchSettings.json │ ├── SameSiteCookieExtensions.cs │ ├── Startup.cs │ └── Controllers │ │ └── UserInvitationController.cs ├── .vscode │ ├── settings.json │ ├── launch.json │ └── tasks.json ├── LICENSE ├── .devcontainer │ ├── Dockerfile │ ├── library-scripts │ │ └── azcli-debian.sh │ └── devcontainer.json ├── IdentitySamplesB2C-DelegatedUserManagement.sln ├── azuredeploy.json ├── README.md └── PageLayouts │ └── selfAsserted.html ├── README.md └── .gitignore /Authorization-AppRoles/AppRoles.WebApp/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Authorization-AppRoles/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "azureadb2ctools.aadb2c", 4 | "ms-vscode.azure-account" 5 | ] 6 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appService.preDeployTask": "publish", 3 | "appService.deploySubpath": "AppRoles.WebApp\\bin\\publish" 4 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appService.preDeployTask": "publish-release", 3 | "appService.deploySubpath": "DelegatedUserManagement.WebApp/bin/publish" 4 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using AppRoles.WebApp 2 | @using AppRoles.WebApp.Services 3 | @namespace AppRoles.WebApp.Pages 4 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 5 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Services/AppRolesOptions.cs: -------------------------------------------------------------------------------- 1 | namespace AppRoles.WebApp.Services 2 | { 3 | public class AppRolesOptions 4 | { 5 | public string UserAttributeName { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using DelegatedUserManagement.WebApp 2 | @namespace DelegatedUserManagement.WebApp.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your Javascript code. 5 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/AdminOnly.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AdminOnlyModel 3 | @{ 4 | ViewData["Title"] = "Admin Page"; 5 | } 6 |

@ViewBag.Title

7 |

8 | If you can see this page, then you have the @AdminOnlyModel.AdminRoleName role. 9 |

10 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | // for details on configuring this project to bundle and minify static web assets. 3 | 4 | // Write your Javascript code. 5 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Services/IAppRolesProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace AppRoles.WebApp.Services 5 | { 6 | public interface IAppRolesProvider 7 | { 8 | Task> GetAppRolesAsync(string userId, string appId); 9 | } 10 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Models/UserInvitationRequest.cs: -------------------------------------------------------------------------------- 1 | namespace DelegatedUserManagement.WebApp 2 | { 3 | public class UserInvitationRequest 4 | { 5 | public string CompanyId { get; set; } 6 | public string DelegatedUserManagementRole { get; set; } 7 | public int ValidHours { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Services/AzureADAppRolesProviderOptions.cs: -------------------------------------------------------------------------------- 1 | namespace AppRoles.WebApp.Services 2 | { 3 | public class AzureADAppRolesProviderOptions 4 | { 5 | public string AzureADAppRolesProviderClientId { get; set; } 6 | public string AzureADAppRolesProviderClientSecret { get; set; } 7 | public string Domain { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Identity.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace DelegatedUserManagement.WebApp.Pages 5 | { 6 | [Authorize] 7 | public class IdentityModel : PageModel 8 | { 9 | public void OnGet() 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Models/User.cs: -------------------------------------------------------------------------------- 1 | namespace DelegatedUserManagement.WebApp 2 | { 3 | public class User 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string InvitationCode { get; set; } 8 | public string CompanyId { get; set; } 9 | public string DelegatedUserManagementRole { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 3 | ARG app_version=1.0.0.0 4 | ARG source_version=local 5 | WORKDIR /build 6 | COPY . . 7 | RUN dotnet restore 8 | RUN dotnet publish -c Release -o /app /p:Version=${app_version} /p:SourceRevisionId=${source_version} 9 | # Stage 2 10 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS final 11 | ENV ASPNETCORE_URLS=http://+:80 12 | EXPOSE 80 13 | WORKDIR /app 14 | COPY --from=build /app . 15 | ENTRYPOINT ["dotnet", "AppRoles.WebApp.dll"] 16 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Models/UserInvitation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DelegatedUserManagement.WebApp 4 | { 5 | public class UserInvitation 6 | { 7 | public string InvitationCode { get; set; } 8 | public string CompanyId { get; set; } 9 | public string DelegatedUserManagementRole { get; set; } 10 | public string CreatedBy { get; set; } 11 | public DateTimeOffset CreatedTime { get; set; } 12 | public DateTimeOffset ExpiresTime { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace DelegatedUserManagement.WebApp.Pages 5 | { 6 | public class IndexModel : PageModel 7 | { 8 | private readonly ILogger _logger; 9 | 10 | public IndexModel(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | public void OnGet() 16 | { 17 | 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 2 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 3 | ARG app_version=1.0.0.0 4 | ARG source_version=local 5 | WORKDIR /build 6 | COPY . . 7 | RUN dotnet restore 8 | RUN dotnet publish -c Release -o /app /p:Version=${app_version} /p:SourceRevisionId=${source_version} 9 | # Stage 2 10 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS final 11 | ENV ASPNETCORE_URLS=http://+:80 12 | EXPOSE 80 13 | WORKDIR /app 14 | COPY --from=build /app . 15 | ENTRYPOINT ["dotnet", "DelegatedUserManagement.WebApp.dll"] 16 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Identity.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityModel 3 | @{ 4 | ViewData["Title"] = "Identity"; 5 | } 6 |

Claims

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @foreach (var claim in this.User.Claims) 16 | { 17 | 18 | 19 | 20 | 21 | } 22 | 23 |
TypeValue
@claim.Type@claim.Value
-------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace AppRoles.WebApp.Pages 10 | { 11 | public class IndexModel : PageModel 12 | { 13 | private readonly ILogger _logger; 14 | 15 | public IndexModel(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | public void OnGet() 21 | { 22 | 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace DelegatedUserManagement.WebApp 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Services/IUserInvitationRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace DelegatedUserManagement.WebApp 5 | { 6 | public interface IUserInvitationRepository 7 | { 8 | Task CreateUserInvitationAsync(UserInvitation userInvitation); 9 | Task GetPendingUserInvitationAsync(string invitationCode); 10 | Task RedeemUserInvitationAsync(string invitationCode); 11 | Task DeletePendingUserInvitationAsync(string invitationCode); 12 | Task> GetPendingUserInvitationsAsync(string companyId = null); 13 | } 14 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/AppRoles.WebApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | aspnet-AppRoles.WebApp-22159A9C-218E-40F4-9CD1-EFDD2831E8A6 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Identity.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | 4 | namespace AppRoles.WebApp.Pages 5 | { 6 | [Authorize] 7 | public class IdentityModel : PageModel 8 | { 9 | public string CheckRoleResult { get; set; } 10 | 11 | public void OnGet() 12 | { 13 | } 14 | 15 | public void OnPost(string roleName) 16 | { 17 | if (!string.IsNullOrEmpty(roleName)) 18 | { 19 | this.CheckRoleResult = this.User.IsInRole(roleName) ? $"You have the \"{roleName}\" role." : $"You do not have the \"{roleName}\" role."; 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/AdminOnly.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace AppRoles.WebApp.Pages 6 | { 7 | // Only allow users with the "Admin" role to see this page. 8 | [Authorize(Roles = AdminRoleName)] 9 | public class AdminOnlyModel : PageModel 10 | { 11 | public const string AdminRoleName = "Admin"; 12 | 13 | private readonly ILogger _logger; 14 | 15 | public AdminOnlyModel(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | public void OnGet() 21 | { 22 | 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:62080", 7 | "sslPort": 44368 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "AppRoles.WebApp": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AzureAdB2C": { 3 | "Instance": "https://.b2clogin.com/tfp/", 4 | "ClientId": "", 5 | "ClientSecret": "", 6 | "CallbackPath": "/signin-oidc", 7 | "Domain": ".onmicrosoft.com", 8 | "SignUpSignInPolicyId": "", 9 | "ResetPasswordPolicyId": "", 10 | "EditProfilePolicyId": "", 11 | "B2cExtensionsAppClientId": "" 12 | }, 13 | "App": { 14 | "UserInvitationsBasePath": null 15 | }, 16 | "Logging": { 17 | "LogLevel": { 18 | "Default": "Information", 19 | "Microsoft": "Warning", 20 | "Microsoft.Hosting.Lifetime": "Information" 21 | } 22 | }, 23 | "AllowedHosts": "*" 24 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AppRoles.WebApp 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AzureAdB2C": { 3 | "Domain": ".onmicrosoft.com", 4 | "AzureADAppRolesProviderClientId": "", 5 | "AzureADAppRolesProviderClientSecret": "", 6 | "Instance": "https://.b2clogin.com/tfp/", 7 | "ClientId": "", 8 | "CallbackPath": "/signin-oidc", 9 | "SignUpSignInPolicyId": "", 10 | "ResetPasswordPolicyId": "", 11 | "EditProfilePolicyId": "" 12 | }, 13 | "AppRoles": { 14 | "UserAttributeName": "extension_AppRoles" 15 | }, 16 | "Logging": { 17 | "LogLevel": { 18 | "Default": "Information", 19 | "Microsoft": "Warning", 20 | "Microsoft.Hosting.Lifetime": "Information" 21 | } 22 | }, 23 | "AllowedHosts": "*" 24 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/DelegatedUserManagement.WebApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | aspnet-DelegatedUserManagement.WebApp-87089583-9093-4E4B-A396-58B70C40AF4D 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Welcome"; 5 | } 6 |

@ViewBag.Title

7 |

8 | This sample demonstrates delegated user management using 9 | Azure Active Directory B2C 10 |

11 |

12 | 13 | IMPORTANT NOTE: The code in this repository is not production-ready. It serves only to demonstrate the 14 | main points via minimal working code, and contains no exception handling or other special cases. Refer to the 15 | official documentation and samples for more information. Similarly, by design, it does not implement any 16 | caching or data persistence (e.g. to a database) to minimize the concepts and technologies being used. 17 | 18 |

-------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace DelegatedUserManagement.WebApp 2 | { 3 | public static class Constants 4 | { 5 | public static class ClaimTypes 6 | { 7 | public const string ObjectId = "oid"; 8 | } 9 | 10 | public static class UserAttributes 11 | { 12 | public const string DelegatedUserManagementRole = nameof(DelegatedUserManagementRole); 13 | public const string InvitationCode = nameof(InvitationCode); 14 | public const string CompanyId = nameof(CompanyId); 15 | } 16 | 17 | public static class DelegatedUserManagementRoles 18 | { 19 | public const string GlobalAdmin = nameof(GlobalAdmin); 20 | public const string CompanyAdmin = nameof(CompanyAdmin); 21 | public const string CompanyUser = nameof(CompanyUser); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AppRoles.WebApp.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | public string RequestId { get; set; } 16 | 17 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 18 | 19 | private readonly ILogger _logger; 20 | 21 | public ErrorModel(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace DelegatedUserManagement.WebApp.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | public string RequestId { get; set; } 16 | 17 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 18 | 19 | private readonly ILogger _logger; 20 | 21 | public ErrorModel(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:48986", 7 | "sslPort": 44395 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development", 16 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 17 | } 18 | }, 19 | "DelegatedUserManagement.WebApp": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development", 25 | "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Welcome"; 5 | } 6 |

@ViewBag.Title

7 |

8 | This sample demonstrates authorization in 9 | Azure Active Directory B2C 10 | using API Connectors 11 | and Azure AD App Roles. 12 |

13 |

14 | 15 | IMPORTANT NOTE: The code in this repository is not production-ready. It serves only to demonstrate the 16 | main points via minimal working code, and contains no exception handling or other special cases. Refer to the 17 | official documentation and samples for more information. Similarly, by design, it does not implement any 18 | caching or data persistence (e.g. to a database) to minimize the concepts and technologies being used. 19 | 20 |

21 | -------------------------------------------------------------------------------- /Authorization-AppRoles/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jelle Druyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jelle Druyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Configuration 2 | @inject IConfiguration Configuration 3 | 4 | 32 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.Extensions.Configuration 2 | @inject IConfiguration Configuration 3 | 4 | 32 | -------------------------------------------------------------------------------- /Authorization-AppRoles/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/dotnet/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] .NET version 4 | ARG VARIANT="7.0-bullseye-slim" 5 | FROM mcr.microsoft.com/devcontainers/dotnet:0-${VARIANT} 6 | 7 | # [Option] Install Node.js 8 | ARG INSTALL_NODE="true" 9 | ARG NODE_VERSION="lts/*" 10 | RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 11 | 12 | # [Option] Install Azure CLI 13 | ARG INSTALL_AZURE_CLI="false" 14 | COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ 15 | RUN if [ "$INSTALL_AZURE_CLI" = "true" ]; then bash /tmp/library-scripts/azcli-debian.sh; fi \ 16 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 17 | 18 | # [Optional] Uncomment this section to install additional OS packages. 19 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 20 | # && apt-get -y install --no-install-recommends 21 | 22 | # [Optional] Uncomment this line to install global node packages. 23 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/dotnet/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] .NET version 4 | ARG VARIANT="7.0-bullseye-slim" 5 | FROM mcr.microsoft.com/devcontainers/dotnet:0-${VARIANT} 6 | 7 | # [Option] Install Node.js 8 | ARG INSTALL_NODE="true" 9 | ARG NODE_VERSION="lts/*" 10 | RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 11 | 12 | # [Option] Install Azure CLI 13 | ARG INSTALL_AZURE_CLI="false" 14 | COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ 15 | RUN if [ "$INSTALL_AZURE_CLI" = "true" ]; then bash /tmp/library-scripts/azcli-debian.sh; fi \ 16 | && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 17 | 18 | # [Optional] Uncomment this section to install additional OS packages. 19 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 20 | # && apt-get -y install --no-install-recommends 21 | 22 | # [Optional] Uncomment this line to install global node packages. 23 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Services/StringSplitClaimTransformation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication; 6 | 7 | namespace AppRoles.WebApp.Services 8 | { 9 | public class StringSplitClaimsTransformation : IClaimsTransformation 10 | { 11 | private readonly string claimType; 12 | 13 | public StringSplitClaimsTransformation(string claimType) 14 | { 15 | this.claimType = claimType; 16 | } 17 | 18 | public Task TransformAsync(ClaimsPrincipal principal) 19 | { 20 | // Find all claims of the requested claim type, split their values by spaces 21 | // and then take the ones that aren't yet on the principal individually. 22 | var claims = principal.FindAll(this.claimType) 23 | .SelectMany(c => c.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries)) 24 | .Where(s => !principal.HasClaim(this.claimType, s)).ToList(); 25 | 26 | // Add all new claims to the principal's identity. 27 | ((ClaimsIdentity)principal.Identity).AddClaims(claims.Select(s => new Claim(this.claimType, s))); 28 | return Task.FromResult(principal); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/AppRoles.WebApp/AppRoles.WebApp.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile", 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "label": "publish", 22 | "command": "dotnet", 23 | "type": "process", 24 | "args": [ 25 | "publish", 26 | "${workspaceFolder}/AppRoles.WebApp/AppRoles.WebApp.csproj", 27 | "/property:GenerateFullPaths=true", 28 | "/consoleloggerparameters:NoSummary" 29 | ], 30 | "problemMatcher": "$msCompile" 31 | }, 32 | { 33 | "label": "watch", 34 | "command": "dotnet", 35 | "type": "process", 36 | "args": [ 37 | "watch", 38 | "run", 39 | "${workspaceFolder}/AppRoles.WebApp/AppRoles.WebApp.csproj", 40 | "/property:GenerateFullPaths=true", 41 | "/consoleloggerparameters:NoSummary" 42 | ], 43 | "problemMatcher": "$msCompile" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/AppRoles.WebApp/bin/Debug/net7.0/AppRoles.WebApp.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/AppRoles.WebApp", 16 | "console": "integratedTerminal", 17 | "stopAtEntry": false, 18 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 19 | "serverReadyAction": { 20 | "action": "openExternally", 21 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 22 | }, 23 | "env": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "sourceFileMap": { 27 | "/Views": "${workspaceFolder}/AppRoles.WebApp/Views" 28 | } 29 | }, 30 | { 31 | "name": ".NET Core Attach", 32 | "type": "coreclr", 33 | "request": "attach" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Provide sufficient contrast against white background */ 11 | a { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 22 | color: #fff; 23 | background-color: #1b6ec2; 24 | border-color: #1861ac; 25 | } 26 | 27 | /* Sticky footer styles 28 | -------------------------------------------------- */ 29 | html { 30 | font-size: 14px; 31 | } 32 | @media (min-width: 768px) { 33 | html { 34 | font-size: 16px; 35 | } 36 | } 37 | 38 | .border-top { 39 | border-top: 1px solid #e5e5e5; 40 | } 41 | .border-bottom { 42 | border-bottom: 1px solid #e5e5e5; 43 | } 44 | 45 | .box-shadow { 46 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 47 | } 48 | 49 | button.accept-policy { 50 | font-size: 1rem; 51 | line-height: inherit; 52 | } 53 | 54 | /* Sticky footer styles 55 | -------------------------------------------------- */ 56 | html { 57 | position: relative; 58 | min-height: 100%; 59 | } 60 | 61 | body { 62 | /* Margin bottom by footer height */ 63 | margin-bottom: 60px; 64 | } 65 | .footer { 66 | position: absolute; 67 | bottom: 0; 68 | width: 100%; 69 | white-space: nowrap; 70 | line-height: 60px; /* Vertically center the text there */ 71 | } 72 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/DelegatedUserManagement.WebApp/bin/Debug/net7.0/DelegatedUserManagement.WebApp.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/DelegatedUserManagement.WebApp", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/DelegatedUserManagement.WebApp/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/.devcontainer/library-scripts/azcli-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/azcli.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./azcli-debian.sh 11 | 12 | set -e 13 | 14 | if [ "$(id -u)" -ne 0 ]; then 15 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 16 | exit 1 17 | fi 18 | 19 | export DEBIAN_FRONTEND=noninteractive 20 | 21 | # Install curl, apt-transport-https, lsb-release, or gpg if missing 22 | if ! dpkg -s apt-transport-https curl ca-certificates lsb-release > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then 23 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 24 | apt-get update 25 | fi 26 | apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates lsb-release gnupg2 27 | fi 28 | 29 | # Install the Azure CLI 30 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list 31 | curl -sL https://packages.microsoft.com/keys/microsoft.asc | (OUT=$(apt-key add - 2>&1) || echo $OUT) 32 | apt-get update 33 | apt-get install -y azure-cli 34 | echo "Done!" -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification 2 | for details on configuring this project to bundle and minify static web assets. */ 3 | 4 | a.navbar-brand { 5 | white-space: normal; 6 | text-align: center; 7 | word-break: break-all; 8 | } 9 | 10 | /* Provide sufficient contrast against white background */ 11 | a { 12 | color: #0366d6; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link { 22 | color: #fff; 23 | background-color: #1b6ec2; 24 | border-color: #1861ac; 25 | } 26 | 27 | /* Sticky footer styles 28 | -------------------------------------------------- */ 29 | html { 30 | font-size: 14px; 31 | } 32 | @media (min-width: 768px) { 33 | html { 34 | font-size: 16px; 35 | } 36 | } 37 | 38 | .border-top { 39 | border-top: 1px solid #e5e5e5; 40 | } 41 | .border-bottom { 42 | border-bottom: 1px solid #e5e5e5; 43 | } 44 | 45 | .box-shadow { 46 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); 47 | } 48 | 49 | button.accept-policy { 50 | font-size: 1rem; 51 | line-height: inherit; 52 | } 53 | 54 | /* Sticky footer styles 55 | -------------------------------------------------- */ 56 | html { 57 | position: relative; 58 | min-height: 100%; 59 | } 60 | 61 | body { 62 | /* Margin bottom by footer height */ 63 | margin-bottom: 60px; 64 | } 65 | .footer { 66 | position: absolute; 67 | bottom: 0; 68 | width: 100%; 69 | white-space: nowrap; 70 | line-height: 60px; /* Vertically center the text there */ 71 | } 72 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/.devcontainer/library-scripts/azcli-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/azcli.md 8 | # Maintainer: The VS Code and Codespaces Teams 9 | # 10 | # Syntax: ./azcli-debian.sh 11 | 12 | set -e 13 | 14 | if [ "$(id -u)" -ne 0 ]; then 15 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 16 | exit 1 17 | fi 18 | 19 | export DEBIAN_FRONTEND=noninteractive 20 | 21 | # Install curl, apt-transport-https, lsb-release, or gpg if missing 22 | if ! dpkg -s apt-transport-https curl ca-certificates lsb-release > /dev/null 2>&1 || ! type gpg > /dev/null 2>&1; then 23 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 24 | apt-get update 25 | fi 26 | apt-get -y install --no-install-recommends apt-transport-https curl ca-certificates lsb-release gnupg2 27 | fi 28 | 29 | # Install the Azure CLI 30 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" > /etc/apt/sources.list.d/azure-cli.list 31 | curl -sL https://packages.microsoft.com/keys/microsoft.asc | (OUT=$(apt-key add - 2>&1) || echo $OUT) 32 | apt-get update 33 | apt-get install -y azure-cli 34 | echo "Done!" -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | "/property:GenerateFullPaths=true", 13 | "/consoleloggerparameters:NoSummary" 14 | ], 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | }, 19 | "presentation": { 20 | "reveal": "silent" 21 | }, 22 | "problemMatcher": "$msCompile" 23 | }, 24 | { 25 | "label": "clean", 26 | "command": "dotnet", 27 | "type": "process", 28 | "args": [ 29 | "clean", 30 | "${workspaceFolder}/DelegatedUserManagement.WebApp", 31 | "/property:GenerateFullPaths=true", 32 | "/consoleloggerparameters:NoSummary" 33 | ], 34 | "problemMatcher": "$msCompile" 35 | }, 36 | { 37 | "label": "publish-release", 38 | "command": "dotnet", 39 | "type": "process", 40 | "args": [ 41 | "publish", 42 | "${workspaceFolder}/DelegatedUserManagement.WebApp", 43 | "--configuration", 44 | "Release", 45 | "/property:GenerateFullPaths=true", 46 | "/consoleloggerparameters:NoSummary" 47 | ], 48 | "problemMatcher": "$msCompile", 49 | "dependsOn": "clean" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/IdentitySamplesB2C-DelegatedUserManagement.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DelegatedUserManagement.WebApp", "DelegatedUserManagement.WebApp\DelegatedUserManagement.WebApp.csproj", "{34D0D033-B71C-464E-AF42-223622E36687}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {34D0D033-B71C-464E-AF42-223622E36687}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {34D0D033-B71C-464E-AF42-223622E36687}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {34D0D033-B71C-464E-AF42-223622E36687}.Debug|x64.ActiveCfg = Debug|Any CPU 24 | {34D0D033-B71C-464E-AF42-223622E36687}.Debug|x64.Build.0 = Debug|Any CPU 25 | {34D0D033-B71C-464E-AF42-223622E36687}.Debug|x86.ActiveCfg = Debug|Any CPU 26 | {34D0D033-B71C-464E-AF42-223622E36687}.Debug|x86.Build.0 = Debug|Any CPU 27 | {34D0D033-B71C-464E-AF42-223622E36687}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {34D0D033-B71C-464E-AF42-223622E36687}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {34D0D033-B71C-464E-AF42-223622E36687}.Release|x64.ActiveCfg = Release|Any CPU 30 | {34D0D033-B71C-464E-AF42-223622E36687}.Release|x64.Build.0 = Release|Any CPU 31 | {34D0D033-B71C-464E-AF42-223622E36687}.Release|x86.ActiveCfg = Release|Any CPU 32 | {34D0D033-B71C-464E-AF42-223622E36687}.Release|x86.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Identity.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IdentityModel 3 | @inject AppRolesOptions AppRolesOptions 4 | @{ 5 | ViewData["Title"] = "Identity"; 6 | } 7 |

App Roles

8 |
9 | These are all the app roles you have for this application (based on the @AppRolesOptions.UserAttributeName claims). 10 |
11 |
12 | Note that the @AppRolesOptions.UserAttributeName claim can occur multiple times even though Azure AD B2C emits 13 | app roles as a single (space-separated) value; this is because the application splits it into multiple claim values to make 14 | it easier to consume the app roles. 15 |
16 |
    17 | @foreach (var claim in this.User.Claims.Where(c => c.Type == AppRolesOptions.UserAttributeName)) 18 | { 19 |
  • @claim.Value
  • 20 | } 21 |
22 | 23 |

App Role Check

24 |
25 |
26 | 27 | 28 |
29 | 30 |
31 | @if (!string.IsNullOrWhiteSpace(Model.CheckRoleResult)) 32 | { 33 |
34 | @Model.CheckRoleResult 35 |
36 | } 37 | 38 |

Claims

39 |
This is the full list of claims you have.
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | @foreach (var claim in this.User.Claims) 49 | { 50 | 51 | 52 | 53 | 54 | } 55 | 56 |
TypeValue
@claim.Type@claim.Value
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # api-connector-samples 2 | This is a community maintained collection of samples for scenarios enabled by API connectors for Azure AD B2C 'built-in' user flows. 3 | 4 | ## Overview of API connectors feature 5 | 6 | As a developer or IT administrator, you can use API connectors to integrate your sign-up and sign-in user flows with web APIs to customize the user experience. For example, with API connectors, you can: 7 | 8 | - **Validate user input data**. Validate against malformed or invalid user data. For example, you can validate user-provided data against existing data in an external data store or list of permitted values. If invalid, you can ask a user to provide valid data or block the user from continuing the sign-up flow. 9 | - **Integrate with a custom approval workflow**. Connect to a custom approval system for managing and limiting account creation. 10 | - **Overwrite user attributes**. Reformat or assign a value to an attribute collected from the user. For example, if a user enters the first name in all lowercase or all uppercase letters, you can format the name with only the first letter capitalized. 11 | - **Perform identity verification**. Use an identity verification service to add an extra level of security to account creation decisions. 12 | - **Run custom business logic**. You can trigger downstream events in your cloud systems to send push notifications, update corporate databases, manage permissions, audit databases, and perform other custom actions. 13 | - **Augment tokens**. Enrich tokens for your sign-in and sign-up user flows with attributes from legacy identity systems, custom data stores, and other cloud services. 14 | 15 | ## Microsoft documentation 16 | 17 | - [Overview](https://docs.microsoft.com/azure/active-directory-b2c/api-connectors-overview) 18 | - [Add an API connector](https://docs.microsoft.com/azure/active-directory-b2c/add-api-connector) 19 | - [Official quickstarts and samples](https://docs.microsoft.com/azure/active-directory-b2c/code-samples#api-connectors) 20 | -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Authorization using App Roles 7 | 8 | 9 | 10 | 11 |
12 | 31 |
32 |
33 |
34 | @RenderBody() 35 |
36 |
37 | 38 | 39 | 40 | 41 | @RenderSection("Scripts", required: false) 42 | 43 | 44 | -------------------------------------------------------------------------------- /Authorization-AppRoles/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/dotnet 3 | { 4 | "name": "C# (.NET)", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update 'VARIANT' to pick a .NET Core version: 3.1, 6.0, 7.0 9 | // Append -bullseye or -focal to pin to an OS version. 10 | "VARIANT": "7.0", 11 | // Options 12 | "INSTALL_NODE": "false", 13 | "NODE_VERSION": "lts/*", 14 | "INSTALL_AZURE_CLI": "true" 15 | } 16 | }, 17 | 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": {}, 20 | 21 | // Add the IDs of extensions you want installed when the container is created. 22 | "extensions": [ 23 | "ms-dotnettools.csharp", 24 | "azureadb2ctools.aadb2c", 25 | "ms-vscode.azure-account", 26 | "humao.rest-client" 27 | ], 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | "forwardPorts": [5000, 5001], 31 | 32 | // [Optional] To reuse of your local HTTPS dev cert: 33 | // 34 | // 1. Export it locally using this command: 35 | // * Windows PowerShell: 36 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 37 | // * macOS/Linux terminal: 38 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 39 | // 40 | // 2. Uncomment these 'remoteEnv' lines: 41 | // "remoteEnv": { 42 | // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", 43 | // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", 44 | // }, 45 | // 46 | // 3. Do one of the following depending on your scenario: 47 | // * When using GitHub Codespaces and/or Remote - Containers: 48 | // 1. Start the container 49 | // 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer 50 | // 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https" 51 | // 52 | // * If only using Remote - Containers with a local container, uncomment this line instead: 53 | // "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" ], 54 | 55 | // Use 'postCreateCommand' to run commands after the container is created. 56 | "postCreateCommand": "dotnet restore", 57 | 58 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 59 | "remoteUser": "vscode" 60 | } 61 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/dotnet 3 | { 4 | "name": "C# (.NET)", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update 'VARIANT' to pick a .NET Core version: 3.1, 6.0, 7.0 9 | // Append -bullseye or -focal to pin to an OS version. 10 | "VARIANT": "7.0", 11 | // Options 12 | "INSTALL_NODE": "false", 13 | "NODE_VERSION": "lts/*", 14 | "INSTALL_AZURE_CLI": "true" 15 | } 16 | }, 17 | 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": {}, 20 | 21 | // Add the IDs of extensions you want installed when the container is created. 22 | "extensions": [ 23 | "ms-dotnettools.csharp", 24 | "azureadb2ctools.aadb2c", 25 | "ms-vscode.azure-account", 26 | "humao.rest-client" 27 | ], 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | "forwardPorts": [5000, 5001], 31 | 32 | // [Optional] To reuse of your local HTTPS dev cert: 33 | // 34 | // 1. Export it locally using this command: 35 | // * Windows PowerShell: 36 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "$env:USERPROFILE/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 37 | // * macOS/Linux terminal: 38 | // dotnet dev-certs https --trust; dotnet dev-certs https -ep "${HOME}/.aspnet/https/aspnetapp.pfx" -p "SecurePwdGoesHere" 39 | // 40 | // 2. Uncomment these 'remoteEnv' lines: 41 | // "remoteEnv": { 42 | // "ASPNETCORE_Kestrel__Certificates__Default__Password": "SecurePwdGoesHere", 43 | // "ASPNETCORE_Kestrel__Certificates__Default__Path": "/home/vscode/.aspnet/https/aspnetapp.pfx", 44 | // }, 45 | // 46 | // 3. Do one of the following depending on your scenario: 47 | // * When using GitHub Codespaces and/or Remote - Containers: 48 | // 1. Start the container 49 | // 2. Drag ~/.aspnet/https/aspnetapp.pfx into the root of the file explorer 50 | // 3. Open a terminal in VS Code and run "mkdir -p /home/vscode/.aspnet/https && mv aspnetapp.pfx /home/vscode/.aspnet/https" 51 | // 52 | // * If only using Remote - Containers with a local container, uncomment this line instead: 53 | // "mounts": [ "source=${env:HOME}${env:USERPROFILE}/.aspnet/https,target=/home/vscode/.aspnet/https,type=bind" ], 54 | 55 | // Use 'postCreateCommand' to run commands after the container is created. 56 | "postCreateCommand": "dotnet restore", 57 | 58 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 59 | "remoteUser": "vscode" 60 | } 61 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Delegated User Management 7 | 8 | 9 | 10 | 11 |
12 | 34 |
35 |
36 |
37 | @RenderBody() 38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | @RenderSection("Scripts", required: false) 47 | 48 | 49 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/User.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model UserModel 3 | @{ 4 | ViewData["Title"] = "Users"; 5 | } 6 | @if (!Model.CanManageUsers) 7 | { 8 |
You don't have permissions to manage users.
9 | } 10 | else 11 | { 12 |

Manage Users

13 |
14 | Note that making any changes to user information will write them immediately to the directory 15 | through the Graph API; but to see the new values reflected in the token (and as a consequence 16 | in the application), the user will have to sign out and back in. 17 |
18 | 19 | 20 | 21 | 22 | @if (Model.CanSelectCompany) 23 | { 24 | 25 | } 26 | 27 | 28 | 29 | 30 | 31 | 32 | @foreach (var user in Model.Users) 33 | { 34 | 35 | 42 | @if (Model.CanSelectCompany) 43 | { 44 | 47 | } 48 | 58 | 61 | 64 | 65 | } 66 | 67 |
CompanyRoleNameInvitation Code
36 |
37 | 38 | 39 | 40 |
41 |
45 | 46 | 49 | 57 | 59 | 60 | 62 |
@user.InvitationCode
63 |
68 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Services/FileStorageUserInvitationRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | 7 | namespace DelegatedUserManagement.WebApp 8 | { 9 | // Stores user invitations in files; in real production scenarios you would likely use a database for this. 10 | public class FileStorageUserInvitationRepository : IUserInvitationRepository 11 | { 12 | private readonly string basePath; 13 | 14 | public FileStorageUserInvitationRepository(string basePath) 15 | { 16 | if (string.IsNullOrWhiteSpace(basePath)) 17 | { 18 | throw new ArgumentNullException(nameof(basePath)); 19 | } 20 | 21 | // Ensure that the directory exists. 22 | this.basePath = basePath; 23 | Directory.CreateDirectory(this.basePath); 24 | } 25 | 26 | public async Task CreateUserInvitationAsync(UserInvitation userInvitation) 27 | { 28 | var contents = JsonSerializer.Serialize(userInvitation); 29 | await File.WriteAllTextAsync(GetUserInvitationFileName(userInvitation.InvitationCode), contents); 30 | } 31 | 32 | public Task GetPendingUserInvitationAsync(string invitationCode) 33 | { 34 | return GetUserInvitationAsync(GetUserInvitationFileName(invitationCode)); 35 | } 36 | 37 | public Task RedeemUserInvitationAsync(string invitationCode) 38 | { 39 | File.Delete(GetUserInvitationFileName(invitationCode)); 40 | return Task.CompletedTask; 41 | } 42 | 43 | public Task DeletePendingUserInvitationAsync(string invitationCode) 44 | { 45 | File.Delete(GetUserInvitationFileName(invitationCode)); 46 | return Task.CompletedTask; 47 | } 48 | 49 | public async Task> GetPendingUserInvitationsAsync(string companyId = null) 50 | { 51 | var userInvitations = new List(); 52 | foreach (var fileName in Directory.EnumerateFiles(this.basePath)) 53 | { 54 | var userInvitation = await GetUserInvitationAsync(fileName); 55 | if (string.IsNullOrWhiteSpace(companyId) || string.Equals(userInvitation.CompanyId, companyId, StringComparison.InvariantCultureIgnoreCase)) 56 | { 57 | userInvitations.Add(userInvitation); 58 | } 59 | } 60 | return userInvitations; 61 | } 62 | 63 | private async Task GetUserInvitationAsync(string fileName) 64 | { 65 | if (!File.Exists(fileName)) 66 | { 67 | return null; 68 | } 69 | var contents = await File.ReadAllTextAsync(fileName); 70 | return JsonSerializer.Deserialize(contents); 71 | } 72 | 73 | private string GetUserInvitationFileName(string invitationCode) 74 | { 75 | if (string.IsNullOrWhiteSpace(invitationCode)) 76 | { 77 | throw new ArgumentNullException(nameof(invitationCode)); 78 | } 79 | return Path.Combine(this.basePath, invitationCode + ".json"); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/SameSiteCookieExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace AppRoles.WebApp 6 | { 7 | // Allows the breaking change for SameSite cookies to be handled more easily. 8 | // See https://docs.microsoft.com/en-us/aspnet/samesite/system-web-samesite and 9 | // https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. 10 | public static class SameSiteCookieExtensions 11 | { 12 | public static void ConfigureSameSiteCookiePolicy(this IServiceCollection services) 13 | { 14 | services.Configure(options => 15 | { 16 | options.MinimumSameSitePolicy = SameSiteMode.Unspecified; 17 | options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); 18 | options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); 19 | }); 20 | } 21 | 22 | // Must be called before "UseAuthentication" or anything else that writes cookies. 23 | public static void ApplySameSiteCookiePolicy(this IApplicationBuilder app) 24 | { 25 | app.UseCookiePolicy(); 26 | } 27 | 28 | private static void CheckSameSite(HttpContext httpContext, CookieOptions options) 29 | { 30 | if (options.SameSite == SameSiteMode.None) 31 | { 32 | var userAgent = httpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent].ToString(); 33 | if (DisallowsSameSiteNone(userAgent)) 34 | { 35 | options.SameSite = SameSiteMode.Unspecified; 36 | } 37 | } 38 | } 39 | 40 | private static bool DisallowsSameSiteNone(string userAgent) 41 | { 42 | // Cover all iOS based browsers here. This includes: 43 | // - Safari on iOS 12 for iPhone, iPod Touch, iPad 44 | // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad 45 | // - Chrome on iOS 12 for iPhone, iPod Touch, iPad 46 | // All of which are broken by SameSite=None, because they use the iOS 47 | // networking stack. 48 | if (userAgent.Contains("CPU iPhone OS 12") || 49 | userAgent.Contains("iPad; CPU OS 12")) 50 | { 51 | return true; 52 | } 53 | 54 | // Cover Mac OS X based browsers that use the Mac OS networking stack. 55 | // This includes: 56 | // - Safari on Mac OS X. 57 | // This does not include: 58 | // - Chrome on Mac OS X 59 | // Because they do not use the Mac OS networking stack. 60 | if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && 61 | userAgent.Contains("Version/") && userAgent.Contains("Safari")) 62 | { 63 | return true; 64 | } 65 | 66 | // Cover Chrome 50-69, because some versions are broken by SameSite=None, 67 | // and none in this range require it. 68 | // Note: this covers some pre-Chromium Edge versions, 69 | // but pre-Chromium Edge does not require SameSite=None. 70 | if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) 71 | { 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/SameSiteCookieExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace DelegatedUserManagement.WebApp 6 | { 7 | // Allows the breaking change for SameSite cookies to be handled more easily. 8 | // See https://docs.microsoft.com/en-us/aspnet/samesite/system-web-samesite and 9 | // https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/. 10 | public static class SameSiteCookieExtensions 11 | { 12 | public static void ConfigureSameSiteCookiePolicy(this IServiceCollection services) 13 | { 14 | services.Configure(options => 15 | { 16 | options.MinimumSameSitePolicy = SameSiteMode.Unspecified; 17 | options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); 18 | options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); 19 | }); 20 | } 21 | 22 | // Must be called before "UseAuthentication" or anything else that writes cookies. 23 | public static void ApplySameSiteCookiePolicy(this IApplicationBuilder app) 24 | { 25 | app.UseCookiePolicy(); 26 | } 27 | 28 | private static void CheckSameSite(HttpContext httpContext, CookieOptions options) 29 | { 30 | if (options.SameSite == SameSiteMode.None) 31 | { 32 | var userAgent = httpContext.Request.Headers[Microsoft.Net.Http.Headers.HeaderNames.UserAgent].ToString(); 33 | if (DisallowsSameSiteNone(userAgent)) 34 | { 35 | options.SameSite = SameSiteMode.Unspecified; 36 | } 37 | } 38 | } 39 | 40 | private static bool DisallowsSameSiteNone(string userAgent) 41 | { 42 | // Cover all iOS based browsers here. This includes: 43 | // - Safari on iOS 12 for iPhone, iPod Touch, iPad 44 | // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad 45 | // - Chrome on iOS 12 for iPhone, iPod Touch, iPad 46 | // All of which are broken by SameSite=None, because they use the iOS 47 | // networking stack. 48 | if (userAgent.Contains("CPU iPhone OS 12") || 49 | userAgent.Contains("iPad; CPU OS 12")) 50 | { 51 | return true; 52 | } 53 | 54 | // Cover Mac OS X based browsers that use the Mac OS networking stack. 55 | // This includes: 56 | // - Safari on Mac OS X. 57 | // This does not include: 58 | // - Chrome on Mac OS X 59 | // Because they do not use the Mac OS networking stack. 60 | if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") && 61 | userAgent.Contains("Version/") && userAgent.Contains("Safari")) 62 | { 63 | return true; 64 | } 65 | 66 | // Cover Chrome 50-69, because some versions are broken by SameSite=None, 67 | // and none in this range require it. 68 | // Note: this covers some pre-Chromium Edge versions, 69 | // but pre-Chromium Edge does not require SameSite=None. 70 | if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6")) 71 | { 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Services/AzureADAppRolesProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Azure.Identity; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.Options; 8 | using Microsoft.Graph; 9 | 10 | namespace AppRoles.WebApp.Services 11 | { 12 | public class AzureADAppRolesProvider : IAppRolesProvider 13 | { 14 | private readonly ILogger logger; 15 | private readonly GraphServiceClient graphClient; 16 | 17 | public AzureADAppRolesProvider(ILogger logger, IOptions options) 18 | { 19 | this.logger = logger; 20 | // Create the Graph client using an app which refers back to the "regular" Azure AD endpoints 21 | // of the B2C directory, i.e. not "tenant.b2clogin.com" but "login.microsoftonline.com/tenant". 22 | // This can then be used to perform Graph API calls using the specified client application's identity 23 | // and client credentials. 24 | var clientSecretCredential = new ClientSecretCredential(options.Value.Domain, options.Value.AzureADAppRolesProviderClientId, options.Value.AzureADAppRolesProviderClientSecret); 25 | this.graphClient = new GraphServiceClient(clientSecretCredential); 26 | } 27 | 28 | public async Task> GetAppRolesAsync(string userId, string appId) 29 | { 30 | // Look up the user's app roles on the requested app. 31 | // This code requires (Application.Read.All + User.Read.All) OR (Directory.Read.All) for the 32 | // client application calling the Graph API. 33 | // In production code, the graph client as well as potentially the service principals of resource apps and perhaps 34 | // even the user's app roles for each resource app should be cached for optimized performance to avoid additional 35 | // requests for each individual user authentication. 36 | this.logger.LogInformation($"Retrieving app roles for user id \"{userId}\" and app id \"{appId}\""); 37 | 38 | // Get the service principal of the resource app that the user is trying to sign in to. 39 | // See https://docs.microsoft.com/en-us/graph/api/serviceprincipal-list. 40 | var servicePrincipalsForResourceApp = await this.graphClient.ServicePrincipals.Request().Filter($"appId eq '{appId}'").GetAsync(); 41 | var servicePrincipalForResourceApp = servicePrincipalsForResourceApp.SingleOrDefault(); 42 | if (servicePrincipalForResourceApp == null) 43 | { 44 | this.logger.LogError($"The service principal of app \"{appId}\" could not be found; no app roles will be returned."); 45 | throw new ArgumentException($"App roles could not be determined for app \"{appId}\"."); 46 | } 47 | 48 | // Get all app role assignments for the given user and resource app service principal. 49 | // See https://docs.microsoft.com/en-us/graph/api/user-list-approleassignments. 50 | var userAppRoleAssignments = await this.graphClient.Users[userId].AppRoleAssignments.Request().Filter($"resourceId eq {servicePrincipalForResourceApp.Id}").GetAsync(); 51 | var appRoleIds = userAppRoleAssignments.Select(a => a.AppRoleId).ToArray(); 52 | var appRoles = servicePrincipalForResourceApp.AppRoles.Where(a => appRoleIds.Contains(a.Id)).Select(a => a.Value).ToArray(); 53 | 54 | this.logger.LogInformation($"Retrieved app roles for user id \"{userId}\" and app id \"{appId}\": {string.Join(' ', appRoles)}"); 55 | return appRoles; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Controllers/AppRolesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using AppRoles.WebApp.Services; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AppRoles.WebApp.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | [ApiController] 14 | public class AppRolesController : ControllerBase 15 | { 16 | private readonly ILogger logger; 17 | private readonly IAppRolesProvider appRolesProvider; 18 | private readonly AppRolesOptions options; 19 | 20 | public AppRolesController(ILogger logger, IAppRolesProvider appRolesProvider, AppRolesOptions options) 21 | { 22 | this.logger = logger; 23 | this.appRolesProvider = appRolesProvider; 24 | this.options = options; 25 | } 26 | 27 | [HttpPost(nameof(GetAppRoles))] 28 | public async Task GetAppRoles([FromBody] JsonElement body) 29 | { 30 | // Azure AD B2C calls into this API when a user is attempting to sign in. 31 | // We expect a JSON object in the HTTP request which contains the input claims. 32 | try 33 | { 34 | this.logger.LogInformation("App roles are being requested."); 35 | 36 | // Log the incoming request body. 37 | logger.LogInformation("Request body:"); 38 | logger.LogInformation(JsonSerializer.Serialize(body, new JsonSerializerOptions { WriteIndented = true })); 39 | 40 | // Get the object id of the user that is signing in. 41 | var objectId = body.GetProperty("objectId").GetString(); 42 | 43 | // Get the client id of the app that the user is signing in to. 44 | var clientId = body.GetProperty("client_id").GetString(); 45 | 46 | // Retrieve the app roles assigned to the user for the requested application. 47 | var appRoles = await this.appRolesProvider.GetAppRolesAsync(objectId, clientId); 48 | 49 | // Custom user attributes in Azure AD B2C cannot be collections, so we emit them 50 | // into a single claim value separated with spaces. 51 | var appRolesValue = (appRoles == null || !appRoles.Any()) ? null : string.Join(' ', appRoles); 52 | 53 | return GetContinueApiResponse("GetAppRoles-Succeeded", "Your app roles were successfully determined.", appRolesValue); 54 | } 55 | catch (Exception exc) 56 | { 57 | this.logger.LogError(exc, "Error while processing request body: " + exc.ToString()); 58 | return GetBlockPageApiResponse("GetAppRoles-InternalError", "An error occurred while determining your app roles, please try again later."); 59 | } 60 | } 61 | 62 | private IActionResult GetContinueApiResponse(string code, string userMessage, string appRoles) 63 | { 64 | return GetB2cApiConnectorResponse("Continue", code, userMessage, 200, appRoles); 65 | } 66 | 67 | private IActionResult GetValidationErrorApiResponse(string code, string userMessage) 68 | { 69 | return GetB2cApiConnectorResponse("ValidationError", code, userMessage, 400, null); 70 | } 71 | 72 | private IActionResult GetBlockPageApiResponse(string code, string userMessage) 73 | { 74 | return GetB2cApiConnectorResponse("ShowBlockPage", code, userMessage, 200, null); 75 | } 76 | 77 | private IActionResult GetB2cApiConnectorResponse(string action, string code, string userMessage, int statusCode, string appRoles) 78 | { 79 | var responseProperties = new Dictionary 80 | { 81 | { "version", "1.0.0" }, 82 | { "action", action }, 83 | { "userMessage", userMessage }, 84 | { this.options.UserAttributeName, appRoles } 85 | }; 86 | if (statusCode != 200) 87 | { 88 | // Include the status in the body as well, but only for validation errors. 89 | responseProperties["status"] = statusCode.ToString(); 90 | } 91 | return new JsonResult(responseProperties) { StatusCode = statusCode }; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/AppRoles.WebApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using AppRoles.WebApp.Services; 3 | using Microsoft.AspNetCore.Authentication; 4 | using Microsoft.AspNetCore.Authentication.AzureADB2C.UI; 5 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | 12 | namespace AppRoles.WebApp 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | // Configure App Roles options. 27 | var appRolesOptions = new AppRolesOptions(); 28 | Configuration.GetSection("AppRoles").Bind(appRolesOptions); 29 | services.AddSingleton(appRolesOptions); 30 | 31 | // Inject a service to work with App Roles in the Azure AD B2C directory itself which is accessed through the Graph API. 32 | services.Configure(Configuration.GetSection("AzureAdB2C")); 33 | services.AddSingleton(); 34 | 35 | // Configure support for the SameSite cookies breaking change. 36 | services.ConfigureSameSiteCookiePolicy(); 37 | 38 | // Don't map any standard OpenID Connect claims to Microsoft-specific claims. 39 | // See https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/. 40 | JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 41 | 42 | // Add Azure AD B2C authentication using OpenID Connect. 43 | #pragma warning disable 0618 // AzureADB2CDefaults is obsolete in favor of "Microsoft.Identity.Web" 44 | services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme) 45 | .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options)); 46 | 47 | services.Configure(AzureADB2CDefaults.OpenIdScheme, options => 48 | { 49 | // Don't remove any incoming claims. 50 | options.ClaimActions.Clear(); 51 | 52 | // Define the role claim type to match the configured user attribute name in Azure AD B2C. 53 | options.TokenValidationParameters.RoleClaimType = appRolesOptions.UserAttributeName; 54 | }); 55 | #pragma warning restore 0618 56 | 57 | // Add a claims transformation to split the space-separated app roles into multiple individual claims, 58 | // so that we can more easily check if a user has a role with User.IsInRole(roleName) and other built-in 59 | // roles functionality within ASP.NET. 60 | services.AddSingleton(new StringSplitClaimsTransformation(appRolesOptions.UserAttributeName)); 61 | 62 | services.AddRazorPages().AddRazorRuntimeCompilation(); 63 | services.AddControllers(); 64 | services.AddRouting(options => { options.LowercaseUrls = true; }); 65 | } 66 | 67 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 68 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 69 | { 70 | if (env.IsDevelopment()) 71 | { 72 | app.UseDeveloperExceptionPage(); 73 | } 74 | else 75 | { 76 | app.UseExceptionHandler("/Error"); 77 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 78 | app.UseHsts(); 79 | } 80 | 81 | app.UseHttpsRedirection(); 82 | app.UseStaticFiles(); 83 | 84 | app.UseRouting(); 85 | 86 | app.UseAuthentication(); 87 | app.UseAuthorization(); 88 | 89 | app.UseEndpoints(endpoints => 90 | { 91 | endpoints.MapRazorPages(); 92 | endpoints.MapControllers(); 93 | }); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/User.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace DelegatedUserManagement.WebApp.Pages 10 | { 11 | [Authorize] 12 | public class UserModel : PageModel 13 | { 14 | private readonly ILogger logger; 15 | private readonly B2cGraphService b2cGraphService; 16 | public bool CanManageUsers { get; set; } 17 | public bool CanSelectGlobalAdmins { get; set; } 18 | public bool CanSelectCompany { get; set; } 19 | public IList Users { get; set; } 20 | 21 | public UserModel(ILogger logger, B2cGraphService b2cGraphService) 22 | { 23 | this.logger = logger; 24 | this.b2cGraphService = b2cGraphService; 25 | } 26 | 27 | public async Task OnGetAsync() 28 | { 29 | if (this.User.IsInRole(Constants.DelegatedUserManagementRoles.GlobalAdmin)) 30 | { 31 | // If the current user is a global admin, show all users. 32 | this.Users = await this.b2cGraphService.GetUsersAsync(); 33 | this.CanManageUsers = true; 34 | this.CanSelectGlobalAdmins = true; 35 | this.CanSelectCompany = true; 36 | } 37 | else if (this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 38 | { 39 | // If the current user is a company admin, show only that company's users. 40 | var userCompanyId = this.User.FindFirst(this.b2cGraphService.GetUserAttributeClaimName(Constants.UserAttributes.CompanyId))?.Value; 41 | this.Users = await this.b2cGraphService.GetUsersAsync(userCompanyId); 42 | this.CanManageUsers = true; 43 | this.CanSelectGlobalAdmins = false; 44 | this.CanSelectCompany = false; 45 | } 46 | else 47 | { 48 | // If the current user is no admin, they cannot see or manage any users. 49 | this.CanManageUsers = false; 50 | } 51 | 52 | this.Users = this.Users?.OrderBy(u => u.CompanyId).ThenBy(u => u.DelegatedUserManagementRole).ThenBy(u => u.Name).ToArray(); 53 | } 54 | 55 | public async Task OnPostUpdateUserAsync(User user) 56 | { 57 | // Check that the current user has permissions to create the invitation. 58 | if (!this.User.IsInRole(Constants.DelegatedUserManagementRoles.GlobalAdmin) && !this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 59 | { 60 | return this.Unauthorized(); 61 | } 62 | 63 | // In a real production scenario, additional validation would be needed here especially for Company Admins: 64 | // - Ensure that the user being modified is of the same company as the current user. 65 | // - Ensure that the user being modified isn't being changed to a different company. 66 | // - Ensure that the user's role isn't being elevated to global admin. 67 | // - ... 68 | 69 | await this.b2cGraphService.UpdateUserAsync(user); 70 | return RedirectToPage(); 71 | } 72 | 73 | public async Task OnPostDeleteUserAsync(string id) 74 | { 75 | // Check that the current user has permissions to create the invitation. 76 | if (!this.User.IsInRole(Constants.DelegatedUserManagementRoles.GlobalAdmin) && !this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 77 | { 78 | return this.Unauthorized(); 79 | } 80 | 81 | // In a real production scenario, additional validation would be needed here especially for Company Admins: 82 | // - Ensure that the user being deleted is of the same company as the current user. 83 | // - ... 84 | 85 | // Ensure you can't delete yourself. 86 | var currentUserId = this.User.FindFirst(Constants.ClaimTypes.ObjectId).Value; 87 | if (!string.Equals(currentUserId, id)) 88 | { 89 | await this.b2cGraphService.DeleteUserAsync(id); 90 | } 91 | return RedirectToPage(); 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.IO; 4 | using Microsoft.AspNetCore.Authentication; 5 | using Microsoft.AspNetCore.Authentication.AzureADB2C.UI; 6 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | 13 | namespace DelegatedUserManagement.WebApp 14 | { 15 | public class Startup 16 | { 17 | public Startup(IConfiguration configuration) 18 | { 19 | Configuration = configuration; 20 | } 21 | 22 | public IConfiguration Configuration { get; } 23 | 24 | // This method gets called by the runtime. Use this method to add services to the container. 25 | public void ConfigureServices(IServiceCollection services) 26 | { 27 | // Inject a service to store user invitations. 28 | var userInvitationsBasePath = Configuration.GetValue("App:UserInvitationsBasePath"); 29 | if (string.IsNullOrWhiteSpace(userInvitationsBasePath)) 30 | { 31 | userInvitationsBasePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UserInvitations"); 32 | } 33 | services.AddSingleton(new FileStorageUserInvitationRepository(userInvitationsBasePath)); 34 | 35 | // Inject a service to work with Azure AD B2C through the Graph API. 36 | #pragma warning disable 0618 // AzureADB2CDefaults is obsolete in favor of "Microsoft.Identity.Web" 37 | var b2cConfigurationSection = Configuration.GetSection("AzureAdB2C"); 38 | var b2cGraphService = new B2cGraphService( 39 | clientId: b2cConfigurationSection.GetValue(nameof(AzureADB2COptions.ClientId)), 40 | domain: b2cConfigurationSection.GetValue(nameof(AzureADB2COptions.Domain)), 41 | clientSecret: b2cConfigurationSection.GetValue(nameof(AzureADB2COptions.ClientSecret)), 42 | b2cExtensionsAppClientId: b2cConfigurationSection.GetValue("B2cExtensionsAppClientId")); 43 | services.AddSingleton(b2cGraphService); 44 | 45 | // Configure support for the SameSite cookies breaking change. 46 | services.ConfigureSameSiteCookiePolicy(); 47 | 48 | // Don't map any standard OpenID Connect claims to Microsoft-specific claims. 49 | // See https://leastprivilege.com/2017/11/15/missing-claims-in-the-asp-net-core-2-openid-connect-handler/. 50 | JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 51 | 52 | // Add Azure AD B2C authentication using OpenID Connect. 53 | services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme) 54 | .AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options)); 55 | 56 | services.Configure(AzureADB2CDefaults.OpenIdScheme, options => 57 | { 58 | // Don't remove any incoming claims. 59 | options.ClaimActions.Clear(); 60 | 61 | // Set the "role" claim type to be the "extension_DelegatedUserManagementRole" user attribute. 62 | options.TokenValidationParameters.RoleClaimType = b2cGraphService.GetUserAttributeClaimName(Constants.UserAttributes.DelegatedUserManagementRole); 63 | }); 64 | #pragma warning restore 0618 65 | 66 | services.AddRazorPages(); 67 | services.AddControllers(); 68 | services.AddRouting(options => { options.LowercaseUrls = true; }); 69 | } 70 | 71 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 72 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 73 | { 74 | if (env.IsDevelopment()) 75 | { 76 | app.UseDeveloperExceptionPage(); 77 | } 78 | else 79 | { 80 | app.UseExceptionHandler("/Error"); 81 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 82 | app.UseHsts(); 83 | } 84 | 85 | app.UseHttpsRedirection(); 86 | app.UseStaticFiles(); 87 | 88 | app.UseRouting(); 89 | 90 | app.UseAuthentication(); 91 | app.UseAuthorization(); 92 | 93 | app.UseEndpoints(endpoints => 94 | { 95 | endpoints.MapRazorPages(); 96 | endpoints.MapControllers(); 97 | }); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/UserInvitation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model UserInvitationModel 3 | @{ 4 | ViewData["Title"] = "Invitations"; 5 | } 6 | @if (Model.ShowGlobalAdminUserInvitation) 7 | { 8 |

Sign-up as Global Admin

9 |

10 | There aren't any users yet, you can sign up 11 | as the initial global admin with the following invitation code: 12 |

13 |
14 |
@Model.GlobalAdminInvitationCode
15 |
16 | } 17 | else if (!Model.CanManageUserInvitations) 18 | { 19 |
You don't have permissions to invite users.
20 | } 21 | else 22 | { 23 |

Invite New User

24 |
25 |
26 | 27 |
28 | 29 | 32 |
33 |
34 | 35 | 38 |
39 | @if (Model.CanSelectGlobalAdmins) 40 | { 41 |
42 | 43 | 46 |
47 | } 48 |
49 | @if (Model.CanSelectCompany) 50 | { 51 |
52 | 53 | 54 |
55 | } 56 |
57 | 58 | 59 |
60 | 61 |
62 | 63 |

Pending User Invitations

64 | @if (!Model.PendingUserInvitations.Any()) 65 | { 66 |
There are no pending user invitations.
67 | } 68 | else 69 | { 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | @foreach (var userInvitation in Model.PendingUserInvitations) 82 | { 83 | 84 | 90 | 91 | 92 | 93 | 94 | 95 | } 96 | 97 |
CompanyRoleInvitation CodeExpires (UTC)
85 |
86 | 87 | 88 |
89 |
@userInvitation.CompanyId@userInvitation.DelegatedUserManagementRole@userInvitation.InvitationCode@userInvitation.ExpiresTime.ToString("G")
98 | } 99 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appServiceWebAppName": { 6 | "type": "String" 7 | }, 8 | "appServicePlanName": { 9 | "type": "String" 10 | }, 11 | "azureAdB2cDomain": { 12 | "type": "String", 13 | "defaultValue": ".onmicrosoft.com" 14 | }, 15 | "azureAdB2cInstance": { 16 | "type": "String", 17 | "defaultValue": "https://.b2clogin.com/tfp/" 18 | }, 19 | "azureAdB2cClientId": { 20 | "type": "String", 21 | "defaultValue": "" 22 | }, 23 | "azureAdB2cClientSecret": { 24 | "type": "String", 25 | "defaultValue": "" 26 | }, 27 | "azureAdB2cSignUpSignInPolicyId": { 28 | "type": "String", 29 | "defaultValue": "" 30 | }, 31 | "azureAdB2cResetPasswordPolicyId": { 32 | "type": "String", 33 | "defaultValue": "" 34 | }, 35 | "azureAdB2cEditProfilePolicyId": { 36 | "type": "String", 37 | "defaultValue": "" 38 | }, 39 | "azureAdB2cExtensionsAppClientId": { 40 | "type": "String", 41 | "defaultValue": "" 42 | } 43 | }, 44 | "resources": [ 45 | { 46 | "apiVersion": "2019-08-01", 47 | "type": "Microsoft.Web/serverfarms", 48 | "name": "[parameters('appServicePlanName')]", 49 | "location": "[resourceGroup().location]", 50 | "kind": "linux", 51 | "sku": { 52 | "Name": "F1" 53 | }, 54 | "properties": { 55 | "name": "[parameters('appServicePlanName')]", 56 | "reserved": true 57 | } 58 | }, 59 | { 60 | "apiVersion": "2019-08-01", 61 | "type": "Microsoft.Web/sites", 62 | "name": "[parameters('appServiceWebAppName')]", 63 | "location": "[resourceGroup().location]", 64 | "dependsOn": [ 65 | "[resourceId('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]" 66 | ], 67 | "properties": { 68 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", 69 | "siteConfig": { 70 | "appSettings": [ 71 | { 72 | "name": "AzureAdB2C__Domain", 73 | "value": "[parameters('azureAdB2cDomain')]" 74 | }, 75 | { 76 | "name": "AzureAdB2C__Instance", 77 | "value": "[parameters('azureAdB2cInstance')]" 78 | }, 79 | { 80 | "name": "AzureAdB2C__ClientId", 81 | "value": "[parameters('azureAdB2cClientId')]" 82 | }, 83 | { 84 | "name": "AzureAdB2C__ClientSecret", 85 | "value": "[parameters('azureAdB2cClientSecret')]" 86 | }, 87 | { 88 | "name": "AzureAdB2C__SignUpSignInPolicyId", 89 | "value": "[parameters('azureAdB2cSignUpSignInPolicyId')]" 90 | }, 91 | { 92 | "name": "AzureAdB2C__ResetPasswordPolicyId", 93 | "value": "[parameters('azureAdB2cResetPasswordPolicyId')]" 94 | }, 95 | { 96 | "name": "AzureAdB2C__EditProfilePolicyId", 97 | "value": "[parameters('azureAdB2cEditProfilePolicyId')]" 98 | }, 99 | { 100 | "name": "AzureAdB2C__B2cExtensionsAppClientId", 101 | "value": "[parameters('azureAdB2cExtensionsAppClientId')]" 102 | }, 103 | { 104 | "name": "ASPNETCORE_FORWARDEDHEADERS_ENABLED", 105 | "value": "true" 106 | }, 107 | { 108 | "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", 109 | "value": "false" 110 | } 111 | ], 112 | "linuxFxVersion": "DOCKER|jelledruyts/identitysamplesb2c-delegatedusermanagement" 113 | } 114 | } 115 | } 116 | ] 117 | } -------------------------------------------------------------------------------- /Authorization-AppRoles/azuredeploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appServiceWebAppName": { 6 | "type": "String" 7 | }, 8 | "appServicePlanName": { 9 | "type": "String" 10 | }, 11 | "azureAdB2cDomain": { 12 | "type": "String", 13 | "defaultValue": ".onmicrosoft.com" 14 | }, 15 | "azureAdB2cAzureADAppRolesProviderClientId": { 16 | "type": "String", 17 | "defaultValue": "" 18 | }, 19 | "azureAdB2cAzureADAppRolesProviderClientSecret": { 20 | "type": "String", 21 | "defaultValue": "" 22 | }, 23 | "azureAdB2cInstance": { 24 | "type": "String", 25 | "defaultValue": "https://.b2clogin.com/tfp/" 26 | }, 27 | "azureAdB2cClientId": { 28 | "type": "String", 29 | "defaultValue": "" 30 | }, 31 | "azureAdB2cSignUpSignInPolicyId": { 32 | "type": "String", 33 | "defaultValue": "" 34 | }, 35 | "azureAdB2cResetPasswordPolicyId": { 36 | "type": "String", 37 | "defaultValue": "" 38 | }, 39 | "azureAdB2cEditProfilePolicyId": { 40 | "type": "String", 41 | "defaultValue": "" 42 | }, 43 | "appRolesUserAttributeName": { 44 | "type": "String", 45 | "defaultValue": "extension_AppRoles" 46 | } 47 | }, 48 | "resources": [ 49 | { 50 | "apiVersion": "2019-08-01", 51 | "type": "Microsoft.Web/serverfarms", 52 | "name": "[parameters('appServicePlanName')]", 53 | "location": "[resourceGroup().location]", 54 | "kind": "linux", 55 | "sku": { 56 | "Name": "F1" 57 | }, 58 | "properties": { 59 | "name": "[parameters('appServicePlanName')]", 60 | "reserved": true 61 | } 62 | }, 63 | { 64 | "apiVersion": "2019-08-01", 65 | "type": "Microsoft.Web/sites", 66 | "name": "[parameters('appServiceWebAppName')]", 67 | "location": "[resourceGroup().location]", 68 | "dependsOn": [ 69 | "[resourceId('Microsoft.Web/serverfarms/', parameters('appServicePlanName'))]" 70 | ], 71 | "properties": { 72 | "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]", 73 | "siteConfig": { 74 | "appSettings": [ 75 | { 76 | "name": "AzureAdB2C__Domain", 77 | "value": "[parameters('azureAdB2cDomain')]" 78 | }, 79 | { 80 | "name": "AzureAdB2C__AzureADAppRolesProviderClientId", 81 | "value": "[parameters('azureAdB2cAzureADAppRolesProviderClientId')]" 82 | }, 83 | { 84 | "name": "AzureAdB2C__AzureADAppRolesProviderClientSecret", 85 | "value": "[parameters('azureAdB2cAzureADAppRolesProviderClientSecret')]" 86 | }, 87 | { 88 | "name": "AzureAdB2C__Instance", 89 | "value": "[parameters('azureAdB2cInstance')]" 90 | }, 91 | { 92 | "name": "AzureAdB2C__ClientId", 93 | "value": "[parameters('azureAdB2cClientId')]" 94 | }, 95 | { 96 | "name": "AzureAdB2C__SignUpSignInPolicyId", 97 | "value": "[parameters('azureAdB2cSignUpSignInPolicyId')]" 98 | }, 99 | { 100 | "name": "AzureAdB2C__ResetPasswordPolicyId", 101 | "value": "[parameters('azureAdB2cResetPasswordPolicyId')]" 102 | }, 103 | { 104 | "name": "AzureAdB2C__EditProfilePolicyId", 105 | "value": "[parameters('azureAdB2cEditProfilePolicyId')]" 106 | }, 107 | { 108 | "name": "AppRoles__UserAttributeName", 109 | "value": "[parameters('appRolesUserAttributeName')]" 110 | }, 111 | { 112 | "name": "ASPNETCORE_FORWARDEDHEADERS_ENABLED", 113 | "value": "true" 114 | }, 115 | { 116 | "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", 117 | "value": "false" 118 | } 119 | ], 120 | "linuxFxVersion": "DOCKER|jelledruyts/identitysamplesb2c-approles" 121 | } 122 | } 123 | } 124 | ] 125 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Services/B2cGraphService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Azure.Identity; 5 | using Microsoft.Graph; 6 | 7 | namespace DelegatedUserManagement.WebApp 8 | { 9 | public class B2cGraphService 10 | { 11 | private readonly GraphServiceClient graphClient; 12 | private readonly string b2cExtensionPrefix; 13 | 14 | public B2cGraphService(string clientId, string domain, string clientSecret, string b2cExtensionsAppClientId) 15 | { 16 | // Create the Graph client using an app which refers back to the "regular" Azure AD endpoints 17 | // of the B2C directory, i.e. not "tenant.b2clogin.com" but "login.microsoftonline.com/tenant". 18 | // This can then be used to perform Graph API calls using the B2C client application's identity and client credentials. 19 | var clientSecretCredential = new ClientSecretCredential(domain, clientId, clientSecret); 20 | this.graphClient = new GraphServiceClient(clientSecretCredential); 21 | 22 | this.b2cExtensionPrefix = b2cExtensionsAppClientId.Replace("-", ""); 23 | } 24 | 25 | public async Task> GetUsersAsync(string companyId = null) 26 | { 27 | // Determine all the user properties to request from the Graph API. 28 | // Note: there is currently no API to return *all* user properties, only a subset is returned by default 29 | // and if you need more, you have to explicitly request these as below. 30 | var companyIdExtensionName = GetUserAttributeExtensionName(Constants.UserAttributes.CompanyId); 31 | var delegatedUserManagementRoleExtensionName = GetUserAttributeExtensionName(Constants.UserAttributes.DelegatedUserManagementRole); 32 | var invitationCodeExtensionName = GetUserAttributeExtensionName(Constants.UserAttributes.InvitationCode); 33 | var userPropertiesToRequest = new[] { nameof(Microsoft.Graph.User.Id), nameof(Microsoft.Graph.User.DisplayName), nameof(Microsoft.Graph.User.Identities), 34 | companyIdExtensionName, delegatedUserManagementRoleExtensionName, invitationCodeExtensionName }; 35 | 36 | // Perform the Graph API user request and keep paging through the results until we have them all. 37 | var users = new List(); 38 | var userRequest = this.graphClient.Users.Request().Select(string.Join(",", userPropertiesToRequest)); 39 | if (!string.IsNullOrWhiteSpace(companyId)) 40 | { 41 | // Filter directly in the Graph API call to retrieve only users that are from the specified CompanyId. 42 | // Make sure to properly escape single quotes into two consecutive single quotes. 43 | userRequest = userRequest.Filter($"{companyIdExtensionName} eq '{companyId.Replace("'", "''")}'"); 44 | } 45 | while (userRequest != null) 46 | { 47 | var usersPage = await userRequest.GetAsync(); 48 | foreach (var user in usersPage) 49 | { 50 | // Check if the user is a "real" B2C user, i.e. one that has signed up through a B2C user flow 51 | // and therefore has at least one B2C user attribute in the AdditionalData dictionary. 52 | if (user.AdditionalData != null && user.AdditionalData.Any()) 53 | { 54 | users.Add(new User 55 | { 56 | Id = user.Id, 57 | Name = user.DisplayName, 58 | InvitationCode = GetUserAttribute(user, invitationCodeExtensionName), 59 | CompanyId = GetUserAttribute(user, companyIdExtensionName), 60 | DelegatedUserManagementRole = GetUserAttribute(user, delegatedUserManagementRoleExtensionName) 61 | }); 62 | } 63 | } 64 | userRequest = usersPage.NextPageRequest; 65 | } 66 | return users; 67 | } 68 | 69 | public async Task UpdateUserAsync(User user) 70 | { 71 | var userPatch = new Microsoft.Graph.User(); 72 | userPatch.DisplayName = user.Name; 73 | userPatch.AdditionalData = new Dictionary(); 74 | userPatch.AdditionalData[GetUserAttributeExtensionName(Constants.UserAttributes.CompanyId)] = user.CompanyId; 75 | userPatch.AdditionalData[GetUserAttributeExtensionName(Constants.UserAttributes.DelegatedUserManagementRole)] = user.DelegatedUserManagementRole; 76 | await this.graphClient.Users[user.Id].Request().UpdateAsync(userPatch); 77 | } 78 | 79 | public async Task DeleteUserAsync(string userId) 80 | { 81 | await this.graphClient.Users[userId].Request().DeleteAsync(); 82 | } 83 | 84 | public string GetUserAttributeClaimName(string userAttributeName) 85 | { 86 | return $"extension_{userAttributeName}"; 87 | } 88 | 89 | public string GetUserAttributeExtensionName(string userAttributeName) 90 | { 91 | return $"extension_{this.b2cExtensionPrefix}_{userAttributeName}"; 92 | } 93 | 94 | private string GetUserAttribute(Microsoft.Graph.User user, string extensionName) 95 | { 96 | if (user.AdditionalData == null || !user.AdditionalData.ContainsKey(extensionName)) 97 | { 98 | return null; 99 | } 100 | return (string)user.AdditionalData[extensionName]; 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Pages/UserInvitation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace DelegatedUserManagement.WebApp.Pages 11 | { 12 | public class UserInvitationModel : PageModel 13 | { 14 | private readonly ILogger logger; 15 | private readonly IUserInvitationRepository userInvitationRepository; 16 | private readonly B2cGraphService b2cGraphService; 17 | public bool ShowGlobalAdminUserInvitation { get; set; } 18 | public bool CanManageUserInvitations { get; set; } 19 | public bool CanSelectGlobalAdmins { get; set; } 20 | public bool CanSelectCompany { get; set; } 21 | public string GlobalAdminInvitationCode { get; } = Guid.Empty.ToString(); 22 | public IList PendingUserInvitations { get; set; } 23 | 24 | public UserInvitationModel(ILogger logger, IUserInvitationRepository userInvitationRepository, B2cGraphService b2cGraphService) 25 | { 26 | this.logger = logger; 27 | this.userInvitationRepository = userInvitationRepository; 28 | this.b2cGraphService = b2cGraphService; 29 | } 30 | 31 | public async Task OnGetAsync() 32 | { 33 | var allUsers = await this.b2cGraphService.GetUsersAsync(); 34 | if (!allUsers.Any()) 35 | { 36 | // If there aren't any users yet, allow anonymous access to bootstrap the initial global admin. 37 | var globalAdminUserInvitation = new UserInvitation 38 | { 39 | InvitationCode = GlobalAdminInvitationCode, 40 | CompanyId = null, 41 | DelegatedUserManagementRole = Constants.DelegatedUserManagementRoles.GlobalAdmin, 42 | CreatedTime = DateTimeOffset.UtcNow, 43 | ExpiresTime = DateTimeOffset.UtcNow.AddYears(1), 44 | }; 45 | await this.userInvitationRepository.CreateUserInvitationAsync(globalAdminUserInvitation); 46 | this.ShowGlobalAdminUserInvitation = true; 47 | this.CanManageUserInvitations = false; 48 | } 49 | else 50 | { 51 | if (!this.User.Identity.IsAuthenticated) 52 | { 53 | // Force the user to sign in if they're not authenticated at this point. 54 | return this.Challenge(); 55 | } 56 | this.ShowGlobalAdminUserInvitation = false; 57 | 58 | if (this.User.IsInRole(Constants.DelegatedUserManagementRoles.GlobalAdmin)) 59 | { 60 | this.CanManageUserInvitations = true; 61 | this.CanSelectGlobalAdmins = true; 62 | this.CanSelectCompany = true; 63 | this.PendingUserInvitations = await this.userInvitationRepository.GetPendingUserInvitationsAsync(); ; 64 | } 65 | else if (this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 66 | { 67 | this.CanManageUserInvitations = true; 68 | this.CanSelectGlobalAdmins = false; 69 | this.CanSelectCompany = false; 70 | var userCompanyId = this.User.FindFirst(this.b2cGraphService.GetUserAttributeClaimName(Constants.UserAttributes.CompanyId))?.Value; 71 | this.PendingUserInvitations = await this.userInvitationRepository.GetPendingUserInvitationsAsync(userCompanyId); ; 72 | } 73 | else 74 | { 75 | this.CanManageUserInvitations = false; 76 | } 77 | 78 | this.PendingUserInvitations = this.PendingUserInvitations?.OrderBy(u => u.CompanyId).ThenBy(u => u.DelegatedUserManagementRole).ToArray(); 79 | } 80 | return this.Page(); 81 | } 82 | 83 | public async Task OnPostAsync(UserInvitationRequest userInvitationRequest) 84 | { 85 | // Check that the current user has permissions to create the invitation. 86 | if (!this.User.IsInRole(Constants.DelegatedUserManagementRoles.GlobalAdmin) && !this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 87 | { 88 | return this.Unauthorized(); 89 | } 90 | 91 | var userInvitation = new UserInvitation 92 | { 93 | InvitationCode = Guid.NewGuid().ToString(), 94 | CompanyId = userInvitationRequest.CompanyId, 95 | DelegatedUserManagementRole = userInvitationRequest.DelegatedUserManagementRole, 96 | CreatedTime = DateTimeOffset.UtcNow, 97 | ExpiresTime = DateTimeOffset.UtcNow.AddHours(userInvitationRequest.ValidHours), 98 | CreatedBy = this.User.FindFirst(Constants.ClaimTypes.ObjectId)?.Value 99 | }; 100 | 101 | if (this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 102 | { 103 | // For company admins, ensure to set the newly invited user's company to the inviting user's company. 104 | var userCompanyId = this.User.FindFirst(this.b2cGraphService.GetUserAttributeClaimName(Constants.UserAttributes.CompanyId))?.Value; 105 | userInvitation.CompanyId = userCompanyId; 106 | 107 | // Also ensure the invited user isn't elevated to a global admin. 108 | if (string.Equals(userInvitation.DelegatedUserManagementRole, Constants.DelegatedUserManagementRoles.GlobalAdmin, StringComparison.InvariantCultureIgnoreCase)) 109 | { 110 | userInvitation.DelegatedUserManagementRole = Constants.DelegatedUserManagementRoles.CompanyAdmin; 111 | } 112 | } 113 | await this.userInvitationRepository.CreateUserInvitationAsync(userInvitation); 114 | return RedirectToPage(); 115 | } 116 | 117 | public async Task OnPostDeleteUserInvitationAsync(string invitationCode) 118 | { 119 | // Check that the current user has permissions to delete the invitation. 120 | if (!this.User.IsInRole(Constants.DelegatedUserManagementRoles.GlobalAdmin) && !this.User.IsInRole(Constants.DelegatedUserManagementRoles.CompanyAdmin)) 121 | { 122 | return this.Unauthorized(); 123 | } 124 | 125 | // In a real production scenario, additional validation would be needed here especially for Company Admins: 126 | // - Ensure that the user invitation being deleted is of the same company as the current user. 127 | // - ... 128 | 129 | await this.userInvitationRepository.DeletePendingUserInvitationAsync(invitationCode); 130 | return RedirectToPage(); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/DelegatedUserManagement.WebApp/Controllers/UserInvitationController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace DelegatedUserManagement.WebApp.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | [ApiController] 12 | public class UserInvitationController : ControllerBase 13 | { 14 | private readonly ILogger logger; 15 | private readonly IUserInvitationRepository userInvitationRepository; 16 | private readonly B2cGraphService b2cGraphService; 17 | 18 | public UserInvitationController(ILogger logger, IUserInvitationRepository userInvitationRepository, B2cGraphService b2cGraphService) 19 | { 20 | this.logger = logger; 21 | this.userInvitationRepository = userInvitationRepository; 22 | this.b2cGraphService = b2cGraphService; 23 | } 24 | 25 | [HttpPost(nameof(Redeem))] 26 | public async Task Redeem([FromBody] JsonElement body) 27 | { 28 | // Azure AD B2C calls into this API when a user is attempting to sign up with an invitation code. 29 | // We expect a JSON object in the HTTP request which contains the input claims as well as an additional 30 | // property "ui_locales" containing the locale being used in the user journey (browser flow). 31 | try 32 | { 33 | this.logger.LogInformation("An invitation code is being redeemed."); 34 | 35 | // Look up the invitation code in the incoming request. 36 | var invitationCode = default(string); 37 | this.logger.LogInformation("Request properties:"); 38 | foreach (var element in body.EnumerateObject()) 39 | { 40 | this.logger.LogInformation($"- {element.Name}: {element.Value.GetRawText()}"); 41 | // The element name should be the full extension name as seen by the Graph API (e.g. "extension_appid_InvitationCode"). 42 | if (element.Name.Equals(this.b2cGraphService.GetUserAttributeExtensionName(Constants.UserAttributes.InvitationCode), StringComparison.InvariantCultureIgnoreCase)) 43 | { 44 | invitationCode = element.Value.GetString(); 45 | } 46 | } 47 | 48 | if (string.IsNullOrWhiteSpace(invitationCode) || invitationCode.Length < 10) 49 | { 50 | // No invitation code was found in the request or it was too short, return a validation error. 51 | this.logger.LogInformation($"The provided invitation code \"{invitationCode}\" is invalid."); 52 | return GetValidationErrorApiResponse("UserInvitationRedemptionFailed-Invalid", "The invitation code you provided is invalid."); 53 | } 54 | else 55 | { 56 | // An invitation code was found in the request, look up the user invitation in persistent storage. 57 | this.logger.LogInformation($"Looking up user invitation for invitation code \"{invitationCode}\"..."); 58 | var userInvitation = await this.userInvitationRepository.GetPendingUserInvitationAsync(invitationCode); 59 | if (userInvitation == null) 60 | { 61 | // The requested invitation code was not found in persistent storage. 62 | this.logger.LogWarning($"User invitation for invitation code \"{invitationCode}\" was not found."); 63 | return GetValidationErrorApiResponse("UserInvitationRedemptionFailed-NotFound", "The invitation code you provided is invalid."); 64 | } 65 | else if (userInvitation.ExpiresTime < DateTimeOffset.UtcNow) 66 | { 67 | // The requested invitation code has expired. 68 | this.logger.LogWarning($"User invitation for invitation code \"{invitationCode}\" has expired on {userInvitation.ExpiresTime.ToString("o")}."); 69 | return GetValidationErrorApiResponse("UserInvitationRedemptionFailed-Expired", "The invitation code you provided has expired."); 70 | } 71 | else 72 | { 73 | // The requested invitation code was found in persistent storage and is valid. 74 | this.logger.LogInformation($"User invitation found for invitation code \"{invitationCode}\"."); 75 | 76 | // At this point, the invitation can be deleted again as it has been redeemed. 77 | await this.userInvitationRepository.RedeemUserInvitationAsync(invitationCode); 78 | 79 | return GetContinueApiResponse("UserInvitationRedemptionSucceeded", "The invitation code you provided is valid.", userInvitation); 80 | } 81 | } 82 | } 83 | catch (Exception exc) 84 | { 85 | this.logger.LogError(exc, "Error while processing request body: " + exc.ToString()); 86 | return GetBlockPageApiResponse("UserInvitationRedemptionFailed-InternalError", "An error occurred while validating your invitation code, please try again later."); 87 | } 88 | } 89 | 90 | private IActionResult GetContinueApiResponse(string code, string userMessage, UserInvitation userInvitation) 91 | { 92 | return GetB2cApiConnectorResponse("Continue", code, userMessage, 200, userInvitation); 93 | } 94 | 95 | private IActionResult GetValidationErrorApiResponse(string code, string userMessage) 96 | { 97 | return GetB2cApiConnectorResponse("ValidationError", code, userMessage, 400, null); 98 | } 99 | 100 | private IActionResult GetBlockPageApiResponse(string code, string userMessage) 101 | { 102 | return GetB2cApiConnectorResponse("ShowBlockPage", code, userMessage, 200, null); 103 | } 104 | 105 | private IActionResult GetB2cApiConnectorResponse(string action, string code, string userMessage, int statusCode, UserInvitation userInvitation) 106 | { 107 | var responseProperties = new Dictionary 108 | { 109 | { "version", "1.0.0" }, 110 | { "action", action }, 111 | { "userMessage", userMessage }, 112 | { this.b2cGraphService.GetUserAttributeExtensionName(Constants.UserAttributes.CompanyId), userInvitation?.CompanyId }, // Note: returning just "extension_" (without the App ID) would work as well! 113 | { this.b2cGraphService.GetUserAttributeExtensionName(Constants.UserAttributes.DelegatedUserManagementRole), userInvitation?.DelegatedUserManagementRole } // Note: returning just "extension_" (without the App ID) would work as well! 114 | }; 115 | if (statusCode != 200) 116 | { 117 | // Include the status in the body as well, but only for validation errors. 118 | responseProperties["status"] = statusCode.ToString(); 119 | } 120 | return new JsonResult(responseProperties) { StatusCode = statusCode }; 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # Other 353 | .DS_Store -------------------------------------------------------------------------------- /InvitationCodeDelegatedUserManagement/README.md: -------------------------------------------------------------------------------- 1 | # Identity Sample for Azure AD B2C - Delegated User Management 2 | 3 | This repository contains an ASP.NET Core project which demonstrates delegated user management in [Azure Active Directory B2C](https://azure.microsoft.com/services/active-directory-b2c/) using [API Connectors](https://docs.microsoft.com/azure/active-directory-b2c/api-connectors-overview). 4 | 5 | **IMPORTANT NOTE: The code in this repository is _not_ production-ready. It serves only to demonstrate the main points via minimal working code, and contains no exception handling or other special cases. Refer to the official documentation and samples for more information. Similarly, by design, it does not implement any caching or data persistence (e.g. to a database) to minimize the concepts and technologies being used.** 6 | 7 | ## Scenario 8 | 9 | While Azure AD B2C is often used for "open" sign up scenarios, where _any user_ can self-register an account and access the application (e.g. in e-commerce or open community scenarios), it can also be used for a more closed environment where only users who are explicitly invited can register an account. 10 | 11 | This sample demonstrates such a scenario by using an "invitation code" based sign up flow, which allows users to register for an account through any of the regular supported identity providers (local, social or federated accounts). However, before they are actually allowed to create the account they will need to enter an invitation code which they received from an administrator of the application. Without a valid invitation code, they cannot sign up. 12 | 13 | > Alternatively, the administrator could also pre-create the user in Azure AD B2C (e.g. manually in the Azure Portal or through the Graph API) and send them a password reset link to allow them to sign in to their newly created account. However, this has the downside that it does not allow the end user to choose which identity they want to sign in with (i.e. a local, social or federated identity): the administrator must already choose that when the user account is pre-created on their behalf. It also has the disadvantage that users who never actually access the application will still have an inactive "ghost" account in the directory. 14 | 15 | This sample goes one step further in that it also supports _delegated_ user management, where there aren't just administrators who can invite users, but they can delegate user management for a certain subset of users to others. Imagine that the application is a Software-as-a-Service solution that the vendor is selling to their customers (companies). They will want to have a few "global" administrators in the back-office who can sign up new customers (companies) and invite delegated user administrators for those companies. These company administrators in turn have permissions to invite other users, but _only for their own company_. 16 | 17 | This means that for this sample, there are 3 personas (i.e. application roles): 18 | 19 | - **Global Administrators** who can invite anyone and manage all users 20 | - **Company Administrators** who can only invite and manage users for their own company 21 | - **Company Users** who can use the application but cannot invite or manage any users 22 | 23 | The user's role, as well as the company identifier that they belong to (or blank for global administrators) are stored in Azure AD B2C as custom user attributes and are therefore issued as claims inside the token issued by Azure AD B2C so that the application has this information available directly. 24 | 25 | ## Setup 26 | 27 | ### Configure Azure AD B2C 28 | 29 | - Create an **app registration for the sample app**: 30 | - Make sure to create the app registration for use with **Accounts in any identity provider or organizational directory (for authenticating users with user flows)**. 31 | - The client id of this application should go into the `AzureAdB2C:ClientId` application setting. 32 | - Allow the Implicit grant flow (for Access and ID tokens). 33 | - Set the Redirect URI to `https://localhost:5001/signin-oidc` when running locally or `https:///signin-oidc` when running publicly. 34 | - Create a client secret for this application (it will be needed to [acquire tokens using the OAuth 2.0 Client Credentials grant](https://docs.microsoft.com/azure/active-directory-b2c/microsoft-graph-get-started?tabs=app-reg-ga#microsoft-graph-api-interaction-modes)); this secret value should go into the `AzureAdB2C:ClientSecret` application setting. 35 | - Configure **Application Permissions** for the Microsoft Graph with `User.ReadWrite.All` permissions and perform the required admin consent. 36 | - **Create custom user attributes** in Azure AD B2C: 37 | - Follow the documentation to [create a custom attribute](https://docs.microsoft.com/azure/active-directory-b2c/user-flow-custom-attributes?pivots=b2c-user-flow#create-a-custom-attribute) and configure the following user attributes: 38 | - `CompanyId` (String): The identifier of the user's company. 39 | - `DelegatedUserManagementRole` (String): The role of the user for the purposes of delegated user management. 40 | - `InvitationCode` (String): The invitation code that you have received which allows you to sign up. 41 | - **Create user flows** for **Sign up and sign in** (and optionally **Password reset** and **Profile editing**): 42 | - For all these flows, use the *recommended* version which gives you access to the API connectors feature. 43 | - On all these flows, ensure to return at least `CompanyId`, `DelegatedUserManagementRole`, `Display Name`, `InvitationCode` and `User's Object ID` as the **Application claims**. 44 | - On the **Sign up and sign in** flow, ensure to collect at least `CompanyId`, `DelegatedUserManagementRole` and `InvitationCode` as the **User attributes**. Note that the final values of these attributes will be determined by the user invitation. For now, only user attributes that are explicitly selected here will be persisted to the directory, so if you do not configure these claims here as **User attributes**, they will not be populated with the information from the user invitation! To prevent end user confusion around these fields (which they should ideally never see), you can consider hiding them from the page by providing custom page content (see below). 45 | - On the **Profile editing** flow, ensure *not* to select `CompanyId`, `DelegatedUserManagementRole` and `InvitationCode` in the **User attributes**; otherwise, users could change their own role for example! 46 | 47 | ### Configure and run the sample app 48 | 49 | There are a few options to run the sample app (containing both the REST API and the web application): 50 | 51 | - You can build and run it locally. 52 | - You can open the root folder of this repo in [Visual Studio Code](https://code.visualstudio.com/) where you can just build and debug (install the recommended extensions in the workspace if you don't have them). 53 | - In this case, application settings are configured in the `DelegatedUserManagement.WebApp/appsettings.json` file or by using [.NET User Secrets](https://docs.microsoft.com/aspnet/core/security/app-secrets). 54 | - You can build and run it in a [devcontainer](https://code.visualstudio.com/docs/remote/containers) (including [GitHub Codespaces](https://github.com/features/codespaces)). 55 | - All pre-requisites such as .NET Core are provided in the devcontainer so you don't need to install anything locally. 56 | - In this case, application settings are configured in the `DelegatedUserManagement.WebApp/appsettings.json` file or by using [.NET User Secrets](https://docs.microsoft.com/aspnet/core/security/app-secret). 57 | - You can host a pre-built Docker container which contains the sample app. 58 | - You can find the latest published version of the Docker container publicly on **Docker Hub** at **[jelledruyts/identitysamplesb2c-delegatedusermanagement](https://hub.docker.com/r/jelledruyts/identitysamplesb2c-delegatedusermanagement)** 59 | - In this case, application settings are configured through environment variables. Note that on Linux a colon (`:`) is not allowed in an environment variable, so use a double underscore instead of `:` in that case (e.g. `AzureAdB2C__ClientId`). 60 | - You can easily deploy that same container to Azure App Service. 61 | - [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fazure-ad-b2c%2Fapi-connector-samples%2Fmain%2FInvitationCodeDelegatedUserManagement%2Fazuredeploy.json) 62 | - In this case, you will be prompted to fill in the right application settings for the web app during deployment. 63 | 64 | ### Configure the API Connector 65 | 66 | - Create an API connector towards the invitation redemption API exposed by the sample app: 67 | - Follow the documentation to [add an API connector to a user flow](https://docs.microsoft.com/azure/active-directory-b2c/add-api-connector?pivots=b2c-user-flow). 68 | - The API connector should have the endpoint URL defined as `https:///api/userinvitation/redeem`. 69 | - Note that you need a publicly accessible endpoint for this; when running locally you can consider using a tool such as [ngrok](https://ngrok.com/) to tunnel the traffic to your local machine. 70 | - Note that the REST API in this sample isn't secured; you can set the authentication type to Basic and fill in a dummy username and password. In a real world production case, this should of course be [properly secured](https://docs.microsoft.com/azure/active-directory-b2c/api-connectors-overview?pivots=b2c-user-flow#security-considerations). 71 | - Go back to the **Sign up and sign in** user flow you created earlier and configure the API Connector to run during the **Before creating the user** step. 72 | 73 | ### Try it out 74 | 75 | When the sample app is running and the API Connector is configured, you can browse to the web application and navigate to the *Invitations* page where you can find the initial invitation code for the first global admin. Copy it and perform a sign up; during sign up you will be prompted to enter this invitation code. From then on, you can invite and manage other users on the *Users* page. To check if everything is working correctly, you can see all the claims in the token on the *Identity* page. 76 | 77 | ### Use custom page content (optional) 78 | 79 | As explained above, user attributes that need to be persisted during user creation must currently also be selected in the **User attributes** list (even if they are ultimately populated through the API connector). 80 | 81 | For these fields which the user should not see, you can use custom page content with a small CSS snippet that selects the right HTML elements and then hides them. 82 | 83 | Note that this will not allow users to bypass security and provide their own values: even if they *un-hide* the right fields, the API connector will be called *after* the user has filled in their details, and the information coming back from the API connector will overwrite whatever the user had entered manually. 84 | 85 | To improve the user experience by hiding the necessary fields: 86 | 87 | - Ensure to follow the steps to [customize the user interface](https://docs.microsoft.com/azure/active-directory-b2c/customize-ui-overview). 88 | - Host the [selfAsserted.html](PageLayouts/selfAsserted.html) file (which is based on the *Ocean Blue* template in this case) in a publicly accessible location, e.g. in [Azure Blob Storage](https://docs.microsoft.com/azure/storage/blobs/storage-blobs-introduction) by following the steps in the [custom page content walkthrough](https://docs.microsoft.com/azure/active-directory-b2c/custom-policy-ui-customization#custom-page-content-walkthrough). Note the small CSS ` 93 | 101 | 102 | 108 | 109 | 110 | 116 | 117 | 124 | 125 | 126 | 127 |
128 | Illustration 129 |
130 | 131 | 144 | 145 | --------------------------------------------------------------------------------