4

I'm using popup login with Angular Material, when I added Angular Universal the auth guard is the problem. If some route is protected by auth guard the page just start pending and never finishes. Behavior without Angular Universal is when page is reloaded It just open up popup for login.

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(readonly auth: AuthService, public router: Router, private dialog: MatDialog, private store: Store<fromAuth.State>) {}

    /** Performs the user authentication prompting the user when neeed or resolving to the current authenticated user otherwise */

    public authenticate(action: loginAction = 'signIn') {
      return this.store.pipe(
        select(fromAuth.selectAuthState),
        take(1),
        switchMap(user => !user.user ? this.prompt(action) : of(user.user))
      ).toPromise();
    }

  public prompt(data: loginAction = 'signIn'): Promise<any> {

    return this.dialog.open<LogInComponent, loginAction>(LogInComponent, { data }).afterClosed().toPromise();
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    // Gets the authorization mode when specified
    // const mode = route.queryParamMap.get('authMode') || 'signIn';
    // Prompts the user for authentication
     return this.authenticate()
      .then(user => !!user);
  }
}

If I'm directly accessing ngrx store inside canActivate It's working but I want to work with .toPromise()

I'm using httponly cookies and on every reload Angular is sending http request to nodejs db to fetch user data. On every other route It's working as expected.

Todor Pavlovic
  • 160
  • 1
  • 11

2 Answers2

5

I think i have an easy and functional way, just verify in the auth guard, if it is a browser then continue with validations if it's not just continue, you will not have problems because the API will not sent to the server private info because no token is provided and then in the browser the whole process repeats and the token will be provided for the private data.

Example Auth Guard:

import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

import { CanActivate, Router } from '@angular/router';

@Injectable({
    providedIn: 'root'
})
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        @Inject(PLATFORM_ID) private platformId: any
    ) {}

    canActivate(): boolean {
        var canContinue: boolean = false;

        if (isPlatformBrowser(this.platformId)) {
            // Browser
            // Verify here if the token exists
            canContinue = !!localStorage.getItem('token');

            if (!canContinue) this.router.navigate(['/error/401']);
        }
        if (isPlatformServer(this.platformId)) {
            // Server side
            canContinue = true;
        }

        return canContinue;
    }
}

I hope that works for you

JuanDa237
  • 276
  • 3
  • 16
  • 1
    You can also redirect in the server side to the signIn not to the error page. – JuanDa237 Aug 20 '21 at 15:09
  • 1
    Instead of checking local storage (which can only be accessed client side) you could check for a cookie (which can be checked both on server and client side). You could keep all the logic in one place – Andrew Howard Dec 15 '22 at 21:30
3

currently not having too much time but some thoughts in short:

  1. If you are rendering the angular page on the server side (Angular Universal), why don't you handle the auth process on the server? Check if the user is logged in on each request and redirect the user on a login page - you would need a standalone login page instead of an overlay.

  2. I have multiple projects running with AuthGuard / User / Auth, and I would not recommend to return a promise but a boolean for canActivate.

Because:

  • Actually you do not need to check the login state for each request, as your session is usually valid for some time.
  • I usually store a copy or some compact information about the user when the login process is done.
  • Each eg. 1 minute there is a call to some auth endpoint "whoami" or "loggedin" which is returning whether the user-session is still valid.
  • If not the user is logged out.
  • If you want to keep a session alive when the browser or tab is closed you could store the user object in the local storage for some time.

-> This way you can only check if there is a current user object set in your canActivate method and return true or false.

So in my opinion: Either use server side rendering fully which means also check the auth state of the user in the backend. Or use angular as a real front end project and handle the auth process there. Mixing those two worlds will lead to some nasty problems and makes it unnecessarily complex to maintain.

Example Routing

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuardService, CanDeactivateGuard } from '@app/core/services';
import { AppPagesConfig } from '@app/config';
import * as Pages from '@app/pages';

