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

sequenceDiagram participant Admin as Existing Admin participant User as New User participant FrontEnd as Frontend Web participant API as Backend API participant B2C as Azure B2C Admin ->> API: Enrols new user as invited API ->> User: Sends invite email to new user User ->> FrontEnd: Clicks email link FrontEnd ->> API: Checks link validity & if user is already registered API -->> FrontEnd: Returns token validity & registration status FrontEnd ->> B2C: Redirects user to B2C sign-up page User ->> B2C: Completes B2C sign-up & validates email B2C ->> API: Checks if user exists in database as invited API -->> B2C: Confirmation of user existence and status B2C -->> FrontEnd: Redirects with valid token after sign-up completion


User Sign-In Sequence Diagram

sequenceDiagram participant User participant FrontEnd as Frontend Web participant B2C as Azure B2C participant API as Backend API User ->> FrontEnd: Requesting Sign In FrontEnd ->> B2C: Redirects to B2C Sign-In User ->> B2C: Enters credentials B2C -->> FrontEnd: Returns token FrontEnd ->> API: Call Secure Endpoint & Sends token API ->> B2C: Validates token API -->> FrontEnd: Returns success


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.Notification NuGet

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

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)
    • 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)

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.