In this blog post I want to describe how you can add authentication and authorization in your Angular app using NgRx. We are using an ASP.NET Core backend to get our secure data from and a custom STS which we use for the authentication and the id token as well as an access token.

This is a follow up post of Authentication and Authorization with Angular and ASP.NET Core using OIDC and OAuth2 so if you want to get into the complete setup you might give this blog post a shot first :-) We will build this one up on the app which was mentioned in the references blog post.

In this post we are gonna focus on the configuration and the Angular Application itself using NgRx.

Disclaimer: In this blog we will use an Angular library which I wrote some parts of. But the principles are best practice and uses a standard which can be applied to any Angular application no matter what libraries you use.

The code can be found here https://github.com/FabianGosebrink/angular-oauth2-oidc-sample/tree/master/client/angular-oidc-oauth2-ngrx

TOC

Adding the stores

We will add two stores to the sample angular application: one for holding the authentication state auth and one for providing and getting the data, called data.

We can add a folder to the root and call it store placing two folders in it called auth and data to have a nice separation of the different parts of our store.

Both folders get a barrel file index.ts to export what is needed. And the main store folder gets a barrel file, too to keep things nice and clean.

.
├── store
│   ├── auth
│   │   └── index.ts
│   ├── data
│   │   └── index.ts
│   └── index.ts
...

Adding the auth actions

We will add a few actions here to get along with the authentication. To keep it easy we will add actions for checkAuth, login and logout with the appropriate complete actions.

import { createAction, props } from '@ngrx/store';

export const checkAuth = createAction('[Auth] checkAuth');
export const checkAuthComplete = createAction(
  '[Auth] checkAuthComplete',
  props<{ isLoggedIn: boolean }>()
);
export const login = createAction('[Auth] login');
export const loginComplete = createAction(
  '[Auth] loginComplete',
  props<{ profile: any; isLoggedIn: boolean }>()
);
export const logout = createAction('[Auth] logout');
export const logoutComplete = createAction('[Auth] logoutComplete');

These are the actions we are gonna use. loginComplete is carrying the profile as well as if the user is logged in or not and if the checkAuthComplete kicks in we provide the value if the user is logged in or not.

The actions are placed in a file auth.actions.ts lying in the store/auth folder.

Creating the auth state and reducer

Before we can define the reducer we have to define the state which holds the properties for authentication we need in our app. To keep it easy we start with a isLoggedIn property and a property holding the userProfile profile. The name of the feature is called auth and so is the property on the state object of our app. We will expose this as a variable here as well.

export const authFeatureName = 'auth';

export interface AuthState {
  profile: any;
  isLoggedIn: boolean;
}

export const initialAuthState: AuthState = {
  isLoggedIn: false,
  profile: null,
};

Now we can build the reducer right underneath this code and place it in a file called auth.reducer.ts which is placed right beside the actions file in the store/auth folder.

We change our state when the login is complete (action loginComplete) and when the user wants to logout (action logout).

import { createReducer, on, Action } from '@ngrx/store';
import * as authActions from './auth.actions';

export const authFeatureName = 'auth';

export interface AuthState {
  profile: any;
  isLoggedIn: boolean;
}

export const initialAuthState: AuthState = {
  isLoggedIn: false,
  profile: null,
};

const authReducerInternal = createReducer(
  initialAuthState,

  on(authActions.loginComplete, (state, { profile, isLoggedIn }) => {
    return {
      ...state,
      profile,
      isLoggedIn,
    };
  }),
  on(authActions.logout, (state, {}) => {
    return {
      ...state,
      profile: null,
      isLoggedIn: false,
    };
  })
);

export function authReducer(state: AuthState | undefined, action: Action) {
  return authReducerInternal(state, action);
}

In the end we are exporting the authReducer with a function.

Before we now head to the corresponding effects we have to add the service with methods the effects can call.

Adding the auth service

Install the lib angular-auth-oidc-client with

npm install angular-auth-oidc-client

Having done this we can create an auth.service.ts file in a services folder and abstract the usage of the library.

import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { OidcSecurityService } from 'angular-auth-oidc-client';

@Injectable({ providedIn: 'root' })
export class AuthService {
  constructor(private oidcSecurityService: OidcSecurityService) {}

