TL;DR: In this article, we’ll get a quick refresher on NgRx basics and get up to speed on more features of the NgRx ecosystem. We'll then walk through how to add Auth0 authentication to an NgRx app. You can access the finished code for this tutorial on the ngrx-auth GitHub repository.
Adding Authentication to an NgRx Project
We’re caught up on the latest and greatest with NgRx, so let’s learn how to implement authentication in NgRx.
Authentication is a perfect example of shared state in an application. Authentication affects everything from being able to access client-side routes, get data from private API endpoints, and even what UI a user might see. It can be incredibly frustrating to keep track of authentication state in all these places. Luckily, this is exactly the kind of problem NgRx solves. Let’s walk through the steps. (We’ll be following the best practices Brandon Roberts describes in his ng-conf 2018 talk Authentication in NgRx.)
Our Sample App
We’re going to use a heavily simplified version of the official ngrx/platform book collection app to learn the basics of authentication in NgRx. To save time, most of the app is done, but we’ll be adding Auth0 to protect our book collection through an Auth0 login page.
You can access the code for this tutorial at the Auth0 Blog repository. To get started with the application, you'll need to clone the app, install the dependencies, and check out the "Starting point" commit in order to follow along. You can also run the application using the Angular CLI. You can do all of that with these commands:
git clone https://github.com/auth0-blog/ngrx-auth.git
cd ngrx-auth
git checkout 23c1b25
npm install
ng serve
You can now visit http://localhost:4200
to see the running application.
We'll also be taking advantage of NgRx schematics to help us set up authentication in this app, so I've added them to the project and set them as the default.
Our app currently has the book library (found in the books
folder), as well as some basic setup of NgRx already done. We also have the very beginnings of the AuthModule
. The UserHomeComponent
just lets us go to the book collection. This is what we'll protect with authentication.
The first phase of this task will be the basic scaffolding of the authentication state, reducer, and selectors. Then, we’ll add Auth0 to the application, create our UI components, and finally set up effects to make it all work.
Two Quick Asides
Before we get started, I need to make a couple of side notes.
First, because this article is focused on how to set up authentication in an NgRx app, we're going to abstract away and over-simplify things that aren't specific to NgRx. You'll learn the mechanics of adding the necessary state setup, adding actions and reducers to handle the flow of logging in, and using effects to retrieve and process a token.
Second, almost all NgRx tutorials need a caveat. In the real world, if you had an application this simple, you most likely wouldn't use NgRx. NgRx is extremely powerful, but the setup and learning curve involved prevent it from being a fit for every application, especially super simple ones.
Define Global Authentication State
The first step to adding authentication to an NgRx app is to define the piece of your state related to authentication. Do you need to keep track of a user profile, or whether there is a token stored in memory? Often the presence of a token or profile is enough to derive a “logged in” state. In any case, this is what you’ll want to attach to your main state.
We’re going to keep it very simple for this example and just detect the presence of a valid token. If we have a valid token, we'll toggle a global piece of state called isLoggedIn
.
To get started, let’s use a schematic to create a reducer in the main state
folder:
ng g reducer state/auth --no-spec
This is where we’ll define our authentication state, reducer, and selector functions. We can go ahead and update the state interface and initial state like this:
// src/app/state/auth.reducer.ts
// ...
// Leave everything else and update the following:
export interface State {
isLoggedIn: boolean;
}
export const initialState: State = {
isLoggedIn: false
};
// ...
We'll come back to the reducer in this file once we've defined our actions in the next section.
Next, import everything from the auth reducer file into the index.ts
file. That way we can add the authentication state to the overall application state and the authentication reducer to our action reducer map. The complete file will look like this right now:
// src/app/state/index.ts
import {
ActionReducerMap,
createFeatureSelector,
createSelector,
MetaReducer
} from '@ngrx/store';
import { environment } from '../../environments/environment';
import * as fromAuth from './auth.reducer';
export interface State {
auth: fromAuth.State;
}
export const reducers: ActionReducerMap<State> = {
auth: fromAuth.reducer
};
export const metaReducers: MetaReducer<State>[] = !environment.production
? []
: [];
In a more complex application, you’ll need to think through the state of other pages that use authentication in addition to your overall authentication state. For example, it’s common for login pages to need pending or error states. In our example, we’ll be using Auth0’s login page, which will redirect our application to a login screen to authenticate, then back to the application. Because of this, we won’t need to keep track of our login page state in this example.
Define Authentication Actions
Before we write our authentication reducer, let’s take Mike Ryan’s advice and write our actions first. Let’s walk through the flow of authentication in our example:
- User clicks the “Log In” button to trigger the Auth0 login screen.
- After going through the Auth0 authentication process, the user will be redirected to a
callback
route in our application. - That
CallbackComponent
will need to trigger an action to parse the hash fragment and set the user session. (Hint: this is where we’re altering our application state.) - Once that’s done, the user should be redirected to a
home
route. - If the user refreshes while logged in, the authentication state should persist.
I can identify several actions here:
- Login — to trigger the Auth0 login screen.
- LoginComplete — to handle the Auth0 callback.
- LoginSuccess — to update our authentication state
isLoggedIn
totrue
and navigate to thehome
route. - LoginFailure — to handle errors.
- CheckLogin
- to see if the user is still logged in on the server and update the state accordingly.
Notice that only the LoginSuccess action will modify our application state, which means that one will need to be in our reducers. The rest of these actions will use effects.
The logout process is similar:
- User clicks “Log Out” button to trigger a confirmation dialog.
- If the user clicks “Cancel,” the dialog will close.
- If the user clicks “Okay,” we’ll trigger the Auth0 logout process.
- Once logged out, Auth0 will redirect the user back to the application, which should default to the
login
route when not authenticated.
Can you think of what actions we’ll need? I spotted these:
- Logout — to trigger the logout confirmation dialog
- LogoutCancelled — to close the logout dialog.
- LogoutConfirmed — to tell Auth0 to log out and redirect home.
We’ll use the LogoutConfirmed action to reset our authentication state in a reducer in addition to telling Auth0 to log out. The rest will be handled with effects.
Add the Auth NgRx Feature
We've defined our actions and identified which will be handled by the reducer and which will be handled with effects. We'll need to add some new files to the AuthModule
and wire them up to our main application. Luckily, we can use schematics to make this much easier for us:
ng g feature auth/auth --group --no-spec --module auth
Let's break this command down to understand what's happening:
- g
is short for generate
in the CLI
- feature
is the NgRx schematic that generates actions, effects, and reducers for a new feature
- auth/auth
tells the CLI to put these files under the auth
folder and name them auth
- group
is an NgRx schematic option that groups all the files under respective folders (e.g. actions go in an actions
folder)
- no-spec
skips generation of test files
- Finally, module auth
wires up the feature in the AuthModule
We should end up with actions
, effects
, and reducers
folders inside of src/app/auth
with corresponding TypeScript files prefixed with auth
(e.g. auth.actions.ts
).
We actually won't be using the module-specific reducers
, so we can delete that file and folder (src/app/auth/reducers
) and the references to the auth reducer in auth.module.ts
. We'll need to delete the import of the reducer on line 9 and the reducer registration from the imports
array on line 22:
// src/app/auth/auth.module.ts
import * as fromAuth from './reducers/auth.reducer'; // remove this line
// ...
StoreModule.forFeature(‘auth’, fromAuth.reducer), // remove this line
// ...
It's helpful to stop and re-run ng serve
to prevent getting some errors at this stage.
Adding the Auth Actions
Let's create the actions we defined earlier in our newly created /auth/auth.actions.ts
file. We can delete the generated code, but we’ll follow the same pattern in our code: creating an enum with the action type strings, defining each action and optional payload, and finally defining an AuthActions
type that we can use throughout the application. The finished result will look like this:
// src/app/auth/actions/auth.actions.ts
// Finished:
import { Action } from '@ngrx/store';
export enum AuthActionTypes {
Login = '[Login Page] Login',
LoginComplete = '[Login Page] Login Complete',
LoginSuccess = '[Auth API] Login Success',
LoginFailure = '[Auth API] Login Failure',
CheckLogin = '[Auth] Check Login',
Logout = '[Auth] Confirm Logout',
LogoutCancelled = '[Auth] Logout Cancelled',
LogoutConfirmed = '[Auth] Logout Confirmed'
}
export class Login implements Action {
readonly type = AuthActionTypes.Login;
}
export class LoginComplete implements Action {
readonly type = AuthActionTypes.LoginComplete;
}
export class LoginSuccess implements Action {
readonly type = AuthActionTypes.LoginSuccess;
}
export class LoginFailure implements Action {
readonly type = AuthActionTypes.LoginFailure;
constructor(public payload: any) {}
}
export class CheckLogin implements Action {
readonly type = AuthActionTypes.CheckLogin;
}
export class Logout implements Action {
readonly type = AuthActionTypes.Logout;
}
export class LogoutConfirmed implements Action {
readonly type = AuthActionTypes.LogoutConfirmed;
}
export class LogoutCancelled implements Action {
readonly type = AuthActionTypes.LogoutCancelled;
}
export type AuthActions =
| Login
| LoginComplete
| LoginSuccess
| LoginFailure
| CheckLogin
| Logout
| LogoutCancelled
| LogoutConfirmed;
You can see that, in our example, we’re only using a payload for the LoginFailure
action to pass in an error message. If we were using a user profile, we’d need to define a payload in LoginComplete
in order to handle it in the reducer. Instead, we'll just be handling the token through an effect and an AuthService
we'll create later.
Do you notice how thinking through the actions in a feature also helps us identify our use cases? This keeps us from cluttering our reducers and application state because we shift most of the heavy lifting to effects.
Quick aside: if you'd like your application to continue to build at this step, you'll need to comment out the generated code in
auth.effects.ts
, since it now references the deleted schematic-generated action. Comment out lines 8 and 9:
// src/app/auth/effects/auth.effects.ts
// ...
// @Effect()
// loadFoos$ = this.actions$.pipe(ofType(AuthActionTypes.LoadAuths));
// ...
Define Authentication Reducer
Now let’s circle back to authentication reducer (state/auth.reducer.ts
). Since we’ve figured out what our actions are, we know that we’ll only need to change the state of our application when the login is successful and when the logout dialog is confirmed.
First, import the AuthActions
and AuthActionTypes
at the top of the file so we can use them in our reducer:
import { AuthActions, AuthActionTypes } from '@app/auth/actions/auth.actions';
Next, replace the current reducer function with this:
// /state/auth.reducer.ts
export function reducer(state = initialState, action: AuthActions): State {
switch (action.type) {
case AuthActionTypes.LoginSuccess:
return { ...state, isLoggedIn: true };
case AuthActionTypes.LogoutConfirmed:
return initialState; // the initial state has isLoggedIn set to false
default:
return state;
}
}
Notice that the LoginSuccess
case toggles the global isLoggedIn
state to true
, while the LogoutConfirmed
case returns us to our initial state, where isLoggedIn
is false
.
We’ll handle the rest of our actions using effects later on.
Define Authentication Selector
We've defined our isLoggedIn
state, actions related to our login process, and a reducer to modify our application state. But how do we access the status of isLoggedIn
throughout our application? For example, we’ll need to know whether we're authenticated in a route guard to control access to the home
and books
routes.
This is exactly what selectors are for. To create a selector, we’ll first define the pure function in /state/auth.reducer.ts
and then register the selector in index.ts
. Underneath our reducer function, we can add this pure function:
// src/app/state/auth.reducer.ts
// ...all previous code remains the same
export const selectIsLoggedIn = (state: State) => state.isLoggedIn;
We can then define our selectors at the bottom of state/index.ts
:
// src/app/state/index.ts
// ...all previous code remains the same
export const selectAuthState = createFeatureSelector<fromAuth.State>('auth');
export const selectIsLoggedIn = createSelector(
selectAuthState,
fromAuth.selectIsLoggedIn
);
We’ve now got all the basic scaffolding set up for an authenticated state. Let's start setting up our authentication process.