ASP.NET Core Using JWT Authentication WEB API and consume it in Angular2 Client Application with proper project structure setup.

Introduction

I was trying to implement JWT Auth in web API. I want to consume the Web API in my Angular 2 client side application. But while searching on the internet, I didn’t find any correct solution for my problem with Project architecture setup. Finally, I am able to get the actual steps how to approach this problem, which may help you to save your time to check a lot of stuff from this solution. So here is the detailed explanation below.

Link for source code: https://github.com/rajeshdas85/JwtAuthentication

  1. Web API in ASP.NET Core with JWT authentication Project solution
  2. Angular2/4 for Client side application

See the project structure below:

Project_Architecture

Background

Step 1

Create ASP.NET Core Web API Project

  1. Open “Visual Studio 2017” -> go to “File” menu -> New -> Project
  2. Select project template.image1
  3. Right click on Solution Explorer -> Add -> New Project->Class Library.image2

Fitness.JWT.API Project Explanation

I would like to explain about the highlighted part of the project source code for enabling Jwt Authentication.

image3

Using the Code

startup.cs

Configuring secretkey, allowing cross origin and applying Use policy auth.

//
 public IConfigurationRoot Configuration { get; }
        //SecretKey  for Authentication
        private const string SecretKey = "ABCneedtogetthisfromenvironmentXYZ";
        private readonly SymmetricSecurityKey _signingKey = 
              new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            // services.AddMvc();
            // Add framework services.
            // Add framework services.
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",//Allow Cross origin
                    builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials());
            });

            services.AddOptions();

            // Make authentication compulsory across the board (i.e. shut
            // down EVERYTHING unless explicitly opened up).
            services.AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                 .RequireAuthenticatedUser()
                                 .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            });

            // Use policy auth.
            services.AddAuthorization(options =>
            {
                options.AddPolicy("FitnessJWT",
                                  policy => policy.RequireClaim("FitnessJWT", "FitnessUser"));
            });

            // 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);
            });
        }
//

JwtIssuerOptions.cs

This is the class file which is responsible for creating Auth unique ticket in server.

//
  public class JwtIssuerOptions
{
    /// <summary>
    /// "iss" (Issuer) Claim
    /// </summary>
    /// <remarks>The "iss" (issuer) claim identifies the principal that issued the
    ///   JWT.  The processing of this claim is generally application specific.
    ///   The "iss" value is a case-sensitive string containing a StringOrURI
    ///   value.  Use of this claim is OPTIONAL.</remarks>
    public string Issuer { get; set; }

    /// <summary>
    /// "sub" (Subject) Claim
    /// </summary>
    /// <remarks> The "sub" (subject) claim identifies the principal that is the
    ///   subject of the JWT.  The claims in a JWT are normally statements
    ///   about the subject.  The subject value MUST either be scoped to be
    ///   locally unique in the context of the issuer or be globally unique.
    ///   The processing of this claim is generally application specific.  The
    ///   "sub" value is a case-sensitive string containing a StringOrURI
    ///   value.  Use of this claim is OPTIONAL.</remarks>
    public string Subject { get; set; }

    /// <summary>
    /// "aud" (Audience) Claim
    /// </summary>
    /// <remarks>The "aud" (audience) claim identifies the recipients that the JWT is
    ///   intended for.  Each principal intended to process the JWT MUST
    ///   identify itself with a value in the audience claim.  If the principal
    ///   processing the claim does not identify itself with a value in the
    ///   "aud" claim when this claim is present, then the JWT MUST be
    ///   rejected.  In the general case, the "aud" value is an array of case-
    ///   sensitive strings, each containing a StringOrURI value.  In the
    ///   special case when the JWT has one audience, the "aud" value MAY be a
    ///   single case-sensitive string containing a StringOrURI value.  The
    ///   interpretation of audience values is generally application specific.
    ///   Use of this claim is OPTIONAL.</remarks>
    public string Audience { get; set; }

