Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 1

Whatever end of the software development stack you spend the majority of your time in, if you're building a modern web or mobile application in 2019, you've at least heard of or are actively working with GraphQL in some capacity.

Originally created and open-sourced by Facebook back in 2015 its rapid adoption saw it move to its own foundation in 2018 to be maintained by The Linux Foundation technology consortium.

Its become popular due to its ability to simplify client-server interaction while improving the developer experience and productivity. These benefits are a result of reducing the amount of friction and complexity front-end, and back-end teams face while trying to build critical data integrations between application UIs and backend APIs. With such a high emphasis on flexibility and speed to market in today's software solutions, GraphQL is taking preference over traditional REST APIs for these reasons.

Last year I did an introductory post on using GraphQL dotnet with ASP.NET Core. That article still generates some questions from folks involving more real-world use cases that require things like user authentication and UI integration with a given front-end web or mobile technology.

With the recent release of ASP.NET Core 3 and continued interest in GraphQL since my original post, it's a great time to look at a complete example that includes an Angular front-end along with IdentityServer on the back-end to build a fully integrated, modern GraphQL solution.

There's a bit to cover here, so as you may have gathered from the title, this will be part one of a multi-part series.

To start, we'll begin the project on the back-end by building out an authentication and identity server using IdentityServer4.

So, grab a refill of your favorite beverage and let's get to it! β˜•οΈπŸΊπŸ·

Posts in This Series

  • Part 1: Building an authentication and identity server with IdentityServer πŸ‘ˆ You're here!
  • Part 2: Angular app foundation with user signup and login features
  • Part 3: Implementing an ASP.NET Core GraphQL API with authorization using GraphQL .NET
  • Part 4: Integrating Angular with a backend GraphQL API using Apollo Client
Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

The Demo App

FullStack Jobs is a simple yet mighty little job board we're going to build and includes the following functionality:

  • User signup and login flow using Proof Key for Code Exchange (PKCE)
  • Support for different types of user roles. i.e., Employers and Job Seekers
  • Authenticated calls to the GraphQL API to ensure users can only access the data they're authorized for
  • CRUD functionality for managing job postings
  • The ability for job seekers to apply for posted jobs
Job application flow.
Job application flow.
Editing a job posting.
Editing a job posting.

Development Environment

As of December 2019, this guide was created using the most recent versions of:

  • .NET Core 3.1
  • Visual Studio Code
  • Visual Studio 2019 Professional for Windows
  • SQL Server Express 2016 LocalDB
  • Node.js with npm
  • Angular CLI

Running the Solution

See the repository's readme for the steps required to build and run the demo application.

Solution Overview

Here's a diagram showing the major pieces of our solution architecture along with the authentication and data flows that tie them together.

We'll take a detailed look at each of these components and their integrations as we work through building out the application. In this post, we'll be tackling the Identity / Authorization Server.

Auth Server Requirements

Before we look at the implementation, here are the key responsibilities of the Auth Server:

  1. As an OpenID Connect Provider, authenticate users and return access and identity tokens to the Angular client
  2. Use the access token to authorize requests to the OAuth protected GraphQL API
  3. Provide an endpoint to handle user registration from the Angular SPA via ASP.NET Core's Identity membership system

IdentityServer handles the first two as it is our OpenID Connect and OAuth framework. Functions for creating and managing user accounts are not part of IdentityServer as it is for authorizing users. But, for our purposes, it's convenient to include the user registration endpoint here as it will share the user database and ASP.NET Core and Entity Framework Core Identity bits we'll also use with the IdentityServer framework.

Liftoff πŸš€

To get started, I set up a brand new solution for the AuthServer. Next, I added a new ASP.NET Core Web Application project using the empty template followed by a new .NET Core 3.1 Class Library project that will house the data access and other infrastructure bits for IdentityServer which we'll be tackling next.

Finally, I created a new empty solution with a solution folder and added the two projects to it. This structure will allow us to add the GraphQL project down the road in the same fashion and make it easier for us to work with and run both backend projects simultaneously.

At this point, the solution looks something like this:

Infrastructure Layer

To set up the infrastructure project, I first nugetted the latest versions of the following packages:

  • IdentityServer4.EntityFramework
  • Microsoft.AspNetCore.Identity.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.Design
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.Extensions.Configuration.EnvironmentVariables
  • Microsoft.Extensions.Configuration.FileExtensions
  • Microsoft.Extensions.Configuration.Json

User Model

Next, I added a new AppUser class derived from ASP.NET Core's IdentityUser entity which will serve as the centerpiece of our user account model.

