146

I come from the Asp.Net MVC world where users trying to access a page they are not authorized are automatically redirected to the login page.

I am trying to reproduce this behavior on Angular. I came accross the @CanActivate decorator, but it results in the component not rendering at all, no redirection.

My question is the following:

  • Does Angular provide a way to achieve this behaviour?
  • If so, how? Is it a good practice?
  • If not, what would be the best practice for handling user authorization in Angular?
ishandutta2007
  • 16,676
  • 16
  • 93
  • 129
Amaury
  • 1,623
  • 2
  • 11
  • 7

7 Answers7

158

Here's an updated example using Angular 4 (also compatible with Angular 5 - 8)

Routes with home route protected by AuthGuard

import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login/index';
import { HomeComponent } from './home/index';
import { AuthGuard } from './_guards/index';

const appRoutes: Routes = [
    { path: 'login', component: LoginComponent },

    // home route protected by auth guard
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

export const routing = RouterModule.forRoot(appRoutes);

AuthGuard redirects to login page if user isn't logged in

Updated to pass original url in query params to login page

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private router: Router) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        if (localStorage.getItem('currentUser')) {
            // logged in so return true
            return true;
        }

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

For the full example and working demo you can check out this post

Jason Watmore
  • 4,521
  • 2
  • 32
  • 36
  • 7
    I have a follow up Q, isn't if setting an arbitrary value to `currentUser` in the `localStorage` would still be able to access the protected route? eg. `localStorage.setItem('currentUser', 'dddddd')` ? – jsdecena Jul 12 '17 at 12:48
  • 2
    It would bypass client-side security. But it would also clear out the token which would be necessary for server-side transactions, so no useful data could be extracted from the application. – Matt Meng May 15 '18 at 20:11
  • Even after logged-in I am able to go login page If I give like http://localhost:4200/#/login, can we navigate instead of login if user is already logged-in, please let us know – Code_S Jan 19 '21 at 18:27
  • This should be the best answer. – Shaze Apr 22 '22 at 09:04
93

Update: I've published a full skeleton Angular 2 project with OAuth2 integration on Github that shows the directive mentioned below in action.

One way to do that would be through the use of a directive. Unlike Angular 2 components, which are basically new HTML tags (with associated code) that you insert into your page, an attributive directive is an attribute that you put in a tag that causes some behavior to occur. Docs here.

The presence of your custom attribute causes things to happen to the component (or HTML element) that you placed the directive in. Consider this directive I use for my current Angular2/OAuth2 application:

import {Directive, OnDestroy} from 'angular2/core';
import {AuthService} from '../services/auth.service';
import {ROUTER_DIRECTIVES, Router, Location} from "angular2/router";

@Directive({
    selector: '[protected]'
})
export class ProtectedDirective implements OnDestroy {
    private sub:any = null;

    constructor(private authService:AuthService, private router:Router, private location:Location) {
        if (!authService.isAuthenticated()) {
            this.location.replaceState('/'); // clears browser history so they can't navigate with back button
            this.router.navigate(['PublicPage']);
        }

        this.sub = this.authService.subscribe((val) => {
            if (!val.authenticated) {
                this.location.replaceState('/'); // clears browser history so they can't navigate with back button
                this.router.navigate(['LoggedoutPage']); // tells them they've been logged out (somehow)
            }
        });
    }

    ngOnDestroy() {
        if (this.sub != null) {
            this.sub.unsubscribe();
        }
    }
}

This makes use of an Authentication service I wrote to determine whether or not the user is already logged in and also subscribes to the authentication event so that it can kick a user out if he or she logs out or times out.

You could do the same thing. You'd create a directive like mine that checks for the presence of a necessary cookie or other state information that indicates that the user is authenticated. If they don't have those flags you are looking for, redirect the user to your main public page (like I do) or your OAuth2 server (or whatever). You would put that directive attribute on any component that needs to be protected. In this case, it might be called protected like in the directive I pasted above.

<members-only-info [protected]></members-only-info>

Then you would want to navigate/redirect the user to a login view within your app, and handle the authentication there. You'd have to change the current route to the one you wanted to do that. So in that case you'd use dependency injection to get a Router object in your directive's constructor() function and then use the navigate() method to send the user to your login page (as in my example above).

This assumes that you have a series of routes somewhere controlling a <router-outlet> tag that looks something like this, perhaps:

