Windows Hosting

ASP.NET Core 7.0.4 Tutorial: How to Handle Token Expiry in Angular with .Net Core?

Token authentication in Angular involves sending a token with each HTTP request to your API, typically in the Authorization header. Token expiry is a common scenario in web applications that use token-based authentication. In Angular, you can handle token expiry by implementing an HttpInterceptor that intercepts HTTP requests and checks if the token is still valid. Here is a step-by-step guide for implementing token authentication in Angular:

Prerequisites

  • Angular 13
  • HTML/Bootstrap
  • ASP.NET Core Web Api.

For this article, I have created an Angular and ASP.NET Core Web Api project. To create an Angular project, we need to follow the following steps:

Create Project

I have created a project using the following command in the Command Prompt.

ng new AuthDemo

Open a project in Visual Studio Code using the following commands.

cd AuthDemo
Code .

Now in Visual Studio, your project looks as below.

Now Create a Token interceptor name as TokenInterceptor

import { Injectable } from "@angular/core";
import {
    HttpInterceptor, HttpHandler, HttpRequest,
} from '@angular/common/http';
import { AuthService } from "../service/auth.service";
import { Router } from "@angular/router";

@Injectable()
export class TokenInterceptor implements HttpInterceptor {
    constructor(private router: Router,private authService: AuthService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler) {
        let accessToken: string =localStorage.getItem("accessToken");
        if (accessToken && accessToken.length>0 ){
            let tokenExpiration: any =new Date(localStorage.getItem("tokenExpiration"));
            if( tokenExpiration > new Date()) {
                request = request.clone({
                    setHeaders: {
                        Authorization: `Bearer ${accessToken}`
                    }
                });
            }
            else {
                console.log('Token Expired... Token Referesh')
                this.authService.logout().subscribe({
                    next: (response) => {
                      localStorage.clear();
                      this.router.navigate(["/login"]);
                    },
                    error: (err) => {
                      console.log(err)
                    },
                    complete: () => console.info('Session Logout, Due to Token expire')
                });
            }
        }else{
            request = request.clone({
                setHeaders: {
                    Authorization: ""
                }
            });
        }
        return next.handle(request);
    }
}

Now Create another interceptor name as ErrorInterceptor

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError, of, BehaviorSubject } from 'rxjs';
import { catchError, flatMap, switchMap, filter, take } from 'rxjs/operators';
import { Router, Route } from '@angular/router';


@Injectable()

export class ErrorInterceptor implements HttpInterceptor {

constructor(private router: Router) {}

intercept(request: HttpRequest<any>, next: HttpHandler): any {
 return next.handle(request).pipe(catchError(error => {
    return this.handle401Error(request, next);
 }));
}

private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
    return this.handleToken(request, next);
}

private handleToken(responserequest: HttpRequest<any>, next: HttpHandler) {
     localStorage.clear();
     this.router.navigate(['/login']);
     return throwError('Token Expired');
}
}

Now Create a service name as AuthService:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from 'src/environments/environment';

@Injectable({
    providedIn: 'root'
})
export class AuthService {
    httpheader:any;

    constructor(private http: HttpClient) {
        this.httpheader={
            headers:new HttpHeaders({'Content-Type':'application/json'})
          }
     }

    getWeather() {
        return this.http.get(`${environment.baseUrl}WeatherForecast`);
    }

    login(loginModel: any) {
        return this.http.post(`${environment.baseUrl}Account/login`,
        {
            userName:loginModel.userName,
            password:loginModel.password
        },
        this.httpheader
        );
    }

    logout() {
        return this.http.post(`${environment.baseUrl}Account/logout`, null,this.httpheader);
    }

    refreshToken() {
        let accessToken: any =localStorage.getItem("accessToken");
        localStorage.removeItem("accessToken");
        return this.http.post(`${environment.baseUrl}Account/refresh-token`,
        {
            AccessToken:accessToken,
            RefreshToken :this.getCurrentUser().password
        },
        this.httpheader
        );
    }

    getCurrentUser() {
        return JSON.parse(localStorage.getItem('currentUser'));
    }
}

Now Create a login component

ng g c login

login.component.html