public class AppUser : IdentityUser
{
   // Add additional profile data for application users
   // by adding properties to this class
   public string FullName { get; set; }
}

Pretty simple class, however, because we're inheriting from IdentityUser, we're able to easily extend the default set of properties by adding a custom one for FullName. Because Identity makes use of an Entity Framework Core data model by default, this property will automatically map to a new database column in the AspNetUsers table, which we'll create shortly.

DbContext

ASP.NET Core Identity provides its own Entity Framework database context in IdentityDbContext and customization options by deriving from this type.

So, I added a new AppIdentityDbContext class and passed our AppUser type as a generic argument for the context.

public class AppIdentityDbContext : IdentityDbContext<AppUser>
{
   public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : base(options)
   {
   }

   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
     base.OnModelCreating(modelBuilder);
     // Customize the ASP.NET Identity model and override the defaults if needed.
     // For example, you can rename the ASP.NET Identity table names and more.
     // Add your customizations after calling base.OnModelCreating(builder);
     // https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-3.0
   }
}

This guy is pretty barebones too. For our demo, he'll remain that way; however, the additional configuration of Identity model types would go in the OnModelCreating method.

Entity Framework Core Migrations

With a data model and context created, we're ready to add a migration to translate this model into changes we can apply to the database.

Since our DbContext lives in a class library project, we need to give Entity Framework Core's Tools a little help in creating it before we can use any of the CLI commands to create and apply a migration. This is done by implementing the IDesignTimeDbContextFactory<TContext> interface within a class in the same project as the DbContext(s).

I added a new AppIdentityDbContextFactory for the context we created and PersistedGrantDbContextFactory for IdentityServer's operational data store support.

Finally, I added an appsettings.json file containing the database connection string.

We're now ready to create and apply the EF migrations using the CLI tools.

From the command prompt, I generated a migration for each context by running the following commands from the root of the FullStackJobs.AuthServer.Infrastructure project folder.

\> dotnet ef migrations add initial --context PersistedGrantDbContext
\> dotnet ef migrations add initial --context AppIdentityDbContext

After running the commands, I see a new Migrations folder in the root of the infrastructure project containing the generated migration files.

Finally, I used the database CLI command to generate the sql database by applying the migrations for each DbContext.

\> dotnet ef database update --context PersistedGrantDbContext
\> dotnet ef database update --context AppIdentityDbContext

After they completed, I popped open SQL Server Management Studio and found the newly created FullStackJobs database.

If you have a look at the columns in the AspNetUsers table you will see a FullName column which maps to the property we added earlier on the AppUser model.

AuthServer Web Application & API

With the database created, we're ready to begin adding some real functionality to the web application project we created earlier.

Accounts Controller

Before we can look at how to authenticate and authorize users, we need a mechanism to create new user accounts using ASP.NET Core's Identity membership API.

To handle this, I added a new AccountsController to the project and a single action method to handle the HTTP POST action.

[Route("api/[controller]")]
public async Task<IActionResult> Post([FromBody]SignupRequest model)
{
   if (!ModelState.IsValid)
   {
      return BadRequest(ModelState);
   }

   var user = new AppUser { UserName = model.Email, FullName = model.FullName, Email = model.Email };

   var result = await _userManager.CreateAsync(user, model.Password);

   if (!result.Succeeded) return BadRequest(result.Errors);

   await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("userName", user.UserName));
   await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("name", user.FullName));
   await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("email", user.Email));
   await _userManager.AddClaimAsync(user, new System.Security.Claims.Claim("role", model.Role));

   return Ok(new SignupResponse(user, model.Role));
}

The action method expects an incoming SignupRequest model sent as part of the request body.

From there, we initialize a new AppUser instance with the incoming values from the request object and then pass that as an argument along with the password the to CreateAsync method of the UserManager class. UserManager provides various APIs for managing users stored in the configured Identity database.

Finally, if account creation succeeds, we call on the UserManager again to add some custom claims to the user. We'll make use of these claim values a little further down the road as we add the authentication and integration layers between the Angular client and GraphQL API.

Writing an Integration Test

ASP.NET Core provides a slick integration test framework that allows you to write simple tests that ensure the different infrastructure components in your application like the database, filesystem, and request-response pipeline are all playing nicely together. The coolest part is that it uses HttpClient to allow you to poke at your API/Razor Page/View "from the outside" in a similar fashion as a mobile, SPA, or browser client would.

I spun up a new xUnit test project with a test class for the AccountsController and then wrote a simple test for the happy path of our account create method.

