Angular2: Global Guard (user has to be logged in always)
Asked Answered
C

2

9

I'm building an application where there's no access at all for unauthenticated users.

I wrote a LoggedInGuard, but now I have to add canActivate: [LoggedInGuard] to every route inside my router configuration (except the LoginComponent).

Is there a better way to get this working?


My file / module layout looks like this:

app/
  AppModule
  AppRoutingModule
  AppComponent

  authentication/
    AuthenticationModule
    AuthenticationRoutingModule
    LoginComponent

  contacts/
    ContactsModule
    ContactsRoutingModule
    ContactListComponent

  users/
    UsersModule
    UsersRoutingModule
    UserEditComponent

  ...

Maybe it's possible to create two separate routing spaces (one for login, one for the rest of the app) and apply the guard only to the rest of the app part?


I hope there's a simple solution.

Thanks in advance!

Coston answered 19/12, 2016 at 9:29 Comment(1)
if my answer does not make sense let me know man. I really think it should help you based on your circumstances. basically I am saying you can make one route with guard on it. Then make all of the other pages in the site which need to be secure a child of the parent route with the Guard.Tertias
T
10

I think I do it in a much more logical way. I guess it is an opinion. I separate my application by secured pages and public pages. I use templates for each set. So public component and secure component then put the guard on the secure template.

Make sure you are adding [Guard] to the full route that is in need of protection.

So when I secure a route I add the parents to app.routing.ts

const APP_ROUTES: Routes = [
    { path: '', redirectTo: '/home', pathMatch: 'full', },
    { path: '', component: PublicComponent, data: { title: 'Public Views' }, children: PUBLIC_ROUTES },
    { path: '', component: SecureComponent, canActivate: [Guard], data: { title: 'Secure Views' }, children: SECURE_ROUTES }
];



export const routing = RouterModule.forRoot(APP_ROUTES);

Make sure this line is noticed,

 { path: '', component: SecureComponent, canActivate: [Guard], data: { title: 'Secure Views' }, children: SECURE_ROUTES }

So I create 2 layouts

/public/ all public components

/public/public.routes.ts

/secure/ all secure components

/secure/secure.routes.ts

Secure routes

Notice that these routes do not need Guard now because it is handled by the template parent.

export const SECURE_ROUTES: Routes = [
    { path: '', redirectTo: 'overview', pathMatch: 'full' },
    { path: 'items', component: ItemsComponent },
    { path: 'overview', component: OverviewComponent },
    { path: 'profile', component: ProfileComponent },
];

Main routes in app.routing.ts

const APP_ROUTES: Routes = [
    { path: '', redirectTo: '/home', pathMatch: 'full', },
    { path: '', component: PublicComponent, data: { title: 'Public Views' }, children: PUBLIC_ROUTES },
    { path: '', component: SecureComponent, canActivate: [Guard], data: { title: 'Secure Views' }, children: SECURE_ROUTES }
];

export const routing = RouterModule.forRoot(APP_ROUTES);

And in the directory /layouts I create a layout that is

/layouts/secure.component.ts

/layouts/secure.component.html

/layouts/public.component.ts

/layouts/public.component.html

Everything is routed through the layout public or secure and [Guard] is on secure.

Then I handle authentication with a token in the local storage.

@Injectable()
export class Guard implements CanActivate {

    constructor(protected router: Router, protected auth: Auth ) {}

     canActivate() {
        if (localStorage.getItem('access_token')) {
            // logged in so return true
            return true;
        }
        // not logged in so redirect to login page
        this.router.navigate(['/home']);
        return false;
    }
}

Once I set my app up like this I put all my routes that need to be secure in the secure directory and the public routes in public. Then I create their routes in the public.routes.ts file or the secure.routes.ts file which are in their respective directory.

