Bootstrap

JWT Authentication with ASP.NET Core 2 Web API, Angular 5, .NET Core Identity and Facebook Login

This is an updated version of a post I did last May on the topic of jwt auth with Angular 2+ and ASP.NET Core Web Api. That post was based on ASP.NET Core 1.x so it's a little dated and not as relevant now since everyone is hacking on .NET Core 2.0 which brought changes to both the Identity membership system and jwt implementation.

So, here's an updated guide on implementing user registration and login functionality using ASP.NET Core 2 Web API and Angular 5. As a bonus, I see lots of folks wondering how to include social login with token-based web api authentication and spa apps (no cookies) so I have implemented Facebook login in this demo to show a potential approach of how this could be done.

Facebook login flow
Email login flow

Development Environment

  • Windows 10
  • Sql Server Express 2017 & Sql Server Management Studio 2017
  • Runs in both Visual Studio 2017 & Visual Studio Code
  • Node 8.9.4 & NPM 5.6.0
  • .NET Core 2.0 sdk
  • Angular CLI -> npm install -g @angular/cli https://github.com/angular/angular-cli
Get notified on new posts

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

The data model

The user is the centerpiece of our demo and luckily the ASP.NET Core Identity provider offers up the IdentityUser class which provides a convenient entity to store all of our user-related data. Even better, it can be extended to add custom properties you may require all users to possess in your application. I did exactly this with AppUser class. This class maps directly to the AspNetUsers table in the database.

// Add profile data for application users by adding properties to this class
public class AppUser : IdentityUser
{
  // Extended Properties
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public long? FacebookId { get; set; }
  public string PictureUrl { get; set; }
}

In applications with a number of different user roles we often need additional entities to store the unique bits of data for each role and also have a reference back to their main identity. To simulate this scenario, I created the Customer class. In this class, we have some custom properties and a reference to AppUser via the Identity property. The IdentityId is the foreign key in the database which Entity Framework Core uses to map the relationship between the two.

public class Customer
{
 public int Id { get; set; }
 public string IdentityId { get; set; }
 public AppUser Identity { get; set; }  // navigation property
 public string Location { get; set; }
 public string Locale { get; set; }
 public string Gender { get; set; }
}

The database context

We need to wire up the database and object graph in our application by creating a new DatabaseContext class which Entity Framework Core uses to interact with the database and our application entities. I created ApplicationDbContext which is quite simple as we only have to add the Customers mapping here. Because it inherits from IdentityDbContext it is already aware of IdentityUser and the other identity-related classes/tables so we don't have to map them explicitly.

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

 public DbSet<Customer> Customers { get; set; }
}

The last important step is to register the context in the DI container in Startup so it can be automatically injected into other consuming classes.

public void ConfigureServices(IServiceCollection services)
{
  // Add framework services.
  services.AddDbContext(options =>
      options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
      b => b.MigrationsAssembly("AngularASPNETCore2WebApiAuth")));
...

Spin up a database

With the model and context created in the application, we can use Entity Framework Core's migrations to generate the database and its schema based on the entities, data types, and relationships we defined in our code.

The Entity Framework tooling is available from the .NET Core CLI so we can create an initial migration file by running this from the command line in the project root:

src>dotnet ef migrations add initial

The files are created in the migrations folder.

To create the database, return to the command line and run:

src>dotnet ef database update

This command will pull the connection string from the project's appsettings.json file, connect to Sql Server and create a new database based on the previously generated migrations. If everything worked well, you should see a shiny new database.

Create new user accounts with email registration

We have two flows for creating users in our app, we'll look at email first. The API for creating new users via standard email registration will be the responsibility of the AccountsController.

There's just a single action method here to accept a POST request with the user registration details. It's fairly straightforward as we're just using Identity's UserManager to create a new user in the database and then using the context to create the related customer entity as described earlier. Note there is also some implicit mapping and validation happening here using AutoMapper and FluentValidation to help keep our code a little tidier. I won't go into detail on those aspects but encourage you to explore the source code and these libraries can help produce cleaner and DRYer code.

