Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add a mobile access token in Angular 8 application?

I have deployed my application to UAT but I am not able to run my application in mobile because it directly goes to access-denied(401) page. I think it is due to a access token issue.

I have mainly 2 interceptors to handle my application. 1.Error interceptor - which handles when there is any route error or unauthorized error. 2. jwt - assign token which internally calls the authentication-service to get token.

Below is my service file where I am trying to get access token in session or local or from the window event by sending a postMessage that i am ready to receive the token.

authentication-service.ts

public getToken() {
    let accessToken = null;
     const auth = JSON.parse(sessionStorage.getItem('auth'));
    if (auth) {
        accessToken = auth.access_token;
    } elseif (accessToken == null || accessToken === undefined) {
        accessToken = localStorage.getItem('access_token');
    }

 window.addEventListener('message', function(event){
      // this event should have all the necessary tokens
 }, false);

 // once my page is loaded to indicate that I am ready to receive the message from server side.
  parent.postMessage({ askForToken:"true"}, "*");

  return accessToken;
}

I am sending a parent.postMessage to make the window.addEventListener to retrive the data but the event is not sending the token as expected.

I am doing all these above code implementation in authentication.service.ts, I am not sure if it is right way to do it.

Can anyone please suggest me the right way to implement this code and receive token suitably?

Please correct me as I am trying deployment with tokens for the first time.

like image 709
Onera Avatar asked Jun 21 '19 07:06

Onera


1 Answers

Source code and demo:

https://github.com/trungk18/angular-authentication-demo-with-lazy-loading

https://stackblitz.com/edit/angular-authentication-demo-with-lazy-loading

I will add the section to lazy load all the other modules when we first run. Meaning only login page will be loaded first time. After login, the next module will be loaded. It will save us a lot of bandwidth.


There is a User and I will store this whole object into the localStorage once I got it after login successfully.

The flow could be something like.

  1. Open the application

  2. AuthGuard will be triggered. If there is a user object in localStorage with the token, then activate the route. If not then route back to the login page.

  3. Upon the route get activated and started to make API call to the server, the JWTInterceptor will be trigger to send the token on each subsequent request.

  4. ErrorInterceptor check if there is 401, then delete the user from localStorage and reload the page. This handling more on the use case of somebody try to manually update the localStorage with a different token or object. If the token is correct with no modifier from the server, it should not happen.


model

export const ConstValue = { 
    ReturnUrl: "returnUrl",
    CurrentUser: "currentUser",    
}

export const ConstRoutingValue = {
    Login: "login"
}

export interface AICreateUser {
    firstName: string;
    lastName: string;
    email: string;
    password: string;    
    roleIds: string[]
}

export interface PartnerUser extends AICreateUser {
    id: string;    
    createdAt: string;    
    token: string;    
    featureSet: string[]
}

export interface AuthDto {
    email: string;
    password: string;
}

auth.service

export class AuthService {
    private _currentUserSubject: BehaviorSubject<User>;
    public currentUser: Observable<User>;

    public get currentUserVal(): User {
        return this._currentUserSubject.value;
    }

    get currentUserToken() {
        return this.currentUserVal.token;
    }

    constructor(private http: HttpClient) {
        this._currentUserSubject = new BehaviorSubject<User>(this.getUserFromLocalStorage());
        this.currentUser = this._currentUserSubject.asObservable();
    }

    login(username, password) {
        return this.http.post<any>(`/users/authenticate`, { username, password })
            .pipe(map(user => {
                // store user details and jwt token in local storage to keep user logged in between page refreshes
                localStorage.setItem(ConstValue.CurrentUser, JSON.stringify(user));
                this._currentUserSubject.next(user);
                return user;
            }));
    }

    logout() {
        // remove user from local storage and set current user to null
        localStorage.removeItem(ConstValue.CurrentUser);
        this._currentUserSubject.next(null);
    }

    private getUserFromLocalStorage(): User {
        try {
          return JSON.parse(localStorage.getItem(ConstValue.CurrentUser)!);
        } catch (error) {
          return null!;
        }
      }
}

And there is an JwtInterceptor to append the token into header every request

export class JwtInterceptor implements HttpInterceptor {
  constructor(private authenticationService: AuthService) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    let currentUser = this.authenticationService.currentUserVal;
    if (currentUser && currentUser.token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${currentUser.token}`
        }
      });
    }

    return next.handle(request);
  }
}

export const JWTInterceptorProvider = {
  provide: HTTP_INTERCEPTORS,
  useClass: JwtInterceptor,
  multi: true
};

ErrorInterceptor to check if there is 401 then delete the user from localStorage reload the page.

export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthService) {}

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if (err.status === 401) {
                // auto logout if 401 response returned from api
                this.authenticationService.logout();
                location.reload(true);
            }
            
            const error = err.error.message || err.statusText;
            return throwError(error);
        }))
    }
}

export const ErrorInterceptorProvider = { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }

Also there is a AuthGuard to make sure you have the token before activate a route. It must be included in all the route when configure the angular router, except the login page.

export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthService
    ) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const currentUserToken = this.authenticationService.currentUserToken;
        if (currentUserToken) {
            return true;
        }

        // not logged in so redirect to login page with the return url
        this.router.navigate(['/login'], { queryParams: { [ConstValue.ReturnUrl]: state.url }});
        return false;
    }
}

If I want to use the User object, I will take the public observable of currentUser inside the AuthService. For instance I want to show the user user first name on the list

export class AppComponent {
    currentUser: User;

    constructor(
        private router: Router,
        private authenticationService: AuthService
    ) {
        this.authenticationService.currentUser.subscribe(
            x => (this.currentUser = x)
        );
    }

    logout() {
        this.authenticationService.logout();
        this.router.navigate(["/login"]);
    }
}

I hope you get the idea from that.

like image 75
trungk18 Avatar answered Sep 27 '22 19:09

trungk18