[Fact]
public async Task CanCreateAccount()
{
   var httpResponse = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "/api/accounts")
   {
      Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(_signupRequests[0]), Encoding.UTF8, "application/json")
   });

   httpResponse.EnsureSuccessStatusCode();

   // Deserialize and examine results.
   var stringResponse = await httpResponse.Content.ReadAsStringAsync();
   var response = JsonConvert.DeserializeObject<SignupResponse>(stringResponse);
   Assert.Equal(_signupRequests[0].FullName, response.FullName);
   Assert.Equal(_signupRequests[0].Email, response.Email);
   Assert.Equal(_signupRequests[0].Role, response.Role);
   Assert.True(Guid.TryParse(response.Id, out _));
} 

The real magic behind these tests comes from the WebApplicationFactory which provides a test server to host your application as well as an entry point to override any components or services registered in your app's Startup.ConfigureServices.

For example, we can use a different database in our tests instead of the app's configured database. The app's database context can be swapped in builder.ConfigureServices to use an in-memory version instead. I wrote a AuthServerWebApplicationFactory to do just that.

Authentication with IdentityServer

With the capability in place to create new users, we're ready to add a login mechanism to authenticate them. In the next set of steps, we'll add support for interactive user authentication via the OpenID Connect protocol using the Proof Key for Code Exchange flow (PKCE).

In my previous post exploring Angular authentication with IdentityServer we used the Implicit authentication flow. However, today, the recommendation from the OAuth working group when it comes to JavaScript-based browser applications is to use PKCE instead as it does not expose tokens in the client URL. For more information please check out Brock Allen's assessment of the changes to Implicit flow in OAuth2.

To start setting up IdentityServer, I installed the IdentityServer4.AspNetIdentity package via nuget. As the name implies, this package provides integration bits between ASP.NET Core Identity and IdentityServer.

Startup Configuration

Our first step when implementing IdentityServer is to configure it in the app's Startup class.

...
var builder = services.AddIdentityServer()
// this adds the operational data from DB (codes, tokens, consents)
.AddOperationalStore(options =>
{
  options.ConfigureDbContext = builder => 
  builder.UseSqlServer(Configuration.GetConnectionString(_connectionStringName));
  // this enables automatic token cleanup. this is optional.
  options.EnableTokenCleanup = true;
  options.TokenCleanupInterval = 30; // interval in seconds
  })
  .AddInMemoryIdentityResources(Config.GetIdentityResources())
  .AddInMemoryApiResources(Config.GetApiResources())
  .AddInMemoryClients(Config.GetClients())
  .AddAspNetIdentity<AppUser>();

if (Environment.IsDevelopment())
{
   builder.AddDeveloperSigningCredential();
}
else
{
   throw new Exception("need to configure key material");
}
...

Note, for development purposes, we're using a temporary key generated at startup as indicated by AddDeveloperSigningCredential. However, for production scenarios, you'll want to replace this with a real certificate. Refer to the docs for more details on how to configure key material.

Client Configuration

One of the most powerful features of IdentityServer IMHO is its ability to provide a single, centralized authentication and authorization solution that can be used across multiple applications by simply configuring a client to represent each application we'd like to interact with IdentityServer.

I added a test client in Config as a placeholder for now which we'll use shortly with a bare-bones JavaScript client to validate our implementation.

We'll explore the client configuration further and these settings when we add a new client config to represent our Angular SPA in the next post.

public static IEnumerable<Client> GetClients()
{
   return new[]
   {
      new Client {
          RequireConsent = false,
          ClientId = "js_test_client",
          ClientName = "Javascript Test Client",
          AllowedGrantTypes = GrantTypes.Code,
          RequirePkce = true,
          RequireClientSecret = false,
          AllowedScopes = { "openid", "profile", "email", "api.read" },
          RedirectUris = {"http://localhost:9090/test-client/callback.html"},
          AllowedCorsOrigins = {"http://localhost:9090"},
          AccessTokenLifetime = (int)TimeSpan.FromMinutes(120).TotalSeconds
       }
   };
}

Login View

The great thing about IdentityServer is that all the complex protocol support needed for OpenID Connect comes baked in. So, to hook up a login form to perform the authentication step, we're only on the hook to provide the necessary MVC UI parts and controller support. To make things even easier, the IdentityServer repo contains a very handy Quickstarts section that contains templates for getting off the ground with various clients and OAuth/OIDC flow scenarios.

Using the quickstart samples as a guide, I added a new Login view and associated controller action within the AccountsController to handle the login form postback. The action method is pretty straightforward. First, IdentityServer validates the incoming authorization request, and then the user's credentials are validated by the Identity system. If everything checks out, the browser gets redirected back to the client application that initiated the request at the location defined by their client RedirectUris setting.