  get isLoggedIn() {
    return this.oidcSecurityService.isAuthenticated$;
  }

  get token() {
    return this.oidcSecurityService.getToken();
  }

  get userData() {
    return this.oidcSecurityService.userData$;
  }

  checkAuth() {
    return this.oidcSecurityService.checkAuth();
  }

  doLogin() {
    return of(this.oidcSecurityService.authorize());
  }

  signOut() {
    this.oidcSecurityService.logoffAndRevokeTokens();
  }
}

Your folder should now look like this

.

├── services
│   ├── auth.service.ts
├── store
│   ├── auth
│   │   ├── auth.actions.ts
│   │   ├── auth.reducer.ts
│   │   └── index.ts
│   ├── data
│   │   └── index.ts
│   └── index.ts

Now we can build the effects for the authentication:

Creating the auth effects

For the actions we have we will add the corresponding effects and inject the AuthService we created. If we navigate away from our app to an external source (like the STS in this case when logging in and out) we have to config the actions with { dispatch: false } to clarify that we will NOT return an action to the actions stream inside our app.

So when the action login is dispatched we react with calling doLogin() from our AuthService. as this redirects, we configure it with { dispatch: false }

login$ = createEffect(
  () =>
    this.actions$.pipe(
      ofType(fromAuthActions.login),
      switchMap(() => this.authService.doLogin())
    ),
  { dispatch: false }
);

The checkAuth action is called everytime we want to check whether we are authenticated or not. It is also called when the application starts so we will react to that accordingly as well, returning the corresponding ...Complete action.

checkauth$ = createEffect(() =>
  this.actions$.pipe(
    ofType(fromAuthActions.checkAuth),
    switchMap(() =>
      this.authService
        .checkAuth()
        .pipe(
          map((isLoggedIn) => fromAuthActions.checkAuthComplete({ isLoggedIn }))
        )
    )
  )
);

checkAuthComplete$ = createEffect(() =>
  this.actions$.pipe(
    ofType(fromAuthActions.checkAuthComplete),
    switchMap(({ isLoggedIn }) => {
      if (isLoggedIn) {
        return this.authService.userData.pipe(
          map((profile) =>
            fromAuthActions.loginComplete({ profile, isLoggedIn })
          )
        );
      }
      return of(fromAuthActions.logoutComplete());
    })
  )
);

If the checkAuthComplete action is returning true for isLoggedIn we are asking the service for the userData with this.authService.userData and return the loginComplete action with both profile and isLoggedIn on it. This gets handled by the reducer then and sets the state accordingly.

The logout action will call the signOut method on the AuthService and handle that event then. Putting it together these are our effects:

/* imports */

@Injectable()
export class AuthEffects {
  constructor(
    private actions$: Actions,
    private authService: AuthService,
    private router: Router
  ) {}

  login$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(fromAuthActions.login),
        switchMap(() => this.authService.doLogin())
      ),
    { dispatch: false }
  );

  checkauth$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromAuthActions.checkAuth),
      switchMap(() =>
        this.authService
          .checkAuth()
          .pipe(
            map((isLoggedIn) =>
              fromAuthActions.checkAuthComplete({ isLoggedIn })
            )
          )
      )
    )
  );

  checkAuthComplete$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromAuthActions.checkAuthComplete),
      switchMap(({ isLoggedIn }) => {
        if (isLoggedIn) {
          return this.authService.userData.pipe(
            map((profile) =>
              fromAuthActions.loginComplete({ profile, isLoggedIn })
            )
          );
        }
        return of(fromAuthActions.logoutComplete());
      })
    )
  );

  logout$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromAuthActions.logout),
      tap(() => this.authService.signOut()),
      map(() => fromAuthActions.logoutComplete())
    )
  );

  logoutComplete$ = createEffect(
    () =>
      this.actions$.pipe(
        ofType(fromAuthActions.logoutComplete),
        tap(() => this.router.navigate(['/']))
      ),
    { dispatch: false }
  );
}

So we have everything prepared now! What is missing are the selectors to get a nicer access to the values form our store.

Adding the auth selectors