    /// <summary>
    /// "nbf" (Not Before) Claim (default is UTC NOW)
    /// </summary>
    /// <remarks>The "nbf" (not before) claim identifies the time before which the JWT
    ///   MUST NOT be accepted for processing.  The processing of the "nbf"
    ///   claim requires that the current date/time MUST be after or equal to
    ///   the not-before date/time listed in the "nbf" claim.  Implementers MAY
    ///   provide for some small leeway, usually no more than a few minutes, to
    ///   account for clock skew.  Its value MUST be a number containing a
    ///   NumericDate value.  Use of this claim is OPTIONAL.</remarks>
    public DateTime NotBefore => DateTime.UtcNow;

    /// <summary>
    /// "iat" (Issued At) Claim (default is UTC NOW)
    /// </summary>
    /// <remarks>The "iat" (issued at) claim identifies the time at which the JWT was
    ///   issued.  This claim can be used to determine the age of the JWT.  Its
    ///   value MUST be a number containing a NumericDate value.  Use of this
    ///   claim is OPTIONAL.</remarks>
    public DateTime IssuedAt => DateTime.UtcNow;

    /// <summary>
    /// Set the timespan the token will be valid for (default is 3 min/180 seconds)
    /// </summary>
    public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(1);

    /// <summary>
    /// "exp" (Expiration Time) Claim (returns IssuedAt + ValidFor)
    /// </summary>
    /// <remarks>The "exp" (expiration time) claim identifies the expiration time on
    ///   or after which the JWT MUST NOT be accepted for processing.  The
    ///   processing of the "exp" claim requires that the current date/time
    ///   MUST be before the expiration date/time listed in the "exp" claim.
    ///   Implementers MAY provide for some small leeway, usually no more than
    ///   a few minutes, to account for clock skew.  Its value MUST be a number
    ///   containing a NumericDate value.  Use of this claim is OPTIONAL.</remarks>
    public DateTime Expiration => IssuedAt.Add(ValidFor);

    /// <summary>
    /// "jti" (JWT ID) Claim (default ID is a GUID)
    /// </summary>
    /// <remarks>The "jti" (JWT ID) claim provides a unique identifier for the JWT.
    ///   The identifier value MUST be assigned in a manner that ensures that
    ///   there is a negligible probability that the same value will be
    ///   accidentally assigned to a different data object; if the application
    ///   uses multiple issuers, collisions MUST be prevented among values
    ///   produced by different issuers as well.  The "jti" claim can be used
    ///   to prevent the JWT from being replayed.  The "jti" value is a case-
    ///   sensitive string.  Use of this claim is OPTIONAL.</remarks>
    public Func<Task<string>> JtiGenerator =>
      () => Task.FromResult(Guid.NewGuid().ToString());

    /// <summary>
    /// The signing key to use when generating tokens.
    /// </summary>
    public SigningCredentials SigningCredentials { get; set; }
}
//

JwtController.cs

It is the controller where anonymous user will login and which creates the JWT security token and encodes it and sends back to client as Response with policy.

identity.FindFirst(“FitnessJWT”)

Please see the below code:

