Login Service

Introduction

The Login service is a fundamental pilar that complements MAGES SDK.

As briefly described in the introductory section, all traffic from and to the VR module goes through this service.

Namely, the core functionalities that Login provides are the following:

  • User management

  • User authentication & authorization

  • Product management & licensing system

  • SingleSignOn (SSO) capabilities

Essentially, Login is an identity provider and access controller.

In this section we are going to explore how you can utilize the service for your needs, starting with the basics.

First, we are going to discuss how Login is packaged into a sample application, and proceed to outline the technical details (e.g., frameworks, depending third-party libraries, etc.).

Then, we will proceed to show how you can configure the Login service, run it, and deploy it on the Azure infrastructure (Cloud provider).

Finally, we will explain how you can customize it to serve your needs.

Note

If you wish to directly run the service without going over the details and explanations scroll down to Development Environment.

Sample App

The Login service is packaged as a Visual Studio 2019 project.

In this manner, we enable developers to directly configure and customize the codebase to tailor their needs.

Besides customization to the maximum, our solution is fully transparent as to what it does under the hood with the organization acquiring it.

Furthermore, it is easier for customers to explore alternative ways of solving the problems, and perhaps extending it to fit Cloud providers of their choice.

Requirements

At its core, Login service is a full-stack .NET Core 3.1 MVC web application including a RESTful API for HTTP calls.

Therefore, it is required that you download the latest .NET Core 3.1 SDK, and update your Visual Studio to 2019 version.

Additionally, the service itself has a series of 3rd party open-source dependencies that you can install through the NuGet Package Manager through Visual Studio.

Package

Description

IdentityServer4

IdentityServer4 is responsible for OpenIDConnect and OAuth2 support, as well as identity management.

MailKit

MailKit is responsible for the EmailService. In other words, you can configure it to send emails to your clients such as Account Confirmation, or Reset Password functionalities.

EF Core

EF Core is Microsoft’s object-relational mapper, particularly useful to work with databases and treat them as .NET Objects.

ASP.NET Core Identity

ASP.NET Core Identity is Microsoft’s user management package that enables user management, role, claims, tokens and everything user-related as well as a lot of out-of-the-box functionalities.

Note

You don’t have to manually download or install required NuGet packages. Visual Studio handles this for you.

In any case, you can follow one of the methods below:

  1. Right click on the Solution in the Solution Explorer and select Restore NuGet Packages

  2. Right click on the Solution in the Solution Explorer and select Manage NuGet Packages for Solution…. Then, you can install them one-by-one via GUI.

Basic Configuration

Now let’s dive into the basics.

In case you haven’t already, proceed to open the Visual Studio project and load the NuGet packages.

Open the Startup.cs file, in the root project folder.

Startup.cs

Startup is perhaps the most fundamental class in an ASP.NET Core project.

In this file we configure all services via Dependency Injection (DI) that will be used at runtime.

Also, in Startup we provide configurations for 3rd party packages, routing, middlewares, authentication, etc.

Read more about App startup.

Constructor

As with any C# class, Startup starts with the constructor. Take a look at the one below:

public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(environment.ContentRootPath)
        .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
        .AddJsonFile("appsettings.Development.json", optional: true);
    builder.AddEnvironmentVariables();

    Environment = environment;
    Configuration = builder.Build();
}

In the above snippet, all we do is specify a few of the basics regarding the environment and configuration.

Based on the ASPNETCORE_ENVIRONMENT environment variable and whether its value is Development or Production a different appSettings.json loads.

Note

The appSettings.json is responsible for runtime configurations and we explore it further down.

Configure Services

In the Configure Services function we provide the specifics for all services and the connection blocks for external ones.

More importantly, in this function we declare all services that can be injected (DI) to other modules (i.e., Controllers, Services, etc.) at run-time.