Tertias answered 19/12, 2016 at 9:35 Comment(7)
Interesting... are your path for secure and public component ok or did you mean { path: 'public', component: PublicComponent and { path: 'secure', component: SecureComponent ?Boettcher
Well the path could be whatever you want to make them. I use them the way they are that way I can control the routes from the child routesTertias
I missed the SecureComponent and PublicComponent.Boettcher
Sorry, I'm not Benjamin M. but I think your answer describe a very good pattern for Angular2 autentification flow.Boettcher
Oh whoops! Gotcha. Awesome man I'm glad I could help you. Let me know if you need anything else.Tertias
While working, this is currently not safe because child routes added via forChild are added as siblings to the forRoot routes thus not within the hierarchy and will not trigger the guard.Verboten
@ShlomiAssaf This definitely triggers the Guard in Angular 2. If there was an update at some point and caused this not to work than that is another story. But when this flow was being used 7 months ago it definitely worked. In fact it still works in that project. I appreciate your information though. If you can provide what version this stopped working I would be happy to add it to my answer so people know.Tertias
U
6

I was able to provide a set of global guards that spanned across multiple modules by moving the guards into a router event listener.

To make the event listener fire for all requests, I inserted it into the AppComponent.

Note that, in both the examples below, you can still add in custom guards for individual routes, and those will still work.

Without Guards

You can remove your use of guards and instead implement the logic directly in the event listener.

import { Component, OnInit } from '@angular/core';
import { Router, RoutesRecognized } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/filter';

// I use a service to keep track of the authentication ticket.
// Replace this with whatever mechanism you use.
import { AuthenticationService }  from './_services/index';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  constructor(
      private router: Router,
      private authService: AuthenticationService
  ) {}

  ngOnInit() {
    this.router.events
    .filter(event => event instanceof RoutesRecognized)
    .subscribe((event: RoutesRecognized) => {
      const url = event.urlAfterRedirects;

      // Public URLs don't need any kind of authorization.
      if (url === '/public' || url.startsWith('/public/') || url.startsWith('/public?')) {
        return;
      }

      // Everything else must be authenticated.
      if (!this.authService.isAuthenticated()) {
        // Allow for the login page to redirect back to the originally
        // requested page.
        this.router.navigate(['/public/login'], { queryParams: { returnUrl: state.url } });
      }
    });
  }
}

Requests that are passed to any sub-page of /public will pass through regardless, but any other request must have authentication, or it will redirect to /public/login.

Take care that the redirect page isn't in the protected area, or else the router will enter an infinite loop.

With Guards

The implementation below shows how I reused the existing guards. This is only if you need to keep them, or if it makes your code cleaner.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, RoutesRecognized, CanActivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';

// Reused guards.
import { AdminGuard, AuthGuard } from './_guards/index';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor(
      private route: ActivatedRoute,
      private router: Router,
      private adminGuard: AdminGuard,
      private authGuard: AuthGuard
  ) {}

  ngOnInit() {
    this.router.events
    .filter(event => event instanceof RoutesRecognized)
    .subscribe((event: RoutesRecognized) => {
      // Public pages don't require authentication.
      if (this.isSubPage(event, '/public')) {
        return;
      }

      // All other requests MUST be done through an
      // authenticated connection.  The guard performs
      // the redirection for us.
      if (!this.callCanActivate(event, this.authGuard)) {
        return;
      }

      // Administration pages require additional restrictions.
      // The guard performs the redirection for us.
      if (this.isSubPage(event, '/admin')) {
        if (!this.callCanActivate(event, this.adminGuard)) {
          return;
        }
      }
    });
  }

  callCanActivate(event: RoutesRecognized, guard: CanActivate) {
    return guard.canActivate(this.route.snapshot, event.state);
  }

  isSubPage(event: RoutesRecognized, parent: string) {
    const url = event.urlAfterRedirects;
    return (url === parent
        || url.startsWith(parent + '/')
        || url.startsWith(parent + '?'));
  }
}

This example is the same as the one above, but with added protection for the /admin area, which ensures that the user also has administrative permissions.

Underpants answered 31/3, 2017 at 18:1 Comment(1)
While "with guards" approach is working, this is currently not safe because child routes added via forChild are added as siblings to the forRoot routes thus not within the hierarchy and will not trigger the guard.Verboten

© 2022 - 2024 — McMap. All rights reserved.