@RouteConfig([
    {path: '/loggedout', name: 'LoggedoutPage', component: LoggedoutPageComponent, useAsDefault: true},
    {path: '/public', name: 'PublicPage', component: PublicPageComponent},
    {path: '/protected', name: 'ProtectedPage', component: ProtectedPageComponent}
])

If, instead, you needed to redirect the user to an external URL, such as your OAuth2 server, then you would have your directive do something like the following:

window.location.href="https://myserver.com/oauth2/authorize?redirect_uri=http://myAppServer.com/myAngular2App/callback&response_type=code&client_id=clientId&scope=my_scope
Michael Oryl
  • 20,856
  • 14
  • 77
  • 117
  • 4
    It works! thanks! I also found another method here - https://github.com/auth0/angular2-authentication-sample/blob/master/src/app/LoggedInOutlet.ts I cannot say which method is better, but maybe someone will find it useful too. – Sergey Jan 03 '16 at 17:08
  • 3
    Thank you ! I also added a new route containing a parameter /protected/:returnUrl, returnUrl being the location.path() intercepted at ngOnInit of the directive. This allows to navigate the user after login to the originally prompted url. – Amaury Jan 11 '16 at 10:18
  • 2
    See answers below for a simple solution. Anything this common (redirect if not authenticated) should have a simple solution with a single sentence answer. – Rick O'Shea Aug 01 '16 at 01:56
  • 8
    Note: This answer addresses a beta or release-candidate version of Angular 2 and is no longer applicable for Angular 2 final. – jbandi Sep 23 '16 at 13:26
  • 2
    There's a much better solution to this now using Angular Guards – mwilson Sep 19 '17 at 01:35
  • A better solution is Jason's https://stackoverflow.com/a/38968963/3197387 – Sisir Apr 08 '21 at 13:12
58

Usage with the final router

With the introduction of the new router it became easier to guard the routes. You must define a guard, which acts as a service, and add it to the route.

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { UserService } from '../../auth';

@Injectable()
export class LoggedInGuard implements CanActivate {
  constructor(user: UserService) {
    this._user = user;
  }

  canActivate() {
    return this._user.isLoggedIn();
  }
}

Now pass the LoggedInGuard to the route and also add it to the providers array of the module.

import { LoginComponent } from './components/login.component';
import { HomeComponent } from './components/home.component';
import { LoggedInGuard } from './guards/loggedin.guard';

const routes = [
    { path: '', component: HomeComponent, canActivate: [LoggedInGuard] },
    { path: 'login', component: LoginComponent },
];

The module declaration:

@NgModule({
  declarations: [AppComponent, HomeComponent, LoginComponent]
  imports: [HttpModule, BrowserModule, RouterModule.forRoot(routes)],
  providers: [UserService, LoggedInGuard],
  bootstrap: [AppComponent]
})
class AppModule {}

Detailed blog post about how it works with the final release: https://medium.com/@blacksonic86/angular-2-authentication-revisited-611bf7373bf9

Usage with the deprecated router

A more robust solution is to extend the RouterOutlet and when activating a route check if the user is logged in. This way you don't have to copy and paste your directive to every component. Plus redirecting based on a subcomponent can be misleading.

@Directive({
  selector: 'router-outlet'
})
export class LoggedInRouterOutlet extends RouterOutlet {
  publicRoutes: Array;
  private parentRouter: Router;
  private userService: UserService;

  constructor(
    _elementRef: ElementRef, _loader: DynamicComponentLoader,
    _parentRouter: Router, @Attribute('name') nameAttr: string,
    userService: UserService
  ) {
    super(_elementRef, _loader, _parentRouter, nameAttr);

    this.parentRouter = _parentRouter;
    this.userService = userService;
    this.publicRoutes = [
      '', 'login', 'signup'
    ];
  }

  activate(instruction: ComponentInstruction) {
    if (this._canActivate(instruction.urlPath)) {
      return super.activate(instruction);
    }

    this.parentRouter.navigate(['Login']);
  }

  _canActivate(url) {
    return this.publicRoutes.indexOf(url) !== -1 || this.userService.isLoggedIn()
  }
}

The UserService stands for the place where your business logic resides whether the user is logged in or not. You can add it easily with DI in the constructor.