At this point, I set up a tiny JavaScript test-client to test things out using the oidc-client library to handle all of the necessary protocol interactions with our OpenID Connect Provider. We'll explore the oidc-client in more detail when we implement it in the Angular project. For now, we'll use this simple test client to help validate our AuthServer's configuration.

Manual Test

Using Postman and our JavaScript test client, we can now validate the user creation and login functionality together. I fired up the project and then used Postman to issue a POST request to the action method we created earlier.

I got a 200 OK response back and a quick check of the AspNetUsers table in the database shows the new account!

To test the authentication portion, I pointed my browser to the test client at http://127.0.0.1:8787/test-client/index.html and used the account we made in the previous step to test the login flow between the test client, login view and IdentityServer. It doesn't do much. Still, it does use the same client library and APIs we'll implement in the Angular project, so testing in this fashion now sets us up for success later as we know the core functionality of the AuthServer is working as expected. βœ”οΈ

Triggering interactive user authentication via oidc-client.
Triggering interactive user authentication via oidc-client.

With everything in place, I fired up the project and was able to trigger and complete the authentication handshake successfully using the test client and login view.

Automated Integration Test with Puppeteer Sharp and Kestrel

We used ASP.NET Core's TestServer in our first test to spin up an in-memory application host and database to validate these components worked properly within the account creation endpoint.

This setup works great for simple tests against a single endpoint using a given HttpRequestMessage to assert the controller generates the expected response. However, TestServer does not open a real network socket, and as such, cannot be used with a browser to simulate more complicated scenarios like the manual test we just did involving Postman and our browser.

Luckily, we have access to a lightweight and fast application server in Kestrel which we can spin up as part of our test context to host the application while using Puppeteer Sharp to control a headless-chrome instance that automates the browser's behavior.

In the integration test project, I added a new xUnit fixture responsible for starting up the Kestrel web server. I then installed the PuppeteerSharp library from nuget to automate the JavaScript test client in a browser.

I added a new CanLogin test to AccountsControllerIntegrationTests.

In this test, I'm emulating the manual steps we performed above to create a new user in the in-memory database and then using Puppeteer Sharp to initiate and execute the login in the browser.

[Fact]
public async Task CanLogin()
{
  // 1. Create a new account against the InMemory database
  var httpResponse = await _client.SendAsync(new HttpRequestMessage(HttpMethod.Post, "/api/accounts")
  {
     Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(_signupRequests[1]), Encoding.UTF8, "application/json")
  });

 httpResponse.EnsureSuccessStatusCode();

 // 2. Ensure PuppeteerSharp has the browser downloaded
 await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultRevision);

 using (var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true }))
 {
   using (var page = await browser.NewPageAsync())
   {
     // 3. Navigate to the test client page
     await page.GoToAsync($"http://{_webHostFixture.Host}/test-client/index.html");

     var navigationTask = page.WaitForNavigationAsync();

     await Task.WhenAll(navigationTask, page.ClickAsync("button"));

     // 4. Fill out the login form
     await page.TypeAsync("#Username", _signupRequests[1].Email);
     await page.TypeAsync("#Password", _signupRequests[1].Password);

     // 5. Hit the login button and wait for redirect navigation...
     navigationTask = page.WaitForNavigationAsync(new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
     await Task.WhenAll(navigationTask, page.ClickAsync(".btn-primary"));

    var content = await page.GetContentAsync();
    await page.CloseAsync();

    // 6. Assert we have a logged-in state in the test client
    Assert.Contains("User logged in", content);
    Assert.Contains("Prescott Terrell", content);
    Assert.Contains("pterrell@mailinator.com", content);
    Assert.Contains("employer", content);
  }
 };
}

Wrapping Up

In this post, we walked through the process to build out an ASP.NET Core-based backend using Entity Framework Core and IdentityServer that is capable of creating and authenticating user accounts using the authorization code flow with Proof-Key for Code Exchange (PKCE). We also added a simple JavaScript client based on the oidc-client library and some automated integration tests to validate all of our hard work.

In the next post, we'll start looking at the Angular client and work through integrating it with our auth server to begin adding some real functionality to our app, so stay tuned!

Thanks for reading and happy programming!

Source code here

Get notified on new posts

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.

Related Posts


Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 2
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 2

Dec 30, 2019

Read more
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 3
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 3

Jan 30, 2020

Read more
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 4
Build an Authenticated GraphQL App with Angular, ASP.NET Core and IdentityServer - Part 4

Mar 19, 2020

Read more
Get notified on new posts
X

Straight from me, no spam, no bullshit. Frequent, helpful, email-only content.