<div class="sidenav">
    <div class="login-main-text">
       <h2>Application<br> Login Page</h2>
       <p>Login or register from here to access.</p>
    </div>
 </div>
 <div class="main">
    <div class="col-md-6 col-sm-12">
       <div class="login-form">
          <form #loginForm="ngForm" (ngSubmit)="login(loginForm)">
             <div class="form-group">
                <label for="username" class="sr-only">User Name</label>
                <input type="email" id="userName" name="userName" ngModel class="form-control" placeholder="User Name" required autofocus>
             </div>
             <div class="form-group">
                <label for="password" class="sr-only">Password</label>
                <input type="password" id="password" name="password" ngModel class="form-control" placeholder="Password" required>
             </div>
             <button type="submit" class="btn btn-black">Login</button>
          </form>
       </div>
       <div *ngIf="invalidLogin" class="alert alert-danger">Invalid username or password.</div>
    </div>
 </div>

login.component.ts

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { environment } from 'src/environments/environment';
import { AuthService } from '../service/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  public invalidLogin: boolean = false;

  constructor(private router: Router,
    private authService: AuthService ) { }

  ngOnInit(): void {
  }

  public login = (form: NgForm) => {

    this.authService.login(form.value
    ).subscribe({
      next: (response) => {
        const token = (<any>response);
        localStorage.setItem("currentUser",JSON.stringify(form.value));
        localStorage.setItem("accessToken", token.token);
        localStorage.setItem("tokenExpiration", token.expiration);
        this.invalidLogin = false;
        this.router.navigate(["/home"]);
      },
      error: (err) => {
        console.log(err)
        this.invalidLogin = true;
      },
      complete: () => console.info('Login complete')
    });
  }
}

Now Create a Home Component using the following command

ng g c home

Home.Component.Html

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <a class="navbar-brand" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav mr-auto">
        <li class="nav-item active">
          <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
        </li>
        <li class="nav-item">
          <a class="nav-link" href="#">Link</a>
        </li>
        <li class="nav-item dropdown">
          <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
            Dropdown
          </a>
          <div class="dropdown-menu" aria-labelledby="navbarDropdown">
            <a class="dropdown-item" href="#">Action</a>
            <a class="dropdown-item" href="#">Another action</a>
            <div class="dropdown-divider"></div>
            <a class="dropdown-item" href="#">Something else here</a>
          </div>
        </li>
        <li class="nav-item">
          <a class="nav-link disabled" href="#">Disabled</a>
        </li>
      </ul>
      <form class="form-inline my-2 my-lg-0">
        <button class="btn btn-outline-success my-2 my-sm-0" (click)="logout()" type="button">Logout</button>
      </form>
    </div>
</nav>
<div class="container">
    <table class="table table-hover">
        <thead>
          <tr>
            <th scope="col">#</th>
            <th scope="col">Date</th>
            <th scope="col">Temp. C</th>
            <th scope="col">Temp. F</th>
            <th scope="col">Summary</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let data of weatherData; let i=index">
            <th scope="row">{{i+1}}</th>
            <td>{{data.date | date}}</td>
            <td>{{data.temperatureC}}</td>
            <td>{{data.temperatureF}}</td>
            <td>{{data.summary}}</td>
          </tr>
        </tbody>
      </table>
</div>

Home.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../service/auth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
  weatherData:any;

  constructor(private router: Router,
    private authService: AuthService ) { }

  ngOnInit(): void {
    this.getWeather();
  }

  public getWeather = () => {

    this.authService.getWeather().subscribe({
      next: (response) => {
        this.weatherData = (<any>response);
      },
      error: (err) => {
        console.log(err)
      },
      complete: () => console.info('Weather Loaded')
    });
  }

  public logout = () => {

    this.authService.logout().subscribe({
      next: (response) => {
        localStorage.clear();
        this.router.navigate(["/login"]);
      },
      error: (err) => {
        console.log(err)
      },
      complete: () => console.info('Weather Loaded')
    });
  }
}

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {path:'',component:HomeComponent},
  {path:'home',component:HomeComponent},
  {path:'login',component:LoginComponent},
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { TokenInterceptor } from './Interceptor/token-interceptor';
import { ErrorInterceptor } from './Interceptor/error-interceptor';
import { FormsModule } from '@angular/forms';
import { AuthService } from './service/auth.service';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    AppRoutingModule,
  ],
  providers: [
    AuthService,
    {provide:HTTP_INTERCEPTORS,useClass:TokenInterceptor,multi:true},
    {provide:HTTP_INTERCEPTORS,useClass:ErrorInterceptor,multi:true},
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now we need to move on to Asp.net Core Web API.

In Asp.net Core, we need to add the following packages:

Microsoft.AspNetCore.Authentication.Cookies

This is an ASP.NET Core middleware that enables an application to use cookie based authentication.

Microsoft.AspNetCore.Authentication.JwtBearer

This is an ASP.NET Core middleware that enables an application to receive an OpenID Connect bearer token.

We need to add some lines in appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "JWT": {
    "ValidAudience": "",
    "ValidIssuer": "",
    "Secret": "JWTRefreshTokenHIGHsecuredPasswordVVVp1OH7Xzyr",
    "TokenValidityInMinutes": 1,
    "RefreshTokenValidityInDays": 7
  },
  "AllowedHosts": "*"
}