When the user navigates to a new url on your website, the activate method is called with the current Instruction. From it you can grab the url and decide whether it is allowed or not. If not just redirect to the login page.

One last thing remain to make it work, is to pass it to our main component instead of the built in one.

@Component({
  selector: 'app',
  directives: [LoggedInRouterOutlet],
  template: template
})
@RouteConfig(...)
export class AppComponent { }

This solution can not be used with the @CanActive lifecycle decorator, because if the function passed to it resolves false, the activate method of the RouterOutlet won't be called.

Also wrote a detailed blog post about it: https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492

VuesomeDev
  • 4,095
  • 2
  • 34
  • 44
  • 2
    Also wrote a more detailed blog post about it https://medium.com/@blacksonic86/authentication-in-angular-2-958052c64492 – VuesomeDev Mar 07 '16 at 17:17
  • Hello @Blacksonic. Just started digging into ng2. I followed your suggestion but ended up getting this error during gulp-tslint: `Failed to lint .router-outlet.ts[15,28]. In the constructor of class "LoggedInRouterOutlet", the parameter "nameAttr" uses the @Attribute decorator, which is considered as a bad practice. Please, consider construction of type "@Input() nameAttr: string".` Couldn't figure what to change in the constructor ("_parentRounter") to get rid of this message. Any thoughts? – leovrf Mar 08 '16 at 13:20
  • the declaration is copied from the underlying built in object RouterOutlet to have the same signature as the extended class, i would disable specific tslint rule for this line – VuesomeDev Mar 08 '16 at 13:53
  • I found a reference on mgechev [style-guide](https://github.com/mgechev/angular2-style-guide) (look for "Prefer inputs over the @Attribute parameter decorator"). Changed the line to `_parentRouter: Router, @Input() nameAttr: string,` and tslint no longer raises the error. Also replaced the "Attribute" import to "Input" from angular core. Hope this helps. – leovrf Mar 08 '16 at 14:19
  • I just sticked to it because the class it extends from has this signature, but regarding how it works in this case is identical – VuesomeDev Mar 08 '16 at 15:23
  • `User` doesn't work in this case, I did digging and i found that `ROUTE_DIRECTIVES` had imported `routeLink`. Correct me if I am wrong - i need to replace `ROUTE_DIRECTIVES` to `LoggedInRouterOutlet`. – Akshay Mar 14 '16 at 13:26
  • 1
    There is a problem with 2.0.0-rc.1 because RouterOutlet is not exported and there is no possibility for extending it – mkuligowski Jun 01 '16 at 12:25
  • are you using the router or router-deprecated? – VuesomeDev Jun 01 '16 at 12:42
  • I am getting the error `Cannot reuse an outlet that does not contain a component.` – Double H Jul 10 '16 at 12:10
  • hi Blacksonic, i used the method you showed via your blog link... everything is working fine but the only issue is after successfully logging in I am unable to redirect to another protected page... User Service - isLogged boolean is changed to true before redirecting..... and also no error on the console but still the redirection doesnt take place ... any idea why ?? - if I try accessing the protected page directly then also is redirects to login - as if I am not logged in – Abdeali Chandanwala Sep 17 '16 at 08:35
  • I got the issue, the object where I change the boolean flag to true and router-outlet copy of the user.service object is different... thats why router outlet always gives false ... trying to solve it .. let me know if you can help on it – Abdeali Chandanwala Sep 17 '16 at 08:43
  • In the new router example what happens if you have multiple unguarded routes? So for example you never declare it should go to Login anywhere so I am assuming it is going to the first insecure route, but that doesn't help if I have multiple insecure routes and want to go to say the second one. – Jackie Nov 29 '16 at 14:20
  • Here is a more complete solution http://jasonwatmore.com/post/2016/12/08/angular-2-redirect-to-previous-url-after-login-with-auth-guard – nima Feb 19 '17 at 17:59
56

Please, do not override Router Outlet! It's a nightmare with latest router release (3.0 beta).

Instead use the interfaces CanActivate and CanDeactivate and set the class as canActivate / canDeactivate in your route definition.

Like that:

{ path: '', component: Component, canActivate: [AuthGuard] },

Class:

@Injectable()
export class AuthGuard implements CanActivate {

    constructor(protected router: Router, protected authService: AuthService)
    {

    }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean {

        if (state.url !== '/login' && !this.authService.isAuthenticated()) {
            this.router.navigate(['/login']);
            return false;
        }

        return true;
    }
}

See also: https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

Nilz11
  • 1,242
  • 14
  • 16
  • 2
    Nice one, @Blacksonic's answer was working for me perfectly with the deprecated router. I had to refactor a lot after upgrading to the new router. Your solution is just what I needed! – evandongen Jul 15 '16 at 08:11
  • I cant get canActivate to work in my app.component. I'm looking to redirect user if not authenticated. This is the version of the router I have (If i need to update it how do i do that using git bash command line?) Version I have: "@angular/router": "2.0.0-rc.1" – AngularM Jul 23 '16 at 12:24
  • can i use the same class (AuthGuard) to protect another component route? – tsiro Jan 14 '17 at 19:43
5

Following the awesome answers above I would also like to CanActivateChild: guarding child routes. It can be used to add guard to children routes helpful for cases like ACLs

It goes like this

src/app/auth-guard.service.ts (excerpt)

import { Injectable }       from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild
}                           from '@angular/router';
import { AuthService }      from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router:     Router) {}

  canActivate(route: ActivatedRouteSnapshot, state:    RouterStateSnapshot): boolean {
    let url: string = state.url;
    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state:  RouterStateSnapshot): boolean {
    return this.canActivate(route, state);
  }