const routes: Routes = [
    {
        path: 'dashboard',
        component: Pages.DashboardPage,
        canActivate: [AuthGuardService],
        data: {
            permissions: []
        }
    }, {
        path: 'calendar',
        children: [
            {
                path: '',
                redirectTo: AppPagesConfig.calendarPersonal.path,
                pathMatch: 'full'
            }, {
                path: AppPagesConfig.calendarPersonal.path,
                component: Pages.CalendarPersonalPage,
                canActivate: [AuthGuardService],
                data: {
                    permissions: 'Foo-canEdit'
                }
            }, {
                path: AppPagesConfig.calendarTeam.path,
                component: Pages.CalendarTeamPage,
                canActivate: [AuthGuardService],
                data: {
                    permissions: '0100'
                }
            },
        ]
    }, {
        path: 'contacts',
        children: [
            {
                path: '',
                redirectTo: 'private',
                pathMatch: 'full'
            }, {
                path: 'private',
                component: Pages.ContactsPage,
                canActivate: [AuthGuardService],
                canDeactivate: [CanDeactivateGuard],
                data: {
                    permissions: []
                }
            },
        ]
    }, {
        path: 'errors',
        children: [
            {
                path: '',
                redirectTo: '404',
                pathMatch: 'full'
            }, {
                path: '404',
                component: Pages.ErrorNotFoundPage
            }, {
                path: '403',
                component: Pages.ErrorNoPermissionsPage
            },
        ]
    }, {
        path: 'login',
        component: Pages.LoginPage
    }, {
        path: '**',
        component: Pages.ErrorNotFoundPage
    }
];

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

Example UserModel

import { uniq, union } from 'lodash';
import { UserBaseModel } from '@app/models/user-base';
import { Deserializable } from './deserializable.model';

export class User implements Deserializable {
    public permissions: string[];

    /**
     * Call this function to fill the model with data.
     */
    public deserialize(input: UserBaseModel): this {
        Object.assign(this, input);
        this.updateUserPermissions();

        return this;
    }

    /**
     * Checks if the user has all required permissions.
     */
    public hasPermissions(requiredPermissions: string[]): boolean {
        // If there where no required permissions given it is valid.
        if (!requiredPermissions || !requiredPermissions.length) {
            return true;
        }

        // If there are required permissions given but the user has no permissions at all it is always invalid.
        if (requiredPermissions.length && !this.permissions.length) {
            return false;
        }

        // Check the users permissions to contain all required permissions.
        for (const permission of requiredPermissions) {
            if (!this.permissions.includes(permission)) {
                return false;
            }

        }

        return true;
    }
}

Example AuthGuard

import { isEmpty } from 'lodash';
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { SessionService } from './session.service';

@Injectable({
    providedIn: 'root'
})
export class AuthGuardService implements CanActivate {
    constructor(
        private readonly _router: Router,
        private readonly sessionService: SessionService
    ) { }

    /**
     * Check if user is allowed to navigate to the new state.
     */
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
        const currentUser = this.sessionService.getCurrentUser();

        // Not logged in so redirect to login page.
        if (!currentUser) {
            this.sessionService.logoutUser();
            this._router.navigate(['/login']);

            return false;
        }

        // Route is not protected so continue (for routes without auth permission needed).
        if (isEmpty(route.data) || !route.data.permissions || !route.data.permissions.length) {
            return true;
        }

        // If the permissions do not match redirect.
        if (currentUser && !currentUser.hasPermissions(route.data.permissions)) {
            this._router.navigate(['/errors/403'], {
                queryParams: {
                    referrerUrl: state.url
                }
            });

            return false;
        }

        // If the permissions do match continue.
        if (currentUser && currentUser.hasPermissions(route.data.permissions)) {
            return true;
        }

        // If nothing matches log out the user.
        this.sessionService.logoutUser();
        this._router.navigate(['/login']);

        return false;
    }
}
notsure
  • 282
  • 1
  • 3
  • 8
  • Thank you very much, you are totally right for some things i'm gonna use popup for example for like...but routes needs to be guarded with login page and that is the best approach – Todor Pavlovic Dec 21 '20 at 09:06