Read more about DI in ASP.NET Core here.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
public void ConfigureServices(IServiceCollection services)
{
    string connectionString = Configuration.GetConnectionString("Identity");
    var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

    services.AddControllersWithViews();

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(connectionString));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    services.Configure<IdentityOptions>(options =>
    {
        options.Lockout.MaxFailedAccessAttempts = 20;
        // Password Settings.
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireDigit = false;
        options.Password.RequireUppercase = false;
        // Email Settings.
        options.User.RequireUniqueEmail = true;
        // SignIn Settings.
        options.SignIn.RequireConfirmedEmail = true;
    });

    services.Configure<CookiePolicyOptions>(options =>
    {
        options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
        options.Secure = CookieSecurePolicy.SameAsRequest;
    });

    var builder = services.AddIdentityServer(options =>
    {
        options.Events.RaiseErrorEvents = true;
        options.Events.RaiseInformationEvents = true;
        options.Events.RaiseFailureEvents = true;
        options.Events.RaiseSuccessEvents = true;

        // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
        options.EmitStaticAudienceClaim = true;
    })
        .AddConfigurationStore(options =>
        {
            options.ConfigureDbContext = b =>
            {
                b.UseSqlServer(connectionString, m =>
                m.MigrationsAssembly(migrationsAssembly));
            };
        })

        .AddOperationalStore(options =>
        {
            options.ConfigureDbContext = b =>
            {
                b.UseSqlServer(connectionString, m =>
                m.MigrationsAssembly(migrationsAssembly));
            };
        })
        .AddAspNetIdentity<ApplicationUser>();

    builder.AddDeveloperSigningCredential();

    services.AddAuthentication()
        .AddGoogle(options =>
        {
            options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
            options.Scope.Add("email");
            options.ClientId = "Your Client Id";
            options.ClientSecret = "Your Secret";
        });

    services.AddLocalApiAuthentication();

    services.AddAuthorization(options =>
    {
        options.AddPolicy("Admin", policy =>
        {
            policy.RequireAuthenticatedUser();
            policy.RequireClaim("role", "Admin");
        });
        options.AddPolicy("Supervisor", policy => {
            policy.RequireAuthenticatedUser();
            policy.RequireClaim("role", "Supervisor");
        });
        options.AddPolicy("User", policy =>
        {
            policy.RequireAuthenticatedUser();
            policy.RequireClaim("role", "User");
        });
    });

    services.AddHostedService<BackgroundCheckoutService>();
    services.AddScoped<IProfileService, ProfileService>();
    services.AddScoped<LicenseValidationService>();
    services.AddSingleton<IEmailConfiguration>(Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>());
    services.AddTransient<IEmailService, EmailService>();

    services.AddMvc(option => option.EnableEndpointRouting = false)
        .SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
        .AddNewtonsoftJson(opt => opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
}

Let’s take a look at the important bits on a configuration-basis.

DbConfiguration

Between lines 8-9 we specify the Database Context (i.e., ApplicationDbContext) that points to our SqlServer. For this we need the connection string, whether it is a local database or a database on Azure, that we obtain from the AppSettings.json file in line 3

We use the same database for the Configuration and Operational store required by IdentityServer4 as shown between lines 44-60.

Identity & IdentityServer4

For user management we will be using ASP.NET Identity which provides out-of-the-box a user management store, a signin manager, role-based authorization, and more.

First, we proceed to include and configure ASP.NET Identity to our project in lines 11-13.

Then, we need to specify the user class model that represents our final users. This is the ApplicationUser model included under the Models/ directory. See how ApplicationUser extends the original IdentityUser and introduces more properties that we want to keep track of.

For instance, the Country of origin for the user, and the Tenant (i.e., Organization) the user belongs to.

In addition, we have to specify the database that will store our users. In this case, this the ApplicationDbContext SQL database we configured in the previous paragraph.

Further down, between lines 15 and 26 we specify some Identity options for user management.

For example, we specify that each user should have a unique email address (prevents duplication of user accounts on an email basis), and that we do not require any special characters in the password (consider altering this to prevent users from using weak passwords).

In lines lines 34-61 we proceed to add IdentityServer4 to our services. There we specify the Configuration and Operational store, which is the same database in practice, and more importantly we specify that IdentityServer4 will support out-of-the-box AspNetIdentity and the ApplicationUser we declared earlier on.

Role-based Authorization

Role-based authorization is an important part of user management.

