Protecting Routes with Guards in Angular
The last feature we are going to add is to prevent unauthenticated users from getting to the home page, and to auto redirect logged in users to the home page. Firebase will remember users automatically, so if we are already logged in we should go directly to the home page.
IMPORTANT: The guards feature I am about to show you is for user experience
only. These guards are implemented client side and would not stop a malicious
user from getting to any page they like. If you need to restrict certain users
from accessing certain things, this needs to happen on the backend — i.e. we
should never load the data into the application in the first place. A route
guard like the one we are implementing will not stop a malicious user from
accessing a route. This is why we have things like our Firestore Security Rules.
It doesn’t matter if a malicious user hacks their way past our route guard to
gain access to the main /home
page without logging in, as no data will load in
from Firebase if they haven’t authenticated. They will be on the /home
page,
but it will be empty.
Redirecting the User
Before we create our guard which will help keep our user where they are supposed to be, we are going to implement some redirects that react to the user’s auth state changing.
The problem in our application right now is that when we try to log in or create an account… nothing happens.
These operations are actually happening successfully, it’s just that our
application doesn’t care. What we need to do is have our components react to the
authState
from Firebase changing by triggering a navigation. We already have
a convenient way to do this. Whenever our authState
observable emits we set
our user
state in our AuthService
. This results in the following
possibilities:
- If we do not yet know if the user is authenticated, the
user()
signal will beundefined
- If the user is unauthenticated the
user()
signal will benull
- If the user is authenticated the
user()
signal will be theUser
from Firebase
This user()
signal updates automatically whenever we change the user’s authState
in any way — whether that happens because of a login, logout, create account, or anything else.
We also have a convenient way to trigger running some code, like a navigation, by using the effect
Signal API. That means we can just add some effects to each of our components to handle the navigation:
- The
LoginComponent
should react by navigating to thehome
route when theuser()
signal becomes truthy (i.e. notnull
orundefined
) - The
RegisterComponent
should do the same - The
HomeComponent
should react by navigating to theauth/login
route when theuser()
signal becomesnull
See if you can implement this before continuing.
Click here to reveal solution
Solution
Add the following
effect
to theLoginComponent
:
private router = inject(Router);
constructor() {
effect(() => {
if (this.authService.user()) {
this.router.navigate(['home']);
}
});
}
Add the following
effect
to theRegisterComponent
:
private router = inject(Router);
constructor() {
effect(() => {
if (this.authService.user()) {
this.router.navigate(['home']);
}
});
}
Add the following
effect
to theHomeComponent
:
private router = inject(Router);
constructor() {
effect(() => {
if (!this.authService.user()) {
this.router.navigate(['auth', 'login']);
}
});
}
Now if you try to use the application it should actually appear to work
correctly (although it will still look ugly). However, if you happen to have
logged in before you will automatically be taken to the home
route. This is
good, but we don’t actually have a way to trigger a log out, so you will be
stuck there.
Modify the template of the
HomeComponent
to reflect the following:
<div class="container">
<mat-toolbar color="primary">
<span class="spacer"></span>
<button mat-icon-button (click)="authService.logout()">
<mat-icon>logout</mat-icon>
</button>
</mat-toolbar>
<app-message-list [messages]="messageService.messages()" />
<app-message-input (send)="messageService.add$.next($event)" />
</div>
NOTE: You will need to add the appropriate imports from Angular Material to
the imports
for the component:
imports: [
MessageListComponent,
MessageInputComponent,
MatIconModule,
MatButtonModule,
MatToolbarModule,
],