// POST api/accounts
[HttpPost]
public async Task<IActionResult> Post([FromBody]RegistrationViewModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var userIdentity = _mapper.Map(model);

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

    if (!result.Succeeded) return new BadRequestObjectResult(Errors.AddErrorsToModelState(result, ModelState));

    await _appDbContext.Customers.AddAsync(new Customer { IdentityId = userIdentity.Id, Location = model.Location });
    await _appDbContext.SaveChangesAsync();

    return new OkObjectResult("Account created");
}

Finally, with the database and API created, I was able to run the project and test with Postman to verify new users were created in the database by checking the AspNetUsers and Customers tables.

JWT Authentication

Implementing basic authentication with JSON web tokens on top of an ASP.NET Core Web API is fairly straightforward. Most of what we need is in middleware provided by the Microsoft.AspNetCore.Authentication.JwtBearer package.

To get started, I added a new class JwtIssuerOptions defining some of the claim properties our generated tokens will contain.

I also added a new configuration section to the appsettings.json file and then used the Configuration API in ConfigureServices() to read these settings and wire up JwtIssuerOptions in the IoC container.

public void ConfigureServices(IServiceCollection services)
{
...
// Get options from app settings
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));

// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
  options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
  options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
  options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
...

Following that, I added some more middleware code to ConfigureServices() which introduced JWT authentication to the request pipeline, specified the validation parameters to dictate how we want received tokens validated and finally, created an authorization policy to guard our API controllers and actions which we'll apply in a bit.

public void ConfigureServices(IServiceCollection services)
{
...
var tokenValidationParameters = new TokenValidationParameters
{
   ValidateIssuer = true,
   ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],

   ValidateAudience = true,
   ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],

   ValidateIssuerSigningKey = true,
   IssuerSigningKey = _signingKey,

   RequireExpirationTime = false,
   ValidateLifetime = true,
   ClockSkew = TimeSpan.Zero
};

services.AddAuthentication(options =>
{
   options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
   options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(configureOptions =>
  {
    configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
    configureOptions.TokenValidationParameters = tokenValidationParameters;
    configureOptions.SaveToken = true;
  });

// api user claim policy
services.AddAuthorization(options =>
{
  options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
...

The last piece to add was the JwtFactory which is just a helper to create the encoded tokens we'd like to exchange between the client and backend. This happens in GenerateEncodedToken() which simply creates a JwtSecurityToken with a combination of registered claims (from the jwt spec) Sub, Jti, Iat and two specific to our app: Rol and Id. We're also using values injected from the JwtOptions we set up in the previous step.

public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
 {
   var claims = new[]
   {
     new Claim(JwtRegisteredClaimNames.Sub, userName),
     new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
     new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
                 identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Rol),
                 identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Id)
   };

 // Create the JWT security token and encode it.
 var jwt = new JwtSecurityToken(
     issuer: _jwtOptions.Issuer,
     audience: _jwtOptions.Audience,
     claims: claims,
     notBefore: _jwtOptions.NotBefore,
     expires: _jwtOptions.Expiration,
     signingCredentials: _jwtOptions.SigningCredentials);

 var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

return encodedJwt;
}

Authenticating Identity users and issuing access tokens

We've got the JWT infrastructure in place so we're ready to start generating tokens for authenticated users. The AuthController is responsible for authenticating users who registered directly with the Identity membership system using their username and password aka the email flow.

It contains a single action to receive the POSTed credentials and validate them by calling GetClaimsIdentity() which is just a helper within the same controller that uses the UserManager to check the passed credentials against the database to determine if we have a valid user in the Identity system. If so, a token is generated and returned in the response.

// POST api/auth/login
[HttpPost("login")]
public async Task<IActionResult> Post([FromBody]CredentialsViewModel credentials)
{
    if (!ModelState.IsValid)
    {
         return BadRequest(ModelState);
    }

    var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
    if (identity == null)
       {
         return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
       }

    var jwt = await Tokens.GenerateJwt(identity, _jwtFactory, credentials.UserName, _jwtOptions, new JsonSerializerSettings { Formatting = Formatting.Indented });
    return new OkObjectResult(jwt);
}

private async Task<ClaimsIdentity> GetClaimsIdentity(string userName, string password)
{
   if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password))
       return await Task.FromResult<ClaimsIdentity>(null);

    // get the user to verifty
    var userToVerify = await _userManager.FindByNameAsync(userName);

    if (userToVerify == null) return await Task.FromResult<ClaimsIdentity>(null);

    // check the credentials
    if (await _userManager.CheckPasswordAsync(userToVerify, password))
       {
          return await Task.FromResult(_jwtFactory.GenerateClaimsIdentity(userName, userToVerify.Id));
       }

    // Credentials are invalid, or account doesn't exist
   return await Task.FromResult<ClaimsIdentity>(null);
}