We have two properties to provide here and we will wrap them in their according selectors. Place them in a file called auth.selectors.ts in the store/auth folder. We are using the authFeatureName variable now exposed by the reducer to create a featureSelector returning the auth part of the state we are interested in and then create the selectors for the specific properties.

import { AuthState, authFeatureName } from './auth.reducer';
import { createFeatureSelector, createSelector } from '@ngrx/store';

export const getAuthFeatureState = createFeatureSelector(authFeatureName);

export const selectIsAuthenticated = createSelector(
  getAuthFeatureState,
  (state: AuthState) => state.isLoggedIn
);

export const selectUserInfo = createSelector(
  getAuthFeatureState,
  (state: AuthState) => state.profile
);

The index.ts file is exporting all the stuff we did.

export * from './auth.actions';
export * from './auth.effects';
export * from './auth.reducer';
export * from './auth.selectors';
.
├── services
│   ├── auth.service.ts
├── store
│   ├── auth
│   │   ├── auth.actions.ts
│   │   ├── auth.effects.ts
│   │   ├── auth.reducer.ts
│   │   ├── auth.selectors.ts
│   │   └── index.ts
│   ├── data
│   │   └── index.ts
│   └── index.ts

Creating the store for data

We are using the same files and techniques for getting the data from the server in the end using a data.service.ts which is also placed in the services folder. We are adding only one (two with the complete action) action inside the action file, a reducer, the effect for getting the http data and the selector for selecting the data from the state.

import { createAction, props } from '@ngrx/store';

export const getData = createAction('[Data] getData');
export const getDataComplete = createAction(
  '[Data] getDataComplete',
  props<{ data: any }>()
);
export class DataEffects {
  constructor(private actions$: Actions, private dataService: DataService) {}

  getData$ = createEffect(() =>
    this.actions$.pipe(
      ofType(fromDataActions.getData),
      switchMap(() =>
        this.dataService
          .getData()
          .pipe(map((data) => fromDataActions.getDataComplete({ data })))
      )
    )
  );
}
export const dataFeatureName = 'data';

export interface DataState {
  data: any;
}

export const initialDataState: DataState = {
  data: null,
};

const dataReducerInternal = createReducer(
  initialDataState,

  on(dataActions.getDataComplete, (state, { data }) => {
    return {
      ...state,
      data,
    };
  })
);

export function dataReducer(state: DataState | undefined, action: Action) {
  return dataReducerInternal(state, action);
}
export const getDataFeatureState = createFeatureSelector(dataFeatureName);

export const selectData = createSelector(
  getDataFeatureState,
  (state: DataState) => state.data
);

and the data.service.ts is throwing the http request and returning the observable.

@Injectable({ providedIn: 'root' })
export class DataService {
  constructor(private httpClient: HttpClient) {}

  getData() {
    return this.httpClient
      .get('https://localhost:5001/api/securevalues')
      .pipe(catchError((error) => of(error)));
  }
}
.
├── services
│   ├── auth.service.ts
│   └── data.service.ts
├── store
│   ├── auth
│   │   ├── auth.actions.ts
│   │   ├── auth.effects.ts
│   │   ├── auth.reducer.ts
│   │   ├── auth.selectors.ts
│   │   └── index.ts
│   ├── data
│   │   ├── data.actions.ts
│   │   ├── data.effects.ts
│   │   ├── data.reducer.ts
│   │   ├── data.selectors.ts
│   │   └── index.ts
│   └── index.ts

Building an app state

The main barrel file store/index.ts is used to gather all the states and effects and to provide an app state to the AppModule which we can register.

import { authReducer, AuthEffects } from './auth';
import { DataEffects, dataReducer } from './data';

export * from './auth';
export * from './data';

export const appReducer = {
  auth: authReducer,
  data: dataReducer,
};

export const appEffects = [AuthEffects, DataEffects];

Registering the AppState on the AppModule

We can register the appstate on the AppModule now with the StoreModule and EffectsModule as well as configure our authentication.

/* imports */
import { appReducer, appEffects } from './store'; // importing from `store/index.ts`

