Solvedangular router CanLoad Route guard broken for common use case
✔️Accepted Answer
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
}
})
}
}
Other Answers:
@figuerres This bug negates the usefulness of canLoad guard in many cases.
Common case:
- The user bookmarks a URL into a lazy-loaded module that requires auth. (let's call it
1.chunk
) - The user sends the url to a friend
- Sometimes later, the friend clicks on the link in step 1
canLoad()
kicks-in & wants to possibly prevent an unnecessary fetch of1.chunk
- The friend is redirected to the login page to authenticated first
- The friend is authenticated, but the system has no recollection of where to redirect the friend to.
- 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.
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.
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);
...
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.
there is an issue filed but it is listed as a feature request and i think it needs to be more of a priotity:
#12411
I'm submitting a ... (check one with "x")
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