I tested this action using postman to make sure I got the expected response when sending valid and invalid credentials to the authentication endpoint. Using the mark@fullstackmark.com account we created earlier I set up a post request to /api/auth/login and voila - authentication passes and I get a fresh JWT in the response.

Protecting Web API Controllers with claims-based authorization

One important function of claims in token authentication is that we can use them to tell the application what the user is allowed to access. Earlier, in the JwtFactory, we saw a custom claim called Rol added to the token which is just a string representing a role named ApiAccess.

With this role stashed in our token, we can use a claims-based authorization check to give the role access to certain controllers and actions so that only users possessing the role claim may access those resources.

We already enabled claims based authorization as part of the JWT setup we did earlier. The specific code to do that was this bit in ConfigureServices() in Startup.cs where we build and register a policy called ApiUser which checks for the presence of the Rol claim with a value of ApiAccess.

...
// api user claim policy
services.AddAuthorization(options =>
{
   options.AddPolicy("ApiUser", policy => policy.RequireClaim(Constants.Strings.JwtClaimIdentifiers.Rol, Constants.Strings.JwtClaims.ApiAccess));
});
...

We can then apply the policy using the familiar Authorize attribute on any controllers or actions we wish to guard. An example of this is found in the DashboardController which is decorated with [Authorize(Policy = "ApiUser")] meaning that only users with the ApiAccess role claim as part of the ApiUser policy can access this controller.

[Authorize(Policy = "ApiUser")]
[Route("api/[controller]/[action]")]
public class DashboardController : Controller
...

To test the controller authorization I used postman once again to create a GET request to the /api/dashboard/home endpoint. I also included a request header containing the JWT token we created in the previous login test. The header key is Authorization with a value formatted as Bearer xxx where xxx is the JWT. Issuing this request the Web API responds with a 200 OK status and some secure user data in the body.

I modified the request by changing some characters in the JWT to send an invalid token. This time, the token validation failed and the server responded accordingly with a 401 Unauthorized response when I tried to hit the protected endpoint.

The Angular app

At this point, we've completed the majority of the backend. The remaining bits are for Facebook login which we'll look at shortly. First, we'll build out the frontend in Angular to see how JWT authentication works in a real application.

As you can see from the gifs above, there's not much to this app. It has just 4 functions:

Organizing functionality with modules

Angular modules provide a super-effective way to group related components, directives, and services, in a way that they can be combined with other modules to assemble an application. For this app, I grouped the functions above into two modules by using the Angular CLI to create them within the src\app folder.

src\app>ng g module account
src\app>ng g module dashboard

After running these commands, we can see new folders created for each module. We'll add code to them shortly but we have a few more components to add first.

The registration form component

Next up, we'll add a new form component where users will create their account. Head back to the command line to use the CLI once again within the src\app\account module folder.

