Before we proceed to our main topic on authentication on this tutorial, I will assume that we already have an idea and have a solid understanding on the concepts of state management, If you are an experienced Angular developer, it's a large chance that you have encountered or have an experience on dealing with state management on Angular and the most common used and first released state management library for Angular is Ngrx, It is a very powerful library that allows us to manage data states in our app that brings source of truth capability to maintain consistency of the current state of components throughout our app, it also provides useful plugins such as undo-redo, time travel debugging and router integration for easier development process.
Let's us see how Ngrx works in our app with the flowchart provided.
in the chart above, the main building blocks of Ngrx are Actions, Effects, Reducers and the State/Store itself, we know that states are immutable and the only way to change its values are the reducers, the first step is a specific view (components in angular) will call or dispatch an action to that will map either on a specific effect or reducer, if it requires a http call, the action must be mapped to an effect to handle Observables and on the other hand if it only needs to mutate a specific data to a store, it should be mapped to a reducer and choose what date should be changed and this can be a chaining effect having effects can call another actions that will be mapped to another effects or reducer.
So what seems to be the problem here? yes it achieves the concept of source of truth and will help us maintain data state consistency, but analyzing closely on the flow, we can think forward that if our app grows, our code also grows and this is not inevitable in the effects part of Ngrx as more http calls may be required that will cause boilerplate or redundant written codes that will be harder to maintain in the future.
Dealing with NGXS
this is where NGXS comes in, it is also a state management library for Angular that offers the same plugins and capability for Angular but with lesser boilerplate code on our pockets. let's see the provided flowchart and compare the building blocks of ngxs to ngrx.
in the chart above we can see that the flow and architecture of ngxs is simpler compared to ngrx, the two main blocks is simply the Actions and the State/Store, the Effect and Reducers and Selectors of ngrx are combined into one and all the capabilities and roles of the three parts are placed in the Store itself. this reduces all written codes as the store has the capability to handle observables (Effects), select the state of a specific data (Selector) and mutate the value of the state (Reducer) using setState() or patchState() function. it also offers a more Angular approach as it defines the parts of the store using decorators at the same time the actions are now asynchronous having that they have their own lifecycle having the capability to listen to them if they are already complete.
To learn more about NGXS you can refer to their complete documentation with the link provided:
Configuring NGXS and setting up our JWT Authentication
Now our next step is to create a state that will handle our JWT authentication, let us consider what are the things we need to start in order for NGXS to handle user authentication.
A Selector that will allow us to retrieve state.
A Login Action that will send the user credentials to the login API Request.
A Logout Action that will remove the token in our storage and redirect to the login Page
the first thing we need to do is to install NGXS, assuming we already have our Angular application, we will execute the following command:
npm install @ngxs/store --save
the next thing we need to install is the storage plugin for NGXS, this will allow us to keep track of the state of our token in the local storage.
npm install @ngxs/storage-plugin --save
after successful installation, let us create now our folder structure in our angular project, in my example, I will use the three pillars (core, module, shared) structure in Angular that will be ready for lazy loading implementation.
Under core we will create the two folders and the following are the guards and services, under the service folder, we will create a login service that will provide http calls for the login API and for the guards folder, we will create an auth guard that will check if we are able to access the admin page by checking if a token is available in our state, we will use this guard on the admin routing later on our app routing.
login.service.ts
import { User } from './../../shared/models/User';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
const URL = 'http://localhost:3000';
@Injectable({
providedIn: 'root'
})
export class LoginService {
constructor(private http: HttpClient) { }
login(user: User): Observable<any> {
return this.http.post(`${URL}/login`, user);
}
}
auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Store } from '@ngxs/store';
import { AuthState } from 'src/app/modules/login/states/stores/login.state';
import { Observable } from 'rxjs';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private store: Store) {}
canActivate(): Observable<any> {
return this.store.select(AuthState.token);
}
}
The next folder is the shared folder, we will create a models folder under shared and place the User model here that will contain the username and password property of the user.
User.ts
export class User {
username: string;
password: string;
}
The last folder is the modules folder, this will hold our admin and login modules together with our actions and state that will store our token in the storage.
Creating Actions and State
under the modules folder create a states folder and under the states folder, the states folder will contain two folders namely actions and stores, the actions will contain all the action files and the stores will hold all the declared states related to our login module. Under actions folder, create a file named login.actions.ts, and Under stores folder, create a login.state.ts folder.
login.actions.ts
export class Login {
static readonly type = '[Auth] Login';
constructor(public payload: { username: string, password: string }) {}
}
export class Logout {
static readonly type = '[Auth] Logout';
}
login.state.ts
import { Login, Logout } from './../actions/login.actions';
import { LoginService } from './../../../../core/services/login.service';
import { State, Selector, StateContext, Action } from '@ngxs/store';
import { tap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
export class AuthStateModel {
token: string;
username: string;
}
@State<AuthStateModel>({
name: 'auth',
defaults: {
token: null,
username: null
}
})
export class AuthState {
@Selector()
// tslint:disable-next-line: typedef
static token(state: AuthStateModel) { return state.token; }
constructor(private loginService: LoginService) {}
@Action(Login)
login({ patchState }: StateContext<AuthStateModel>, { payload }:
Login) {
return this.loginService.login(payload).pipe(tap((result: {
token: string }) => {
patchState({ token: result.token, username: payload.username
});
},
catchError((err) => {
return throwError(`Invalid username or password`);
})
));
}
@Action(Logout)
logout({ setState, getState }: StateContext<AuthStateModel>) {
const { token } = getState();
setState(
{
username: null,
token: null
}
);
}
the login actions will contain two classes mainly the login and logout, this will be dispatched and allow the components and states to communicate.
The login.state can be divided into three parts. The first part is the AuthStateModel, this defines the properties that will be used for the store. the second part is the State itself, this is indicated by the @State decorator, this defines the name of the state and the default values of each properties. The third part is the AuthState class that holds the selectors and functions that will be called by a specific action. Selectors are static functions that allows states to be accessed by components, the functions denoted by an Action decorator are the one responsible for calling http requests and mutating the auth state.
Importing NgxsModule and states
We have successfully created our building blocks which is the the state and actions, now we want our app to know what are the states that should be used, in the login module, we will import the NgxsModule.forFeature([AuthState]) that will receive an array of states, in this case we only have the AuthState that we should pass.
We will also include the ReactiveFormsModule and FormsModule as the Logincomponent will use reactive forms.
import { LoginComponent } from './pages/login/login.component';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NgxsModule } from '@ngxs/store';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
import { LoginRoutingModule } from './login.routing';
import { ReactiveFormsModule, FormsModule} from '@angular/forms';
import { AuthState } from './states/stores/login.state';
@NgModule({
declarations: [LoginComponent],
imports: [
NgxsModule.forFeature([AuthState]),
CommonModule,
LoginRoutingModule,
ReactiveFormsModule,
FormsModule
]
})
export class LoginModule { }
Now lets define the routes in our App routing, we need to define the login module as our base route and we will use the AuthGuard we have created earlier on our admin route to check if we have a token.
import { AuthGuard } from './core/guards/auth.guard';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./modules/login/login.module').then((m)
=> m.LoginModule)
},
{
path: 'admin',
loadChildren: () =>
import('./modules/admin/admin.module').then((m) =>
m.AdminModule),
canActivate: [AuthGuard]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
The last thing we need to do is we want to update and track our token on the storage, we will use the Storage plugin by importing this on our app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HttpClientModule} from '@angular/common/http';
import { NgxsModule } from '@ngxs/store';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
@NgModule({
declarations: [AppComponent],
imports: [
NgxsModule.forRoot([]),
NgxsStoragePluginModule.forRoot({
key: 'auth.token'
}),
HttpClientModule,
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
and thats it! we have successfuly configured our Angular with NGXS and JWT authentication, we should just now create the login component with the login form that will dispatch the login action and the admin component where the user will be redirected on successful login, for the example, you can check out the full code here in my repository:
Hope you have learned in this blog, follow me on Github for more tutorials on javascript
Note* I have not provided a deployed api for authentication, you can create your own mock JWT endpoint or backend.
Comments