currently not having too much time but some thoughts in short:
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.
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;
}
}