src\app\account>ng g component registration-form

A new registration-form folder is generated containing associated .ts, .scss and .html files.

Create the additional components

I repeated the steps to above to scaffold out some of the other major components we need:

  • A login-form
  • A home component which is the default view for the app
  • A spinner component to entertain users while the UI is busy

Talking to the backend Web API with UserService

UserService contains the register() and login() methods which use Angular's Http client to invoke the Web API endpoints we built and tested earlier.

register(email: string, password: string, firstName: string, lastName: string,location: string): Observable<UserRegistration> 
{
   let body = JSON.stringify({ email, password, firstName, lastName,location });
   let headers = new Headers({ 'Content-Type': 'application/json' });
   let options = new RequestOptions({ headers: headers });

   return this.http.post(this.baseUrl + "/accounts", body, options)
   .map(res => true)
   .catch(this.handleError);
}

login(userName, password) {
    let headers = new Headers();
    headers.append('Content-Type', 'application/json');

    return this.http
      .post(
      this.baseUrl + '/auth/login',
      JSON.stringify({ userName, password }),{ headers }
      )
      .map(res => res.json())
      .map(res => {
        localStorage.setItem('auth_token', res.auth_token);
        this.loggedIn = true;
        this._authNavStatusSource.next(true);
        return true;
      })
      .catch(this.handleError);
}

Note that in the login() method we're storing the authorization token issued by the server in the users' local storage via the localStorage.setItem('auth_token', res.auth_token) call. We'll see shortly how to use the token to make authenticated requests to the backend api.

Finishing up the registration form

With the component and service ready, we have all the pieces to complete the user registration feature. The last few steps involved adding the form markup to registration-form.component.html and binding the submit button on the form to a method in the registration-form.component.ts class.

registerUser({ value, valid }: { value: UserRegistration, valid: boolean }) 
{
  this.submitted = true;
  this.isRequesting = true;
  this.errors='';
  if(valid)
  {
     this.userService.register(value.email,value.password,value.firstName,value.lastName,value.location)
         .finally(() => this.isRequesting = false)
         .subscribe(result  => {if(result){
             this.router.navigate(['/login'],{queryParams: {brandNew: true,email:value.email}});                         
         }},
         errors =>  this.errors = errors);
    }      
}

This method is pretty simple, it's just calling userService.register() and passing along the user data then handling the observable response accordingly. If the server-side validation returns an error it is displayed to the user. If the request succeeds, the user is routed to the login view. The isRequesting property flag triggers the spinner so the UI can indicate that the app is busy while the request is in flight.

Finishing up the login form

The login and registration forms are nearly identical. I added the required markup to login-form.component.html and wired up an event handler in the login-form.component.ts class.

login({ value, valid }: { value: Credentials, valid: boolean }) {
    this.submitted = true;
    this.isRequesting = true;
    this.errors='';
    if (valid) {
      this.userService.login(value.email, value.password)
        .finally(() => this.isRequesting = false)
        .subscribe(
        result => {         
          if (result) {
             this.router.navigate(['/dashboard/home']);             
          }
        },
        error => this.errors = error);
    }
}

Here we just call userService.login() to make a request to the server with the given user credentials and handle the response accordingly. Again, either display any errors returned by the server or route the user to the Dashboard component if they've successfully authenticated. The check of valid is related to the form validation provided by the ngForm directive in the form markup. I won't cover this in detail but check out the code to get a better understanding of binding and validation in Angular provided by NgForm.

Protecting routes

At this point in our application, users can navigate anywhere. We're going to fix this by restricting access to certain areas to logged-in users only. The Angular router provides a feature specifically for this purpose in Navigation Guards

A guard is simply a function added to your route configuration that returns either true or false.

true means navigation can proceed. false means navigation halts and the route is not accessed.

Guards are registered using providers so they can be injected into your component routing modules where needed.

