Getting Started with Synetec.UserManager NuGet
Written on 2024-11-12 using NuGet version 1.07
Introduction
Synetec.UserManager is a NuGet package designed to streamline and standardize user management in .NET applications. It simplifies the often time-consuming process of handling user registration and authentication by providing pre-built, easy-to-use methods. This package aims to save developer’s time, reduces boilerplate code, and enforces best practices, seamlessly integrating with ASP.NET Core to support secure authentication workflows.
User Registration Sequence Diagram
User Sign-In Sequence Diagram
1. Prerequisites
- Access to the Azure portal to configure necessary services.
- Visual Studio or any compatible C# development environment.
- .NET version supported by
Synetec.UserManager. - Access to
Synetec.NotificationNuGet
2. Setting Up Azure Infrastructure
Main Project Domain
-
Create B2C Domain:
- On the home page of your main project’s azure domain, click Create a resource to go to the Market Place
- Search for Azure Active Directory B2C by Microsoft and Create
- Create a new Azure AD B2C tenant (or link existing if you created it already)
- Organization name : Choose a suitable name for your Azure B2C Domain (e.g. Use existing Domain name and append B2C at the end)
- Initial domain name : Domain name should be fine as it adds .onmicrosoft.com
- Location : Choose a suitable location (e.g. United Kingdom)
- Subscription : Choose related subscription and resource group
Project B2C Domain
-
Azure AD B2C : Switch to the B2C Domain/Directory, afterwards click on Azure AD B2C within the Azure portal
- Create App Registration for API Backend :
- Name (Example): app-{app name}-api
- Overview:
- Client ID: Note down value
- Authentication: No changes needed here for API
- Certificates & secrets: Create new secret and note down value as it is shown only once
- Expose an API : Create a new scope for API access
- Scope name (Example) : {app name}.Access
- State: Enabled
- Create App Registration for Web Frontend :
- Name (Example): app-{app name}-web
- Overview:
- Client ID: Note down value
- Authentication:
- Platform: Single-page application
- Redirect URIs: Website Url for the application to send users after successful sign in (e.g. “https://{WebUrl}”, “http://localhost:4200”)
- Implicit grant and hybrid flows: ID tokens
- API Permissions :
- Click “Add a permission”
- Click tab “APIs my organization uses”
- Search for and click the recently created app registration for the API
- Enable checkbox for the recently created scope
- Click Add permissions
- Click “Grant admin consent for …”
- Create API Connectors:
The API connector will be used during the B2C sign up process to validate if the user exists in the backend database system. If not, the user registration will be preventing the sign up from continuing.
- Click “New API connector”
- Display name : Choose suitable name (e.g. “… API Prod - Validate SignUp”)
- Endpoint URL : https://{ApiUrl}/api/v1/register
- Authentication type, Username, Password: Currently not required by the NuGet endpoint. Use Basic option and dummy values
- Create Sign Up User flow:
- Click “New user flow”
- Click “Sign up” & “Recommended” and then “Create”
- Name B2_1_* (Example): Synetec_SignUp
- Identity providers: Select Email sign up for now. This will be changed a little later
- Multifactor authentication: Disabled
- User attributes and claims:
- Collect attribute (Minimum): Email Address, Display Name
- Return claim (Minimum): Email Addresses, Display Name, User is new, User’s Object ID
- Create Sign In User flow:
- Click “New user flow”
- Click “Sign in” & “Recommended” and then “Create”
- Name B2_1_* (Example): Synetec_SignIn
- Identity providers: Select Email sign in for now. This will be changed a little later
- Multifactor authentication:
- MFA enforcement: Off
- Application claims:
- Return claim (Minimum): Email Addresses, Display Name, User’s Object ID
- Create App Registration for API Backend :
Synetec/Client Domain
Create app registrations for each of the Azure domains (Synetec Domain / Client Domain) who will require login access with their work emails. These app registrations will access the domain’s Azure Entra ID (formerly Azure Active Directory) to validate their emails on sign up and sign in. If the Azure domain is managed by the client, then a request should be sent to their IT team with specific requirements outlined below:
- Create App Registration:
- Name (Example): app-synetec-m365-auth-{B2C Domain}-b2c
- Overview:
- Tenant ID: Note down value
- Client ID: Note down value
- Authentication:
- Platform: Web
- Redirect URIs: https://{B2C Domain}.b2clogin.com/{B2C Domain}.onmicrosoft.com/oauth2/authresp
- Implicit grant and hybrid flows: ID tokens
- Supported account types: Single tenant
- Certificates & secrets: Create new secret and note down value as it is shown only once
Replace {B2C Domain} with the domain of your B2C environment created earlier.
Project B2C Domain - Part 2
- Azure AD B2C : While in Project B2C Domain, go to Azure AD B2C
- Identity providers:
- Create New OpenID Connect for Each Synetec/Client Domain
- Name (Example): Synetec Microsoft Entra ID
- Metadata url: https://login.microsoftonline.com/{Domain Url}/v2.0/.well-known/openid-configuration
- Client ID: Client ID of the respective App registration created in the Synetec/Client Domain
- Client Secret: Client Secret of the respective App registration created in the Synetec/Client Domain
- Scope: openid profile
- Response type: code
- Response mode: form_post
- User ID: oid
- Display name: name
- Given name: given_name
- Surname: family_name
- Email: email
- Edit Recently Created Sign Up User flow:
- Identity Providers
- Local accounts: None
- Custom identity providers: Enable the relevant OpenID Connect created earlier (e.g. Synetec Microsoft Entra ID)
- API connectors:
- Before creating the user: Select recently created API connector (e.g. … API Prod - Validate SignUp)
- Identity Providers
- Edit Recently Created Sign In User flow:
- Identity Providers
- Local accounts: None
- Custom identity providers: Enable the relevant OpenID Connect created earlier (e.g. Synetec Microsoft Entra ID)
- Identity Providers
- Identity providers:
3. C# API Integration
Install the NuGet
Install-Package Synetec.UserManager
Update appsettings.json
Add the following json configuration to the appsettings.json file and fill in the values
"UserManager": {
"AzureAdB2C": {
"Instance": "",
"Domain": "",
"ClientId": "",
"TenantId": "",
"SignUpSignInPolicyId": "",
"ResetPasswordPolicyId": "",
"EditProfilePolicyId": ""
},
"Database": {
"ConnectionString": ""
},
"Email": {
"DefaultFromEmail": "",
"ApiKey": "",
"DefaultToEmail": "",
"SignUpEmail": {
"Template": "",
"Subject": "",
"FromEmail": "",
"Url": ""
},
"ChangedEmailTemplate": {
"Template": "",
"Subject": "",
"FromEmail": "",
"Url": ""
}
},
"SignUpJwt": {
"Issuer": "",
"Audience": "",
"Secret": ""
}
},
Update Program.cs
Add the following code to the Program.cs file.
var builder = WebApplication.CreateBuilder(args);
//...
builder.Services.AddUserManager(builder.Configuration);
//...
var app = builder.Build();
//...
app.AddUserManagerEndpoints();
//...
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<UserManagerDbContext>();
await dbContext.Database.MigrateAsync();
}
4. Front-End Integration (Angular)
Note that this is just one example implementation of how to do it. Feel free to adapt the code to fit your project’s needs.
Install the MSAL package
npm install @azure/msal-angular @azure/msal-browser
Configure the environment.ts
export const environment = {
//...
mainApi: {
url: "__MAIN_API_URL__",
azureApplicationId: "__MAIN_API_AZURE_APPLICATION_ID__",
dbApplicationId: "__MAIN_API_DB_APPLICATION_ID__",
scopes: ["__MAIN_API_SCOPES__"],
authRequiredPaths: ["__MAIN_API_AUTH_REQUIRED_PATHS__"],
},
azureB2C: {
tenantId: "__AZURE_B2C_TENANT_ID__",
clientId: "__AZURE_B2C_CLIENT_ID__",
b2cDomain: "__AZURE_B2C_DOMAIN_NAME__",
signInPolicy: "__AZURE_B2C_SIGN_IN_POLICY__",
baseScopes: ["__AZURE_B2C_BASE_SCOPES__"],
redirectUri: "__AZURE_B2C_REDIRECT_URI__",
},
userManagerApi: {
url: "__USER_MANAGER_API_URL__",
azureApplicationId: "__USER_MANAGER_API_AZURE_APPLICATION_ID__",
scopes: ["__USER_MANAGER_API_SCOPES__"],
authRequiredPaths: ["__USER_MANAGER_AUTH_REQUIRED_PATHS__"],
},
//...
};
Configure MSAL files
-
url.helper.ts
/** * Removes leading and trailing slashes from a given URL or path string. * * @param {string} path - The URL or path string to be trimmed. * @returns {string} - The input string with leading and trailing slashes removed. */ static trimSlashes(path: string): string { return path.replace(/\/+$/, '').replace(/^\/+/, ''); } /** * Joins multiple path segments into a single path string. * * @param {string[]} segments - An array of path segments to join. * @param {boolean} ensureTrailingSlash - Whether to add a trailing slash to the result. * @returns {string} - The combined path. */ static buildPath(ensureTrailingSlash = false, ...segments: string[]): string { let fullPath = segments.map(this.trimSlashes).join('/'); if (ensureTrailingSlash) { fullPath = this.ensureTrailingSlash(fullPath); } return fullPath; } static ensureTrailingSlash(path: string): string { return path.endsWith('/') ? path : `${path}/`; } -
msal-utils.ts
export function isBrowserIE(): boolean { return ( window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1 ); } export function getAuthorityUrl(policy: string): string { return UrlHelper.buildPath( true, `https://${environment.azureB2C.b2cDomain}.b2clogin.com`, environment.azureB2C.tenantId, policy.toLowerCase(), "v2.0" ); } export function addApiPathsToResourceMap( protectedResourceMap: Map<string, Array<string>>, apiUrl: string, authRequiredPaths: string[], applicationId: string, scopes: string[], b2cDomain: string ) { // For all API paths that require authentication, add the list of scopes to the protectedResourceMap authRequiredPaths.forEach((path) => { const fullPath = UrlHelper.buildPath(false, apiUrl, "_", path, "_"); protectedResourceMap.set( fullPath, scopes.map((scope) => getScope(b2cDomain, applicationId, scope)) ); }); } export function getScope( b2cDomain: string, applicationId: string, scope: string ): string { return UrlHelper.buildPath( false, `https://${b2cDomain}.onmicrosoft.com`, applicationId, scope ); } -
msal-guard-config.factory.ts
export function MSALGuardConfigFactory(): MsalGuardConfiguration { return { interactionType: InteractionType.Redirect, authRequest: { scopes: environment.azureB2C.baseScopes, }, }; } -
msal-initialize.factory.ts
export function MSALInitializeFactory( msalService: MsalService ): () => Promise<void> { return () => msalService.instance.initialize(); } -
msal-instance.factory.ts
export function MSALInstanceFactory(): IPublicClientApplication { const isIE = isBrowserIE(); const authority = getAuthorityUrl(environment.azureB2C.signInPolicy); return new PublicClientApplication({ auth: { clientId: environment.azureB2C.clientId, authority: authority, redirectUri: environment.azureB2C.redirectUri, knownAuthorities: [`${environment.azureB2C.b2cDomain}.b2clogin.com`], }, cache: { cacheLocation: BrowserCacheLocation.LocalStorage, storeAuthStateInCookie: isIE, }, }); } -
msal-interceptor-config.factory.ts
export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration { const protectedResourceMap = new Map<string, Array<string>>(); // Add the paths that require authentication for the Microsoft Graph API protectedResourceMap.set("https://graph.microsoft.com/v1.0/me", [ "user.read", ]); // Add the paths that require authentication for the Main API addApiPathsToResourceMap( protectedResourceMap, environment.mainApi.url, environment.mainApi.authRequiredPaths, environment.mainApi.azureApplicationId, environment.mainApi.scopes, environment.azureB2C.b2cDomain ); // Add the paths that require authentication for the User Manager API addApiPathsToResourceMap( protectedResourceMap, environment.userManagerApi.url, environment.userManagerApi.authRequiredPaths, environment.userManagerApi.azureApplicationId, environment.userManagerApi.scopes, environment.azureB2C.b2cDomain ); return { interactionType: InteractionType.Redirect, protectedResourceMap, }; } -
msal-auth-module.ts
@NgModule() export class MsalAuthModule { static forRoot(): ModuleWithProviders<MsalAuthModule> { return { ngModule: MsalAuthModule, providers: [ { provide: APP_INITIALIZER, useFactory: MSALInitializeFactory, deps: [MsalService], multi: true, }, { provide: MSAL_INSTANCE, useFactory: MSALInstanceFactory, }, { provide: MSAL_INTERCEPTOR_CONFIG, useFactory: MSALInterceptorConfigFactory, }, { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true, }, { provide: MSAL_GUARD_CONFIG, useFactory: MSALGuardConfigFactory, }, MsalService, MsalGuard, MsalBroadcastService, ], }; } } -
app.module.ts
@NgModule({ declarations: [AppComponent], bootstrap: [AppComponent, MsalRedirectComponent], //Add MsalRedirectComponent in the bootstrap imports: [ //... MsalAuthModule.forRoot(), //Add the Msal Auth Module from above ], providers: [ //... ], }) export class AppModule {} -
index.html
<body> <app-root></app-root> <app-redirect></app-redirect> <!-- Add app-redirect element show above--> </body> -
app.component.ts
constructor( @Inject(MSAL_GUARD_CONFIG) private readonly msalGuardConfig: MsalGuardConfiguration, private readonly authService: MsalService, private readonly msalBroadcastService: MsalBroadcastService ) {} async ngOnInit(): Promise<void> { this.msalBroadcastService.inProgress$ .pipe( filter( (status: InteractionStatus) => status === InteractionStatus.None ), takeUntil(this._destroying$) ) .subscribe(() => { this.setIsLoggedIn(); }); } setIsLoggedIn() { const allAccounts = this.authService.instance.getAllAccounts(); this.isLoggedIn = allAccounts.length > 0; if (this.isLoggedIn) { const activeAccount = allAccounts[0]; this.authService.instance.setActiveAccount(activeAccount); this.name = activeAccount.name ?? ''; } }
5. Closing Thoughts
Please be aware that both the UserManager NuGet package and this guide are continuously evolving, so staying informed on updates is essential. For a deeper understanding and optimal integration, refer to Microsoft’s documentation. This will help you adapt UserManager to your project’s unique requirements and ensure alignment with the latest best practices.