angular: router CanLoad Route guard broken for common use case

there is an issue filed but it is listed as a feature request and i think it needs to be more of a priotity: https://github.com/angular/angular/issues/12411

I’m submitting a … (check one with “x”)

[ x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior an angular app has a feature module that should be lazy loaded , a CanLoad guard needs to deal with the use case that the caller is not yet authenticated and needs to get a token before the guard can determine how the request will be handled. with a call to https://github.com/IdentityModel/oidc-client-js that uses https://github.com/IdentityModel/oidc-client-js a call to the STS server is used to return back to the angular app with a token. Can Load does not have the same access that Can Activate has to the router.navigate() data so the can load fails to get the user to the route.

Expected behavior

CanLoad should have the navigate route available in some reasonable form. in CanActivate we can access the url and pass it to the STS server and the STS server passes that back to the angular app and everything works.
CanLoad needs to be able to work in the same way, if the router data can not be the final url then we need some other “state” object that we can get and a way to load that state back into the router when the STS server returns the user token to the app.

this STS server case happens with any third party auth like Google,Facebook,Microsoft or Twitter

Minimal reproduction of the problem with instructions this needs several items to repro, i can’t give a sample right now but the parts are this: https://github.com/IdentityModel/oidc-client-js

and an angular app that has a feature module that should not be loaded unless the user has a token that grants access to a view inside the feature module.

canLoad needs to make a call this.mgr.signinRedirect({ data: state }) and the oidc manager ‘mgr’ returns to finish the callback and we then have code like this: this.mgr.signinRedirectCallback().then((user) => { // console.log("signed in", user); this.loggedIn = true; this.currentUser = user; this.userLoadededEvent.emit(user); // // if the login request was for a given view // we saved that before we redirected the user to the login page. // now we need to restore the view or route to the view // restore if they were already there, route to it if needed authentication to // get to that view // // console.log( " user.state = " , user.state ); if( user.state){ this.router.navigate([ user.state ]); }else{ this.router.navigate(['']); } }).catch( (err) => { console.log(err); });

What is the motivation / use case for changing the behavior? so that lazy loading of a feature module will work when using a third party authentication server via OIDC/ Oauth 2.

Please tell us about your environment: WIndows Server 2012, IIS 8.5 but this issue will happen on any web server.

  • Angular version: 2.0.X 4.xxx current version as of a week ago.

  • Browser: [all | Chrome XX | Firefox XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ] tested in chrome as of now, should happen with any browser.

  • Language: [all | TypeScript X.X | ES6/7 | ES5] TypeScript 2.3.2

  • Node (for AoT issues): node --version =
    6.9.1

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 7
  • Comments: 18 (2 by maintainers)

Most upvoted comments

My Current workaround (ng 6 with rxjs 6)


export class AuthGuard implements CanLoad{
  constructor( private router: Router ) {}
  public canLoad(route: Route): boolean {
    if(this.isAuthenticated){
      return true;
    }
    this.router.events.pipe(first(_ => _ instanceof NavigationCancel)).subscribe((event: NavigationCancel) => {
      // event.url is the target route the user was attempting to reach
      this.redirectToLogin(event.url);
    });
   // returning false cancels the navigation triggering the code above
    return false;
  }

  private isAuthenticated(): boolean {
   ...
  }
  private redirectToLogin(redirect:string):void {
     this.router.navigate(['/login'], {
      queryParams: {
        redirect: redirect
      }
    })
  }
}

@figuerres This bug negates the usefulness of canLoad guard in many cases.

Common case:

  1. The user bookmarks a URL into a lazy-loaded module that requires auth. (let’s call it 1.chunk)
  2. The user sends the url to a friend
  3. Sometimes later, the friend clicks on the link in step 1
  4. canLoad() kicks-in & wants to possibly prevent an unnecessary fetch of 1.chunk
  5. The friend is redirected to the login page to authenticated first
  6. The friend is authenticated, but the system has no recollection of where to redirect the friend to.
  7. Very unhappy friend.

The question is, how can we send a link to a lazy-loaded canLoad-guarded module and expect the recipient of the link to make it to the destination, post authentication, in this example?

Workaround: disable canLoad guards and let canActivate do the guarding. Yes, your friend just paid the price of loading a chuck, and probably decided not to even follow up with auth.

@Toxicable This is not a special case. Bookmarking in this example is a normal and a common use-case.

there’s an even more angular compliant solution without using window.location:

export class AuthnGuard implements CanLoad {
  constructor(private authnService: AuthnService, private router: Router) { }

  canLoad( ): Observable<boolean> | Promise<boolean> | boolean {
    const navigation = this.router.getCurrentNavigation();
    let url = '/';

    if (navigation) {
      url = navigation.extractedUrl.toString();
    }
    this.authnService.storeRedirectUrl(url);

Here is a more general statement of the problem:

canLoad cannot decide to load a module if the logic needs the route. for example if the module is Admin and the admin module has a view that it can route to called roles and the navigate route is called /admin/manage/roles can load does not know that the target is manage/roles it only sees that the admin module is the target.

also if a user enters a full url like https://myapp.com/myaccount/orders/list can load can use the location url to see what the target path is. but if you call navigate( ‘/myaccount/orders/list’) the can load function can not see that path / route

so in any case where the user enters the url can load can see the route but a call from navigate blinds can load to the the same information.

that cripples the utility of can load when the application does navigation. but can activate does not suffer this route blandness problem.

this difference means that the application has to be designed to not navigate into any feature that needs a can load guard if it needs the route.

After hours of frustration, I came up with a solution that in my case works perfectly.

canLoad(route: Route) {
        if (this.auth.authenticated) {
            return true;
        }

        this.auth.redirectUrl = window.location.pathname;
        this.router.navigate(['/login']);
        return new Promise((resolve) => {
            resolve();
        })
    }

auth is my Authentication Service auth.redirectUrl is where my Authentication Service navigates the user once authentication succeeds

Basically the Router only knows how to handle true or a Promise. If you return false it throws one or more errors.

@ivanpadavan that only works if the user has entered a url to load the application.

if the app is running and the code issues a navigate request that hit’s a can load the path has not been loaded yet, the navigation has not been sent to the browser. so that does not fix a very common problem.

the problem has to do with loading a module and that if the guard needs to call an auth that we do not have the full url for the child to use in the post login stage. this is not an issue if your login process does not need to do navigation. the other part is that the can load is only able to know the module to be loaded and not which view is the target so it can’t do permission filtering past module level. each item can be worked past

by contrast can activate has the full path .

i think they were adding the full path to can load to solve this.

Can load does not give the target route, it only gives the target module name. Can activate gives the target route. That is the problem, that is all that is needed to fix the can load interface