In this app, I created auth.guard.ts to protect access to the dashboard which acts as an administrative feature only logged in users can see.

// auth.guard.ts
import { Injectable } from '@angular/core';
import { Router, CanActivate } from '@angular/router';
import { UserService } from './shared/services/user.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private user: UserService,private router: Router) {}

  canActivate() {

    if(!this.user.isLoggedIn())
    {
       this.router.navigate(['/account/login']);
       return false;
    }

    return true;
  }
}

The AuthGuard is simply an @Injectable() class that implements CanActivate. It has a single method that checks the logged in status of the user by calling the isLoggedIn() method on the UserService.

isLoggedIn() is a little naive as it just checks for the presence of the JWT in the browser's localStorage. If it exists, we assume the user is logged in by returning true. If it is not found, the user is redirected back to the login page.

...
this.loggedIn = !!localStorage.getItem('auth_token')
...

To implement the guard in the dashboard's routing module I simply imported and updated the root dashboard route with a CanActivate() guard property.

import { ModuleWithProviders } from '@angular/core';
import { RouterModule }        from '@angular/router';

import { RootComponent }    from './root/root.component';
import { HomeComponent }    from './home/home.component'; 
import { SettingsComponent }    from './settings/settings.component'; 

import { AuthGuard } from '../auth.guard';

export const routing: ModuleWithProviders = RouterModule.forChild([
  {
      path: 'dashboard',
      component: RootComponent, canActivate: [AuthGuard],

      children: [      
       { path: '', component: HomeComponent },
       { path: 'home',  component: HomeComponent },
       { path: 'settings',  component: SettingsComponent },
      ]       
    }  
]);

We now have a protected dashboard feature!

Making authenticated Web API requests

Now that we have some level of authorization in place on the frontend, the last thing we want to do is start passing our JWT back to the server for Web API calls that require authentication. This is where we'll be utilizing the ApiAccess authorization policy we created and implemented earlier in the DashboardController.

To achieve this, I created a new dashboard service with a single method that retrieves some data for the Home page by making an authenticated HTTP call to the backend and passing the authorization token along in the request header.

export class DashboardService extends BaseService {

baseUrl: string = ''; 

constructor(private http: Http, private configService: ConfigService) {
   super();
   this.baseUrl = configService.getApiURI();
}

getHomeDetails(): Observable<HomeDetails> {
    let headers = new Headers();
    headers.append('Content-Type', 'application/json');
    let authToken = localStorage.getItem('auth_token');
    headers.append('Authorization', `Bearer ${authToken}`);
  
    return this.http.get(this.baseUrl + "/dashboard/home",{headers})
      .map(response => response.json())
      .catch(this.handleError);
  }  
}

getHomeDetails() simply retrieves the auth_token from localStorage and stashes is part of the Authorization.

With authenticated requests in place, I ran the project again and was able to complete an end to end test by creating a new user, logging in, and navigating to a protected route in the dashboard which displayed some highly secure data!

Adding Facebook OAuth authentication

The guide up to now has been based on standard credentials-based user registration and authentication directly with the ASP.NET Core Identity system. Now, we'll see how to incorporate a Facebook login flow into our app so users can signup/login directly with their Facebook credentials and gain access to the secure regions of the application.

The approach I'm showing here is quite simple. Basically, instead of relying on the ASP.NET Core Identity provider to authenticate the user's credentials as we do in the email flow we will integrate with Facebook's OAuth api and if login succeeds there we'll issue the user a JWT on our end which effectively logs them into the application.

Creating a Facebook application

Before we start coding, we need a Facebook application with which to integrate. For this demo, I created Fullstack Cafe which should work fine for you if you're running the project but if you wish to use your own app you will need to create and configure it on Facebook's developer portal.