Essentially, we provide different level of access scopes to different users based on their associated roles. AspNetIdentity supports role-based authorization out-of-the-box.

That being said, in lines 76-92 we define three different policies.

  1. Admin

  2. Supervisor

  3. User

Role-based access works in an inclusive and not in an exclusive manner.

In other words, an Admin has also the roles of Supervisor and User. A supervisor, has also the role of User. And finally, the User only contains User.

This means that Admins can access all available functionality – scope-wise, whilst the other two are restricted.

For example, deleting a user through an API call is only available if the user who invokes the API call has the role of an Admin. Therefore, this functionality is Unauthorized (401) for Supervisors and Users.

SingleSignOn (SSO)

As an example for SSO support, we provide the snippet between lines 67-72.

There you need to fill in the ClientId and ClientSecret that you obtained from Google, to enable access to users to your service through their Google accounts.

Note

There is also support for other services, but you will have to configure that manually.

Read more about it at Google.

Services

Finally, we have to declare our own custom services in a similar manner to provide DI support at runtime. In this manner we also configure the lifetime per service.

In particular, between lines 94-98 we configure the services found under /Services directory.

Pay attention to how services have different lifetimes depending on the declaration.

For instance, the EmailConfiguration is declared as a Singleton and the lifetime is the same as the application and wherever it is injected, e.g., in some controller, the same object is injected.

On the other hand, the EmailService itself is declared as a transient. This means that wherever EmailService is injected throughout an HTTP request, EmailService will be reinstantiated for this exact declaration. Finally, scoped services live throughtout a single HTTP request.

More information for service lifetimes.

Configure

The Configure method specifies the request pipeline, in other words, the middlewares involved whenever a request reaches the application.

In the snippet below we specify a typical Configure method:

public void Configure(IApplicationBuilder app)
{
    app.UseCookiePolicy(new CookiePolicyOptions {
        MinimumSameSitePolicy = SameSiteMode.None
    });

    app.UseCors(options =>
    {
        options.AllowAnyOrigin();
        options.AllowAnyHeader();
        options.AllowAnyMethod();
    });

    if (Environment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseDatabaseErrorPage();
    }

    app.UseStaticFiles();

    app.UseRouting();
    app.UseIdentityServer();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapDefaultControllerRoute();
    });
}

The code in the snippet is pretty much self-explanatory, we the only difference that the order middlewares are specified in the request pipeline matters.

For instance, app.UseEndpoints function is typically the last one to be declared after authorization, routing, and the rest of the middlewares have been set to ensure traffic goes to specified controllers.

Read more about the Configure method.

MVC

MVC ASP.NET Core applications are based on the Model-View-Controller pattern, which is perhaps the easiest and most fundamental pattern involving UI, data-layers, and business logic under one unified application.

The Login service effectively utilizes this pattern, and specifically for login flows and operations that have to be done on-site.