export function configureAuth(oidcConfigService: OidcConfigService) {
  return () =>
    oidcConfigService.withConfig({
      /*config*/
    });
}

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    ...
    AuthModule.forRoot(),
    StoreModule.forRoot(appReducer),
    EffectsModule.forRoot(appEffects),
    HttpClientModule,
  ],
  providers: [
    OidcConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: configureAuth,
      deps: [OidcConfigService],
      multi: true,
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Now that the store is ready we have to use it in our app.

Using the store in the application

To use the store in our app we added four components to our app.

  • AppComponent (initial loading point for our application)
  • HomeComponent (Landing page for the app)
  • ProtectedComponent (Loading protected data and can not be navigated to when not authenticated –> Guard)
  • Unauthorized (As the sts will redirect us to an unauthorized route we provide the appropriate route)

We can connect them with this routes

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: HomeComponent },
  {
    path: 'protected',
    component: ProtectedComponent,
    canActivate: [AuthorizationGuard],
  },
  { path: 'unauthorized', component: UnauthorizedComponent },
];

The HomeComponent and the UnauthorizedComponent are static and contain no data. Interesting is the ProtectedComponent and the AppComponent

AppComponent

The AppComponent as initial entry point will check for the authentication state and set the properties accordingly.

export class AppComponent implements OnInit {
  isAuthenticated$: Observable<boolean>;
  constructor(private store: Store<any>) {}

  ngOnInit() {
    this.store.dispatch(checkAuth());

    this.isAuthenticated$ = this.store.pipe(select(selectIsAuthenticated));
  }

  login() {
    this.store.dispatch(login());
  }

  logout() {
    this.store.dispatch(logout());
  }
}

In its template we give the possibility to sing in and our as well as the router outlet showing the main page and - later on - the protected page.

<h2>Authentication with NgRx</h2>

<div *ngIf="isAuthenticated$ | async as isAuthenticated; else noAuth">
  <a [routerLink]="'home'">home</a> |
  <a [routerLink]="'protected'">protected</a> |

  <button (click)="logout()">Logout</button>
</div>

<ng-template #noAuth>
  <a [routerLink]="'home'">home</a> |
  <button (click)="login()">Login</button>
  <hr />
</ng-template>

<router-outlet></router-outlet>

ProtectedComponent

The ProtectedComponent can only be accessed when the user is authenticated and calls the data from the ASP.NET Core API which was secured.

/* imports */
import { selectuserInfo, getData, selectData } from '../store';

export class ProtectedComponent implements OnInit {
  secretData$: Observable<any>;
  userData$: Observable<any>;

  constructor(private store: Store<any>) {}

  ngOnInit(): void {
    this.userData$ = this.store.pipe(select(selectuserInfo));
    this.secretData$ = this.store.pipe(select(selectData));
    this.store.dispatch(getData());
  }
}
<p>protected works!</p>

<h2>Userdata</h2>
<div *ngIf="userData$ | async as userData">{{ userData | json }}</div>

<hr />
<h2>Secret Data</h2>
<div *ngIf="secretData$ | async as secretData">{{ secretData | json }}</div>

Due to the NgRx store this is very nice and clean.

Adding the AuthGuard

To save the protected route with a guard we can use the AuthService again as it exposes if we are authenticated or not.

/* imports */

@Injectable({ providedIn: 'root' })
export class AuthorizationGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> {
    return this.authService.isLoggedIn.pipe(
      map((isAuthorized: boolean) => {
        if (!isAuthorized) {
          this.router.navigate(['/unauthorized']);
          return false;
        }

        return true;
      })
    );
  }
}

The guard is used in the routes like we have seen before

{
    path: 'protected',
    component: ProtectedComponent,
    canActivate: [AuthorizationGuard],
},

Adding the Interceptor

We added the interceptor in the last post already, but for completeness we will mention it again here. The interceptor checks the route if it is in the array of secured routes and will add the token if it is available, then handle the request.

/* imports */

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private secureRoutes = ['https://localhost:5001/api'];

  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler) {
    if (!this.secureRoutes.find((x) => request.url.startsWith(x))) {
      return next.handle(request);
    }

    const token = this.authService.token;

    if (!token) {
      return next.handle(request);
    }

    request = request.clone({
      headers: request.headers.set('Authorization', 'Bearer ' + token),
    });

    return next.handle(request);
  }
}

That is it! I hope you enjoyed reading it.

HTH

Fabian