It's fairly quick and painless:

  1. Once you've registered, create a new app

  2. Complete the Create New App Id prompt

  3. Add a product - choose Facebook Login. For the platform choose Web

  4. This step is important, here you must add the URL that Facebook will call back to after the OAuth process completes

  5. The two key values you will need to replace in the demo project are the App Id and App Secret. These live in appsettings.json file under FacebookAuthSettings

Extending the Angular app with Facebook login

Note: there are lots of options for integrating your app with Facebook. For this demo, I'm not using any SDKs or Angular packages. The approach here is probably a bit crude as is and can certainly be improved upon, it is based on this guide.

To add Facebook's browser-based login flow to the existing UI I created a new facebook-login component. The UI template is pretty simple, it's just a button that will invoke the login dialog to begin the process.

export class FacebookLoginComponent {

 private authWindow: Window;
 failed: boolean;
 error: string;
 errorDescription: string;
 isRequesting: boolean; 

 launchFbLogin() {
    // launch facebook login dialog
    this.authWindow = window.open('https://www.facebook.com/v2.11/dialog/oauth?&response_type=token&display=popup&client_id=1528751870549294&display=popup&redirect_uri=http://localhost:5000/facebook-auth.html&scope=email',null,'width=600,height=400');    
}
...

This will open a new dialog window with Facebook's login page. From there, the user carries out the login process by entering their Facebook creds and connecting with the application. When complete, Facebook will redirect back to our application where we must carry on processing the response to determine if login succeeded and take the appropriate action. The redirect in the demo is handled by facebook-auth.html.

There's not much happening here, just an empty page with some javascript to parse out the parameters containing the response data in the query string and then use the native window messaging api to send it back to the component via window.opener.postMessage(). The main thing we're interested in is the access_token which is received on a successful login and required by our backend Web API to carry out further validation.


