Search code examples
angulartypescriptpostjwt

HttpClient post request delayed action - Angular 16


So, I am here trying to implement JWT in Angular with a .Net Core API. When I fire up my Angular server and run the application, the following scenarios happen:

  • Try with the right credentials fails, try again and then it works.
  • Try with the wrong credentials fails, try again and then it fails.
  • Try with the right credentials fails, try again but with wrong credentials and then login works.

I'd like to make clear that the log in or not depends on the JWT token being already stored in localStorage as a value, which means at some point the API call works, it's just not validated at the right time somehow.

So, in order to unveil the mistery, here is the code in question:

This will be the AuthService:

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { User } from 'src/app/models/User';


const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':'application/json;charset=UTF-8'
  })
}


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

  public loggedIn = false;

  constructor(private httpClient:HttpClient) { }

  login(user: User){
    // Set the User object with the corresponding credentials as the request body
    const body = JSON.stringify(user);
    // Send the post request with its corresponding headers and body
    this.httpClient.post<any>("https://localhost:7054/login", body, httpOptions)
    .subscribe(data => {
      // Here, the data received from the API is known (as I developed it myself):
      // it will only contain the token if successful, and will return a bad request
      // with a blank token if it fails, so the only scenario where the token may
      // be valid is through a HTTP 200 response.
      localStorage.setItem("token", data.token);
    });
  }
}

And this will be the HomeComponent.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthResponse } from 'src/app/models/AuthResponse';
import { User } from 'src/app/models/User';
import Swal from "sweetalert2";
import { AuthService } from 'src/app/services/auth/auth.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})

export class HomeComponent {
  user: User = new User();

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

  token: AuthResponse = new AuthResponse();

  //Login method
  login(username: string, password: string) {

    // - User authentication
    // The verification occurs in the backend for security purposes.
    
    // Validate credentials are not blank
    if(username != "" || password != ""){

      // Create a new User object, which will be passed to the API as request body
      let user = new User();
      user.email = username;
      user.pass = password;

      // Call the auth service
      this.authService.login(user);
      
      // If the previous operation went well, the token must be stored, and we should be able to log in
      if(localStorage.getItem("token")){
        this.router.navigate(['/home']);
      }
      // If the request returns nothing, the credentials are incorrect, therefore an error alert will be sent, plus the token is set to blank
      else{
        this.sendError("Credentials are incorrect.")
        localStorage.setItem("token", "");
      }
    }
    else{
      // If the credentials are blank, therefore an error alert will be sent, plus the token is set to blank
      this.sendError("Please type your credentials.")
      localStorage.setItem("token", "");
    }
  }

  sendError(error:string){
    Swal.fire({
      icon: 'error',
      title: 'Error',
      text: error,
    })

  }
}

So, I'd appreciate if anyone could point out the reason for the strange behavior. I'm sure there's something I'm missing or plain out doing wrong, I just can't put my finger on it.

Thanks in advance for your help.

I have tried using interceptors to handle the response, but they don't seem to help with the strange behavior.

I have tried forcing token validation, but it doesn't seem like it will help either since the token kind of gets cached in between attempts.


Solution

  • I see a bunch of problems.

    First here:

      if(localStorage.getItem("token")){
        this.router.navigate(['/home']);
      }
      // If the request returns nothing, the credentials are incorrect, therefore an error alert will be sent, plus the token is set to blank
      else{
        this.sendError("Credentials are incorrect.")
        localStorage.setItem("token", "");
      }
    

    Your else statement is setting the token. So, the second time a person clicks "login"; and the app will navigate to the home route, regardless of whether the login is successful or not.

    Additionally, the login code is asynchronous--as it should be--but you are running the check synchronously. So this section of code (right above the previous section I quoted may be checking for the existence of the token before the login function completes.

     // Call the auth service
      this.authService.login(user);
      
      // If the previous operation went well, the token must be stored, and we should be able to log in
      if(localStorage.getItem("token")){
    

    A possible fix, may be to make the authService return an Observable. Also, process the token using a pipe operator, not a subscribe() method.

    login(user: User){
        // Set the User object with the corresponding credentials as the request body
        const body = JSON.stringify(user);
        // Send the post request with its corresponding headers and body
        return this.httpClient.post<any>("https://localhost:7054/login", body, httpOptions)
        .pipe(tap(data => {
          // Here, the data received from the API is known (as I developed it myself):
          // it will only contain the token if successful, and will return a bad request
          // with a blank token if it fails, so the only scenario where the token may
          // be valid is through a HTTP 200 response.
          localStorage.setItem("token", data.token);
        }));
      }
    

    I used tap, there may be better operators to use in this case, though.

    Then tweak your component code to subscribe to the Observable, so the results are processed after the login() method completes:

      // Call the auth service
      this.authService.login(user).subscribe(() ==> {
      // If the previous operation went well, the token must be stored, and we should be able to log in
         if(localStorage.getItem("token")){
           this.router.navigate(['/home']);
         }
        // If the request returns nothing, the credentials are incorrect, therefore an error alert will be sent, plus the token is set to blank
         else{
           this.sendError("Credentials are incorrect.")
           localStorage.setItem("token", "");
         }
    
      });
      
    

    I would recommend that you process errors based on an error code returned from the server, as opposed to checking for the existence of the token.

    Disclaimer: All this code is written in a browser and is untested, but should demonstrate an alternate approach that will fix the few logistical errors I've seen.