[HttpPost]
       [AllowAnonymous]
       public async Task<IActionResult> Get([FromBody] ApplicationUser applicationUser)
       {
           var identity = await GetClaimsIdentity(applicationUser);
           if (identity == null)
           {
               _logger.LogInformation($"Invalid username ({applicationUser.UserName})
                                      or password ({applicationUser.Password})");
               return BadRequest("Invalid credentials");
           }

           var claims = new[]
           {
       new Claim(JwtRegisteredClaimNames.Sub, applicationUser.UserName),
       new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
       new Claim(JwtRegisteredClaimNames.Iat,
           ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
       identity.FindFirst("FitnessJWT")
     };

           // 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);

           // Serialize and return the response
           var response = new
           {
               access_token = encodedJwt,
               expires_in = (int)_jwtOptions.ValidFor.TotalSeconds,
               State=1,
               expire_datetime= _jwtOptions.IssuedAt
           };

           var json = JsonConvert.SerializeObject(response, _serializerSettings);
           return new OkObjectResult(json);
       }

JwtAuthTestController.cs

This controller where I have defined the policy [Authorize(Policy = "FitnessJWT")] so when user requests the controller, then it must match with the policy and secret key, then the response will return to client.

[HttpGet("[action]")]
      [Authorize(Policy = "FitnessJWT")]
      public IActionResult WeatherForecasts()
      {
          var rng = new Random();

          List<WeatherForecast> lstWeatherForeCast = new List<WeatherForecast>();
          for (int i = 0; i < 5; i++)
          {
              WeatherForecast obj = new WeatherForecast();
              obj.DateFormatted = DateTime.Now.AddDays(i).ToString("d");
              obj.TemperatureC = rng.Next(-20, 55);
              obj.Summary = Summaries[rng.Next(Summaries.Length)];
              lstWeatherForeCast.Add(obj);
          }

          var response = new
          {
              access_token = lstWeatherForeCast,
              State = 1
          };

          var json = JsonConvert.SerializeObject(response, _serializerSettings);
          return new OkObjectResult(json);
      }

Step 2: Angular2/4 for Client Side Application

I would like to add one of this. I do not have much focus on the UI part, but I have tried to implement JWT Auth from Angualar2/4 Application.

Fitness.App.UI Solution

login.component.ts

Login module with typescript by passing Username and password:

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from "../../../app/services/auth.service";
import { LoginModel } from "../../model/login.model";
@Component({
    selector: 'Fitness-Login',
    templateUrl: './login.component.html',
    styleUrls: ['./login.component.css'],
    providers: [AuthService]
})
export class LoginComponent {
    loginModel = new LoginModel();
    constructor(private router: Router, private authService: AuthService) {
    }
    login() {
        this.authService.login(this.loginModel.userName, this.loginModel.password)
            .then(result => {
                if (result.State == 1) {
                    this.router.navigate(["/nav-menu"]);
                }
                else {
                    alert(result.access_token);
                }
            });
    }
}
auth.service.ts

Authentication service which validates credentials and redirects to home page.

login(userName: string, password: string): Promise<ResponseResult> {
    let data = {
        "userName": userName,
        "password": password
    }
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let applicationUser = JSON.stringify(data);
    let options = new RequestOptions({ headers: headers });

    if (this.checkLogin()) {
        return this.authPost(this.localUrl + '/api/Jwt', applicationUser, options);
    }
    else {
        return this.http.post(this.localUrl + '/api/Jwt', applicationUser, options).toPromise()
            .then(
            response => {
                let result = response.json() as ResponseResult;
                if (result.State == 1) {
                    let json = result.access_token as any;
                    localStorage.setItem(this.tokeyKey, json);
                    localStorage.setItem(this.tokeyExpKey, result.expire_datetime);
                    this.sg['isUserExist'] = true;
                }
                return result;
            }
            )
            .catch(this.handleError);
    }
}
app.module.client.ts

{ provide: 'ORIGIN_URL', useValue: 'http://localhost:57323' }, path on the JWT WEB API.

You need to change the local host API based on your machine URL.

  @NgModule({
    bootstrap: sharedConfig.bootstrap,
    declarations: sharedConfig.declarations,
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule,
        ...sharedConfig.imports
    ],
    providers: [
        //{ provide: 'ORIGIN_URL', useValue: location.origin },
        { provide: 'ORIGIN_URL', useValue: 'http://localhost:57323' },
        AuthService, AuthGuard, SimpleGlobal
    ]
})
export class AppModule {
}

To Run the Application, You Need to Set the Project as Below:

Run the solution with multiple startup projects.

Then in browser in two tabs, both client app and web API service will start.

image4

Output on the Application Is Below

image6

image5

User Name:Test and Password:Test

Then, it will redirect to nav menu page as below:

image7

BÌNH LUẬN

Please enter your comment!
Please enter your name here