 // if we don't receive an access token then login failed and/or the user has not connected properly
 var accessToken = getParameterByName("access_token");
 var message = {};
 if (accessToken) {
     message.status = true;
     message.accessToken = accessToken;
 }
 else
 {
     message.status = false;
     message.error = getParameterByName("error");
     message.errorDescription = getParameterByName("error_description");
 }
 window.opener.postMessage(JSON.stringify(message), "http://localhost:5000");

Within the component's handleMessage() method we interrogate the received data to determine authentication status. If a failure is received we show some UI to indicate there was a problem, otherwise if the authentication succeeded we make a call to userService.facebookLogin.

handleMessage(event: Event) {
 const message = event as MessageEvent;
 // Only trust messages from the below origin.
 if (message.origin !== "http://localhost:5000") return;

 this.authWindow.close();

    const result = JSON.parse(message.data);
    if (!result.status)
    {
      this.failed = true;
      this.error = result.error;
      this.errorDescription = result.errorDescription;
    }
    else
    {
      this.failed = false;
      this.isRequesting = true;

      this.userService.facebookLogin(result.accessToken)
        .finally(() => this.isRequesting = false)
        .subscribe(
        result => {
          if (result) {
            this.router.navigate(['/dashboard/home']);
          }
        },
        error => {
          this.failed = true;
          this.error = error;
        });      
    }
}

facebookLogin() passes along the accessToken to a Web API endpoint at /externalauth/facebook and expects an auth_token back. This endpoint is the final piece we need to complete the Facebook login integration.

facebookLogin(accessToken:string) {
  let headers = new Headers();
  headers.append('Content-Type', 'application/json');
  let body = JSON.stringify({ accessToken });  
  return this.http
  .post(
    this.baseUrl + '/externalauth/facebook', body, { headers })
    .map(res => res.json())
    .map(res => {
      localStorage.setItem('auth_token', res.auth_token);
      this.loggedIn = true;
      this._authNavStatusSource.next(true);
      return true;
     })
    .catch(this.handleError);
}

Generating JWT tokens for authenticated Facebook users

To complete the login process I created a new Web API controller named ExternalAuthController with a single action to handle Facebook logins.

// POST api/externalauth/facebook
[HttpPost]
public async Task<IActionResult> Facebook([FromBody]FacebookAuthViewModel model)
{
    // 1.generate an app access token
    var appAccessTokenResponse = await Client.GetStringAsync($"https://graph.facebook.com/oauth/access_token?client_id={_fbAuthSettings.AppId}&client_secret={_fbAuthSettings.AppSecret}&grant_type=client_credentials");
    var appAccessToken = JsonConvert.DeserializeObject<FacebookAppAccessToken>(appAccessTokenResponse);
    // 2. validate the user access token
      var userAccessTokenValidationResponse = await Client.GetStringAsync($"https://graph.facebook.com/debug_token?input_token={model.AccessToken}&access_token={appAccessToken.AccessToken}");
      var userAccessTokenValidation = JsonConvert.DeserializeObject<FacebookUserAccessTokenValidation>(userAccessTokenValidationResponse);

    if (!userAccessTokenValidation.Data.IsValid)
      {
        return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid facebook token.", ModelState));
      }

    // 3. we've got a valid token so we can request user data from fb
    var userInfoResponse = await Client.GetStringAsync($"https://graph.facebook.com/v2.8/me?fields=id,email,first_name,last_name,name,gender,locale,birthday,picture&access_token={model.AccessToken}");
    var userInfo = JsonConvert.DeserializeObject<FacebookUserData>(userInfoResponse);

    // 4. ready to create the local user account (if necessary) and jwt
    var user = await _userManager.FindByEmailAsync(userInfo.Email);

    if (user == null)
    {
       var appUser = new AppUser
       {
         FirstName = userInfo.FirstName,
         LastName = userInfo.LastName,
         FacebookId = userInfo.Id,
         Email = userInfo.Email,
         UserName = userInfo.Email,
         PictureUrl = userInfo.Picture.Data.Url
       };

       var result = await _userManager.CreateAsync(appUser, Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Substring(0, 8));

       if (!result.Succeeded) return new BadRequestObjectResult(Errors.AddErrorsToModelState(result, ModelState));

       await _appDbContext.Customers.AddAsync(new Customer { IdentityId = appUser.Id, Location = "",Locale = userInfo.Locale,Gender = userInfo.Gender});
       await _appDbContext.SaveChangesAsync();
      }

    // generate the jwt for the local user...
    var localUser = await _userManager.FindByNameAsync(userInfo.Email);

    if (localUser==null)
    {
       return BadRequest(Errors.AddErrorToModelState("login_failure", "Failed to create local user account.", ModelState));
    }

    var jwt = await Tokens.GenerateJwt(_jwtFactory.GenerateClaimsIdentity(localUser.UserName, localUser.Id), _jwtFactory, localUser.UserName, _jwtOptions, new JsonSerializerSettings {Formatting = Formatting.Indented});
  
    return new OkObjectResult(jwt);
    }
}

There's a bit of code here that basically does this:

  1. Calls the Facebook api to generate an app access token we need to make the next request
  2. Make another call again to Facebook to validate the user access token we received on the initial login.
  3. If the token is valid, use it to request information about the user from the Facebook graph api: email, name, picture etc.
  4. Uses userManager to check if we have this user in our local database, if not we add them and also add an associated customer entity, in the same manner, we did during the email registration flow.
  5. Generate a JWT token and return it in the response back to the client.

With a token returned to the Angular app the loop is complete. Running the project we're now able to login with Facebook, receive a JWT and then hit our protected dashboard which displays some of our user data - sweet!

That's a wrap

Phew...if you're still reading - you rock! If you found this guide helpful or have any questions or feedback I'd love to hear it in the comments below. Finally, securing real applications for real people in production scenarios demands careful consideration, design, and testing. This post is intended as a guide to illustrate how these technologies can potentially be combined as part of a security solution, not as a prescription for one.

Source code here

Get notified on new posts

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

Get notified on new posts
X

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