Now enable CORS, add swagger, and enable the authentication in asp.net core web API using the following code in the program.cs file

using Microsoft.AspNetCore.Authentication.Cookies;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
var MyAllowedOrigins = "_myAllowedOrigins";

builder.Services.AddCors(options =>
{
    options.AddPolicy(MyAllowedOrigins,
                          builder =>
                          {
                              builder
                              .AllowAnyOrigin()
                              .AllowAnyHeader()
                              .AllowAnyMethod();
                          });
});
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseCors(MyAllowedOrigins);
app.UseHttpsRedirection();

app.UseAuthentication();

app.UseAuthorization();

app.MapControllers();

app.Run();

Now add a new API controller name as AccountController

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace FormAuthAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AccountController : ControllerBase
    {
        private readonly IConfiguration _configuration;

        public AccountController(
            IConfiguration configuration)
        {
            _configuration = configuration;
        }

        [HttpPost("login")]
        public IActionResult Login([FromBody] LoginModel model)
        {
            if (model != null && model.UserName != "")
            {
                ApplicationUser user = new ApplicationUser()
                {
                    UserName = model.UserName,
                    Password = model.Password
                };

                var authClaims = new List<Claim>
                {
                    new Claim(ClaimTypes.Name, model.UserName??"")
                };

                var identity = new ClaimsIdentity(
                    authClaims, CookieAuthenticationDefaults.AuthenticationScheme);
                var principal = new ClaimsPrincipal(identity);
                var props = new AuthenticationProperties();
                var token = CreateToken(authClaims);
                props.Parameters.Add("token", token);

                HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal, props).Wait();

                return Ok(new
                {
                    Token = new JwtSecurityTokenHandler().WriteToken(token),
                    Expiration = token.ValidTo
                });
            }
            return Unauthorized();
        }

        [HttpPost("Logout")]
        public IActionResult Logout()
        {
            HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return Ok();
        }

        [HttpPost]
        [Route("refresh-token")]
        public IActionResult RefreshToken(TokenModel tokenModel)
        {
            var token = HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            if (tokenModel is null)
            {
                return BadRequest("Invalid client request");
            }

            string? accessToken = tokenModel.AccessToken;

            var principal = GetPrincipalFromExpiredToken(accessToken);
            if (principal == null)
            {
                return BadRequest("Invalid access token or refresh token");
            }

            string username = principal.Identity.Name;

            if (username == null)
            {
                return BadRequest("Invalid access token or refresh token");
            }

            var newAccessToken = CreateToken(principal.Claims.ToList());

            return Ok(new
            {
                Token = new JwtSecurityTokenHandler().WriteToken(newAccessToken),
                Expiration = newAccessToken.ValidTo
            });
        }

        private JwtSecurityToken CreateToken(List<Claim> authClaims)
        {
            var authSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"]));
            _ = int.TryParse(_configuration["JWT:TokenValidityInMinutes"], out int tokenValidityInMinutes);

            var token = new JwtSecurityToken(
                issuer: _configuration["JWT:ValidIssuer"],
                audience: _configuration["JWT:ValidAudience"],
                expires: DateTime.Now.AddMinutes(tokenValidityInMinutes),
                claims: authClaims,
                signingCredentials: new SigningCredentials(authSigningKey, SecurityAlgorithms.HmacSha256)
                );

            return token;
        }

        private ClaimsPrincipal? GetPrincipalFromExpiredToken(string? token)
        {
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:Secret"])),
                ValidateLifetime = false
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
            if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
                throw new SecurityTokenException("Invalid token");

            return principal;
        }
    }
}

With these steps, you should now be able to implement Token authentication in Angular.

Overall, we have implemented a combination of cookie and JWT token authentication technique in this application.

ASP.NET Core 7.0.4 Hosting Recommendation

HostForLIFE.eu

HostForLIFE.eu is a popular recommendation that offers various hosting choices. From shared hosting to dedicated servers, you will find options for beginners and popular websites. It offers various hosting choices if you want to scale up. Also, you get flexible billing plans where you can purchase a subscription even for one or six months.