For example, the common Login page where users log into the service is part of the MVC with the structure:

  1. Models are under Controllers/Account/*

  2. Controller is under Controllers/Account/AccountController.cs

  3. Views are under Views/Account/*

You can observe that the AccountController supports more than one functionality, and as the name suggests, operations that naturally are associated with Users and their accounts.

For instance, through the AccountController we support the following functionalities:

  • User Login

  • User Registration

  • Forgot Password

  • Reset Password

  • etc.

While each of these operations are bundled for efficiency under a single Controller, almost everyone necessitates a different View. The same holds true for the Models.

Note

The AccountsController inherits from the base class Controller.

Controller

Routing

Routing for an MVC Controller, unless explicitly specified, inherits the controller’s name.

For example, if your service is running at http://localhost:5002, then AccountController routes at:

  • http://localhost:5002/Account

In addition, routing to a specified Action (i.e., public Controller functions that represent HTTP requests), unless explicitly specified, is as follows:

  • http://localhost:5002/Account/{ActionName}

For instance, to navigate to the Login page you need to enter the following endpoint at your browser:

  • http://localhost:5002/Account/Login

HTTP Methods

Moreover, HTTP verbs are defined as attribute on top of Actions.

As an example, take the Login Action below:

[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
    var vm = await BuildLoginViewModelAsync(returnUrl);
    if (vm.IsExternalLoginOnly)
    {
        return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
    }

    return View(vm);
}

See that the specified Action is declared as a GET method, and will return its associated View (i.e., Login.cshtml).

On the other hand, we can also proceed to specify the POST Login method in a similar manner:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
    // Some code that actually handles the LoginInputModel and logs in a User.
}

Note

Notice that both Actions have the same name and return types, only Action parameters change.

Therefore, a typical flow would normally start from the GET Login at the browser, where the Login screen is returned to the User, and finish when the user enters his credentials at the displayed form, which will be posted at the POST Login Action.

Authorization

Level of access can be declared for the whole Controller, or can be added on a per-action basis. Typically you can mix and match policies inside the Controller, according to your needs.

In the AccountController above, there is no globally defined level of access. Therefore, Actions such as Login and Register inherit by default the [AllowAnonymous] attribute and can be accessed through unauthenticated Users, which makes sense.

On the other hand, Actions as is the ChangePassword are allowed only for logged in Users who have at least the role "User" in their claims.

See the example below:

[HttpGet]
[Route("account/password")]
[Authorize(Roles = "User")]
public IActionResult ChangePassword(string returnUrl = null)
{
    return View();
}

Note

Observe also how this particular Action has a different routing defined and is accessible at /account/password.

Model & View

In the code snippet above that involves the POST login Action, notice how the specified input model is provided as a parameter.

Further, take a look at the associated login View below:

@model LoginViewModel

<div class="login-page">
    <!-- rest of the elements -->

    <!-- FORM -->
    <form asp-route="Login">
        <input type="hidden" asp-for="ReturnUrl" />
        <div class="form-group">
            <label asp-for="Username"></label>
            <input class="form-control" placeholder="Username" asp-for="Username" autofocus>
        </div>
        <div class="form-group">
            <label asp-for="Password"></label>
            <input type="password" class="form-control" placeholder="Password" asp-for="Password" autocomplete="off">
        </div>
        @if (Model.AllowRememberLogin)
        {
            <div class="form-group">
                <div class="form-check">
                    <input class="form-check-input" asp-for="RememberLogin">
                    <label class="form-check-label" asp-for="RememberLogin">
                        Remember My Login
                    </label>
                </div>
            </div>
        }
        <div class="row mt-4">
            <div class="col-md-12 mb-3 text-center">
                <button class="google-button" name="button" value="login">
                    <span class="google-button__icon">
                        <img src="~/VR.png" class="oramavr-button-icon" />
                    </span>
                    <span class="google-button__text oramavr-button">Sign in with ORamaVR</span>
                </button>
            </div>
        </div>
    </form>
</div>

Let’s focus on the important bits.

First, notice how the linked model LoginViewModel is declared on the top of the page.

Second, notice how the form is associated with the asp-route="Login" action.

Finally, each form-control input is associated with the appropriate property of our data model.

API Controllers

On the other hand, we have the ApiControllers to provide an HTTP RESTful API for User and Product management through the Portal.

ApiControllers are under /Controllers/API/* directory.

The main difference is of course that an ApiController is not associated with any View, and does not serve HTML pages to users, but rather HTTP Responses.

In terms of code, here is a following example from the UsersController:

[Route("api/[controller]")]
[ApiController]
[Authorize(IdentityServerConstants.LocalApi.AuthenticationScheme)]
public class UsersController : ControllerBase
{
    private readonly ApplicationDbContext _context;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly RoleManager<IdentityRole> _roleManager;


    public UsersController(
        ApplicationDbContext context,
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager)
    {
        _context = context;
        _userManager = userManager;
        _roleManager = roleManager;
    }

    // ----------------------------------------------------------- GET -----------------------------------------------------------
    [HttpGet]
    [Authorize(Policy = "Admin")]
    public async Task<IActionResult> Get()
    {
        var users = await _userManager.Users.ToListAsync();
        if (users == null)
        {
            return NotFound();
        }

        return Ok(users);
    }

    // Rest is omitted for simplicity
}

Compared to MVC

The first few obvious differences regarding UsersController are the following:

  • Routing is explicitly specified at /api/Users to differentiate from MVC Controllers.

  • The [ApiController] attribute is utilized that provides certain features.

  • And the [Authorize(IdentityServerConstants.LocalApi.AuthenticationScheme)] is also utilized to ensure Clients who make API calls have the registered scoped.

  • The controller itself inherits from ControllerBase this time.

Apart from those differences, let’s take a look at the first GET functionality which returns all Users to the caller.

First, in this controller you can make sense of the DI pattern we discussed earlier.

In detail observe how :code`ApplicationDbContext, UserManager, RoleManager` are injected into the Constructor of the UsersController for use throughout the application.

Then, take a look at the specific GET call.

In this case, we have specifically declared that this is an HTTP GET call through the use of the [HttpGet] attribute.

Moreover, we have restricted access to Admins only through the [Authorize(Policy = "Admin"] attribute.

Note

We have not defined any explicit routing for this method. Therefore, requests fired at http://{service}/api/Users will invoke this method.

If the User is not authorized to do so, an HTTP 401 Unauthorized response will return to the client.

What it does

Essentially, the Get() call querys the database and returns a list of all ApplicationUser users. In this case, we could have also used the ApplicationDbContext => _context to query the database, but we prefer to utilize the UserManager API provided by AspNet.Identity for user related operations.

Now, if users are null, an HTTP 404 NotFound response is sent back to the client.

In all other cases, an HTTP 200 Ok response with the list of users is returned.

Note

You can access and actively test all available API calls through Postman Client. But remember to authorize yourself first.

Database Migrations

Introduction

We utilize Entity Framework Core (EF Core) as an object-relational mapper (O/RM).

This enables us to work directly with .NET objects that relate directly to our database tables and the underlying schema.

EF Core has two ways of managing the database schema.

In our case we utilize the code-first approach and work with Data Migrations to keep the database up-to-date.

Simply speaking, if we add a new property to the ApplicationUser we can add a new migration to that will update the database table accordingly.

The Login service has already certain migrations for your needs that correspond to all models found in the database.

Current database models correspond to the ones under Models/* directory and have certain attributes that define relationships between other objects.

In addition, certain relationships have to be defined at the ApplicationDbContext with the OnModelCreating method, as attributes do not cover more complex relationships.

Add New Migration

You can add a new migration through the NuGet Package Manager console simply as follows:

Add-Migration {MigrationName} -Context ApplicationDbContext -OutputDir "/Migrations/ApplicationDb/"

Moreover, if the above command builds successfully, you can proceed to update the database with the following command:

Update-Database

You can also skip this step and read the next section.

Existing Data and Applying Migrations

For convience and ease of use, we provide certain data that will be applied the first time you run the Login Servive.

The code will also ensure that the database is created and all migrations are applied.

This code lies onto the Program.cs file under the root project directory.

Particularly, this code is inside the Main method and will be invoked as soon as you start running the service and access one of the endpoints or do an API call.

Responsible for this is the helper class is SeedDataHelper under Helpers/* directory.

The helper class will make sure to add certain dummy users and organizations, as well as a few products and link them with the tenants.

More importantly, it will create all necessary Clients, ApiResources and Scopes required for IdentityServer4 and the services (AnalyticsAPI, Portal, VR module) to communicate.

The initial data for IdentityServer4 resources are under Config.cs.

Danger

It is strongly suggested that you alter the ClientSecrets for each Client, especially in Production database.

Getting Ready for Development

Assuming you read the previous sections and obtained a basic understanding of how everything works, in this section we will provide all necessary configurations you have to follow before you start play-testing around the Login service.

1. Local SQL Database

First you need to create a local SQL Database.

The easiest way to do so is with SQL Server Express LocalDB. There are two options to install LocalDB onto your machine.

  1. One is to download and install separately from the link above.

  2. Through Visual Studio Installer as described in Installation Media.

Then, you can manage the SQL Connection to the LocalDB through Visual Studio 2019.

To do so, click on the top toolbar View and then click on Sql Server Object Explorer.

This will open a panel as follows:

SQL Server Object Explorer

On the top bar click on the Add SQL Server button and add select from the Local list the LocalDB SQL Server as in the figure below:

SQL Server Object Explorer

Click connect and proceed to expand the SQL Server tree structure on the SQL Server Object Explorer as follows:

SQL Server Object Explorer Expanded

Right click on the Databases icon and select the Add New Database option.

Add new Database

Give a descriptive name for your Database (something like IdentityDb) and click Ok.

Now you have created your Database but it is empty. Don’t worry we will populate it soon, keep reading!

2. App Settings

DB Connection String

After the database is set you need to obtain the connection string.

The easiest to obtain the connection string is through the SQL Server Object Explorer.

Click on the database, and then on the Properties window you will find the Connection String as in the figure below.

Connection String

Proceed to open the file appSettings.Development.json and paste it inside the Connection Strings/Identity as in the example snippet below:

"ConnectionStrings": {
    "Identity": "Server=(localdb)\\mssqllocaldb;Database=IdentityServer;Trusted_Connection=True;MultipleActiveResultSets=true"
}

Email Configuration

While in the appSettings.Development.json file, proceed to also include the configuration for your EmailService.

This is needed for user account operations that necessitate sending an email to the User.

For instance, forgot password functionality.

"EmailConfiguration": {
  "SmtpServer": "",
  "SmtpPort": 587,
  "SmtpUsername": "",
  "SmtpPassword": "",
  "PopServer": "",
  "PopPort": 995,
  "PopUsername": "",
  "PopPassword": ""
}

If you need to configure extra TLS or other security options, you can do so in the EmailService.cs file under Services/ directory.

Optional Step. You can configure the email from, subject, and content fields in the AccountController and UsersController, where the EmailService is invoked and Emails are sent.

3. (HTTPS) Self-signed Certificate

Now that all configurations are set, we need to set a self-signed developer certificate before running our service.

This is because IdentityServer4 by default prompts to encrypted traffic (HTTPS) and in production won’t work otherwise.

So for development purposes, Visual Studio and IIS will generate a self-signed certificate for us.

First, right-click on the Project and click on the Properties option.

Connection String

Scroll down and make sure the Enable SSL checkbox is ticked! If it is not, check it.

Note

This is also the default development environment endpoint to access the service https://localhost:44355.

When you check it for the first time, a window prompt will appear asking you to create a self-signed certificate that will be stored on your computer.

Click Agree on all dialogs, and you are set!

4. Start the Service

Assuming you followed all previous steps successfully and everything is set, you are ready to hit the start button.

Note

Make sure IIS profile is selected and that Debug mode is set on the top bar.

Hitting the Start button will fire up Login service at the specified endpoint in Step 3.

https://localhost:44355

When you point your browser the first time at this URL, it might be a bit slower than expected. This happens mainly due to two reasons:

  1. ASP.NET Core slow-start

  2. The SeedDataHelper.cs script will be invoked and subsequently:

2.1. Will ensure all Tables are initialized.

2.2. Migrations are applied.

2.3. First dummy data will be inserted to the database.

After this cold start you will be navigated to the Login page. Login with one of the predifined users at SeedDataHelper.cs and then you can go back to the Home page and see the default page of IdentityServer4 which is useful for debugging user claims, etc.

Note

The Default HomePage of IdentityServer4 is only for the development environment. This page will not be visible when you deploy to Azure.

If everything went fine at this point, you are set. Most likely you won’t experience any strange behaviors.

Note

Make sure you test thoroughly all supported functionality (e.g., registration page, forgot password, etc.).

Getting Ready for Production

Assuming you followed all steps in the previous section, and everything is working perfect locally, it’s about time to deploy.

Below we outline all necessary steps and precautions before you do so.

Warning

The deployment environment is configured for Azure Cloud provider.

1. App Settings

If you recall from the previous section, we modified the appSettings.Development.json file for working locally.

This time, we will have to modify in the same manner appSettings.json for use in the deployment.

As before, proceed to insert Database connection string in the ConnectionStrings: Identity, this time with the connection string from the live database in Azure.

Note

(Optional), You can download SQL Server Management Studio (SSMS) to actively monitor your databases and input data.