/* . . . */
}

src/app/admin/admin-routing.module.ts (excerpt)

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(adminRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AdminRoutingModule {}

This is taken from https://angular.io/docs/ts/latest/guide/router.html#!#can-activate-guard

notnotundefined
  • 3,493
  • 3
  • 30
  • 39
3

Refer this code, auth.ts file

import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';
import {  } from 'angular-2-local-storage';
import { Router } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(public localStorageService:LocalStorageService, private router: Router){}
canActivate() {
// Imaginary method that is supposed to validate an auth token
// and return a boolean
var logInStatus         =   this.localStorageService.get('logInStatus');
if(logInStatus == 1){
    console.log('****** log in status 1*****')
    return true;
}else{
    console.log('****** log in status not 1 *****')
    this.router.navigate(['/']);
    return false;
}


}

}
// *****And the app.routes.ts file is as follow ******//
      import {  Routes  } from '@angular/router';
      import {  HomePageComponent   } from './home-page/home- page.component';
      import {  WatchComponent  } from './watch/watch.component';
      import {  TeachersPageComponent   } from './teachers-page/teachers-page.component';
      import {  UserDashboardComponent  } from './user-dashboard/user- dashboard.component';
      import {  FormOneComponent    } from './form-one/form-one.component';
      import {  FormTwoComponent    } from './form-two/form-two.component';
      import {  AuthGuard   } from './authguard';
      import {  LoginDetailsComponent } from './login-details/login-details.component';
      import {  TransactionResolver } from './trans.resolver'
      export const routes:Routes    =   [
    { path:'',              component:HomePageComponent                                                 },
    { path:'watch',         component:WatchComponent                                                },
    { path:'teachers',      component:TeachersPageComponent                                         },
    { path:'dashboard',     component:UserDashboardComponent,       canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'formone',       component:FormOneComponent,                 canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'formtwo',       component:FormTwoComponent,                 canActivate: [AuthGuard],   resolve: { dashboardData:TransactionResolver } },
    { path:'login-details', component:LoginDetailsComponent,            canActivate: [AuthGuard]    },

]; 
sojan
  • 67
  • 1
  • 10
2

1. Create a guard as seen below. 2. Install ngx-cookie-service to get cookies returned by external SSO. 3. Create ssoPath in environment.ts (SSO Login redirection). 4. Get the state.url and use encodeURIComponent.

import { Injectable } from '@angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from 
  '@angular/router';
import { CookieService } from 'ngx-cookie-service';
import { environment } from '../../../environments/environment.prod';

@Injectable()
export class AuthGuardService implements CanActivate {
  private returnUrl: string;
  constructor(private _router: Router, private cookie: CookieService) {}

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.cookie.get('MasterSignOn')) {
      return true;
    } else {
      let uri = window.location.origin + '/#' + state.url;
      this.returnUrl = encodeURIComponent(uri);      
      window.location.href = environment.ssoPath +  this.returnUrl ;   
      return false;      
    }
  }
}
M.Laida
  • 1,818
  • 1
  • 13
  • 19