angularforms-authenticationangular-router-guards

Authentication guard with angular


can anyone help me with this. I am trying to implement an authentication guard for on my LogIn component. This guard should allow users to access the dashboard only in the following conditions: If there is a session token defined in local storage and if the token is valid (validity must be checked through the GET call that checks the session token). If the token is not valid, I want to delete the token from the local storage and redirect to login page. Here is what I've done so far:

core/guard/auth.guard.ts

import { Injectable } from '@angular/core';
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
import { Observable } from 'rxjs';
import {AuthService} from "../services/auth.service";

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor( private service: AuthService, private router:Router) {
  }
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    if(this.service.isLoggedIn()){
      return true;
    }else {
      return this.router.navigate(["login"]);
    }
  }
}`

app.module.ts

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

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { KpiComponent } from './kpi/kpi.component';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { PieComponent } from './kpi/pie/pie.component';
import { BarComponent } from './kpi/bar/bar.component';
import { MainTemplateComponent } from './atomic/main-template/main-template.component';
import { LoginPageComponent } from './atomic/organisms/login-page/login-page.component';
import { DashboardPageComponent } from './atomic/organisms/dashboard-page/dashboard-page.component';
import { NavbarComponent } from './navbar/navbar.component';
import { MatMenuModule } from '@angular/material/menu';
import { ReactiveFormsModule } from '@angular/forms';
import { reducers } from './store';
import { HrMaterialModule } from './material/material.module';


@NgModule({
  declarations: [
    AppComponent,
    KpiComponent,
    PieComponent,
    BarComponent,
    MainTemplateComponent,
    LoginPageComponent,
    DashboardPageComponent,
    NavbarComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    StoreModule.forRoot(reducers, {}),
    StoreDevtoolsModule.instrument(),
    BrowserAnimationsModule,
    MatCardModule,
    MatButtonModule,
    MatIconModule,
    ReactiveFormsModule,
    MatMenuModule,
    HrMaterialModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

navbar.component.ts

<div class="navbar">
    <a routerLink="/dashboard">
        <img src="../../assets/imbus-logo.svg" alt="imbus-logo">
    </a>
    <div class="navbar-elements">
        <div class="user-elements">
            <a routerLink="/dashboard" class="account-icon" [matMenuTriggerFor]="menu">
                <img src="../../assets/manage-account.svg" alt="">
            </a>
            <mat-menu #menu="matMenu" xPosition="before">
                <button mat-menu-item>
                    <mat-icon>settings</mat-icon>
                    <span>My Settings</span>
                  </button>
                  <button mat-menu-item  routerLink="/login">
                    <mat-icon>keyboard_backspace</mat-icon>
                    <span>Logout</span>
                  </button>
            </mat-menu>
        </div>

        <a routerLink="/dashboard" class="settings-icon">
            <img src="../../assets/setttings.svg" alt="">
        </a>
    </div>
</div>

login-page.component.html

<div class="container">
  <div class="screen">
    <div class="screen-content">
      <form [formGroup]="loginForm" (ngSubmit)="loginUser()" class="login">
        <div class="logo">
          <img src="../../../../assets/imbus-logo.svg" alt="imbus-logo" />
        </div>
        <div class="user-field">
          <input #email
            formControlName="email"
            type="text"
            class="login-input"
            placeholder="User login"
          />
          <br />
          <span *ngIf="user && user.invalid && user.touched" style="color: red"
            >User name is required.</span
          >
        </div>
        <div class="password-field">
          <input
            formControlName="password"
            type="{{ type }}"
            class="login-input"
            placeholder="Password"
          />

          <mat-icon
            (click)="togglePassword($event)"
            *ngIf="showPassword"
            svgIcon="hr:hide-text"
            class="hide-icon"
          ></mat-icon>
          <mat-icon
            (click)="togglePassword($event)"
            *ngIf="!showPassword"
            svgIcon="hr:show-text"
            class="show-icon"
          ></mat-icon>

          <span
            *ngIf="password && password.invalid && password.touched"
            style="color: red"
          >
            Password is required.</span
          >
        </div>

        <button [disabled]="loginForm.invalid" class="button login-submit">
          <span class="button-text" (click)="proceedlogin(name.value)">LogIn</span>
        </button>
      </form>
      <p id="copy-rights-text" class="copy-right">&copy; IMBUS HR DASHBOARD 2023</p>
    </div>
  </div>
</div>

login-page.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import {Router} from "@angular/router";
import {AuthService} from "../../../core/services/auth.service";
@Component({
  selector: 'app-login-page',
  templateUrl: './login-page.component.html',
  styleUrls: ['./login-page.component.scss'],
})
export class LoginPageComponent implements OnInit {
  public showPassword = false;
  public type = 'password';

  constructor( private service: AuthService,
    private router: Router) {
    localStorage.clear();
  }

  loginForm = new FormGroup({
    email: new FormControl('', [Validators.required]),
    password: new FormControl('', [
      Validators.required,
      Validators.minLength(8),
    ]),
  });

  loginUser() {
    console.warn(this.loginForm.value);
    if (this.loginForm.valid) {
      console.log('Form Submitted!', this.loginForm.value);
    }
  }


  ngOnInit():void {}
  proceedlogin(email:any){
    localStorage.setItem("user", email);
    this.router.navigate(["/dashboard"])
  }

  get user() {
    return this.loginForm.get('user');
  }
  get password() {
    return this.loginForm.get('password');
  }
  togglePassword(e: Event) {
    e.stopPropagation();
    e.preventDefault();
    this.showPassword = !this.showPassword;
    this.type = this.showPassword ? 'text' : 'password';
  }
}

core/service/authservice.ts

import { Injectable } from '@angular/core';


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

  constructor() {}

  isLoggedIn(){
    return localStorage.getItem("user")!=null;
  }
}

I don't understand the logic on how to validate the token.


Solution

  • The AuthGuardService from @SeF is the right way. Independently from it you need a HttpInterceptor, too I think. This one adds Bearer XXX to your header and will try to refresh your token (if needed). If both fails you can set a value in your AuthService to false as example and your AuthGuardService will redirect the user automatically.

    import { Injectable } from "@angular/core";
    import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from "@angular/common/http";
    import { AuthenticationService } from "../authentication.service";
    import { Observable } from "rxjs/Observable";
    import { BehaviorSubject } from "rxjs/BehaviorSubject";
    
    @Injectable()
    export class RefreshTokenInterceptor implements HttpInterceptor {
        private refreshTokenInProgress = false;
        // Refresh Token Subject tracks the current token, or is null if no token is currently
        // available (e.g. refresh pending).
        private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
            null
        );
        constructor(public auth: AuthenticationService) {}
    
        intercept(
            request: HttpRequest<any>,
            next: HttpHandler
        ): Observable<HttpEvent<any>> {
            return next.handle(request).catch(error => {
                // We don't want to refresh token for some requests like login or refresh token itself
                // So we verify url and we throw an error if it's the case
                if (
                    request.url.includes("refreshtoken") ||
                    request.url.includes("login")
                ) {
                    // We do another check to see if refresh token failed
                    // In this case we want to logout user and to redirect it to login page
    
                    if (request.url.includes("refreshtoken")) {
                        this.auth.logout();
                    }
    
                    return Observable.throw(error);
                }
    
                // If error status is different than 401 we want to skip refresh token
                // So we check that and throw the error if it's the case
                if (error.status !== 401) {
                    return Observable.throw(error);
                }
    
                if (this.refreshTokenInProgress) {
                    // If refreshTokenInProgress is true, we will wait until refreshTokenSubject has a non-null value
                    // – which means the new token is ready and we can retry the request again
                    return this.refreshTokenSubject
                        .filter(result => result !== null)
                        .take(1)
                        .switchMap(() => next.handle(this.addAuthenticationToken(request)));
                } else {
                    this.refreshTokenInProgress = true;
    
                    // Set the refreshTokenSubject to null so that subsequent API calls will wait until the new token has been retrieved
                    this.refreshTokenSubject.next(null);
    
                    // Call auth.refreshAccessToken(this is an Observable that will be returned)
                    return this.auth
                        .refreshAccessToken()
                        .switchMap((token: any) => {
                            //When the call to refreshToken completes we reset the refreshTokenInProgress to false
                            // for the next time the token needs to be refreshed
                            this.refreshTokenInProgress = false;
                            this.refreshTokenSubject.next(token);
    
                            return next.handle(this.addAuthenticationToken(request));
                        })
                        .catch((err: any) => {
                            this.refreshTokenInProgress = false;
    
                            this.auth.logout();
                            return Observable.throw(error);
                        });
                }
            });
        }
    
        addAuthenticationToken(request) {
            // Get access token from Local Storage
            const accessToken = this.auth.getAccessToken();
    
            // If access token is null this means that user is not logged in
            // And we return the original request
            if (!accessToken) {
                return request;
            }
    
            // We clone the request, because the original request is immutable
            return request.clone({
                setHeaders: {
                    Authorization: this.auth.getAccessToken()
                }
            });
        }
    }
    

    Don't forgot to change the links in the interceptor. It will be looking for refreshtoken inside the URL if it fails the second time.