Creating an Angular Login With the Latest Version

In this article, we will look over creating an Angular application. This will provide us with options to authenticate, register new users, and log out. It will be a proof of concept for OAuth 2.0 implementation and refresh token. This example will communicate with a backend REST API implemented with .NET Core.

We will cover only the front-end part using the IdentityMicroservice created in a previous article .

Creating a new Angular project with Material

To develop the angular login application we will use Visual Studio Code. You can find out more about the editor in a previous article I wrote a while back.

For the Angular Identity application, I chose to also use Angular Material. You can find out more about it here .

Let’s start by creating our Angular application:

  1. ng new AngularIdentity
  2. Add angular routing yes
  3. Stylesheet SCSS

And now we can add Angular Material to our application:

ng add @angular/material

Angular project structure

The most common change to my angular projects was the folder projects. I always found myself not happy with it and with the way I had to have references to different sections from my projects. Most likely I will change my mind in the future and come up with a different folder structure, but for now, this is the folder structure that I am using:

Angular Folder Structure

Layouts

As mentioned above, the user will be able to register a new user, log in, and log out. For this, I chose to use three different layouts. One for authentication, another for the main layout, and the last one for errors.

Auth Layout

In this section, we will design how the UI will look for the pages before accessing the main application. Under this layout, we will have Login and Register page.

Error Layout

We will use this layout to design the UI for our error pages.

Main Layout

Under this layout, we will display all other components used across the application. In this layout, we will have a header and a sidenav that will allow the user to navigate across the application. The authentication guard restricts access to all pages from this layout.

Authentication Guard

This guard restricts access to some resources and allows access to others for authenticated users.

@Injectable()
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private userPersistenceService: UserPersistenceService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        if (this.userPersistenceService.getUser()) {
            return true;
        }

        this.router.navigate(['/auth/login'], {
            queryParams: { returnUrl: state.url },
        });
        return false;
    }
}

We reference the guard inside the routing module file. This restricts access to all pages from the Main Layout based on the AuthGuard rules.

component: MainLayoutComponent,
        canActivate: [AuthGuard],

During the authentication process, we save the user data into the session storage. For this, we are using the UserPersistenceService.

Authentication Interceptor

The authentication guard is protecting different sections in the UI and now is time to allow the requests to our Web API. For this, I have created an authentication interceptor. When a user is logging in to the application, the Web API is sending back the user together with the access token and the refresh token. We save all this data into the session storage with the help of our UserPersistenceService.

The authentication interceptor is adding an Authorization header to all the web requests. Besides this, the interceptor is checking if the server is replying with 401 Unauthorized. If the server is sending back 401, most likely the token has expired. In this case, the authentication interceptor is trying to get a new refresh token based on the refresh token. We use the new access token to execute again the failed request.

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    private isRefreshing = false;
    private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(
        null
    );

    constructor(
        private userPersistenceService: UserPersistenceService,
        private authenticationService: AuthenticationService,
        private progressService: ProgressService
    ) { }
    intercept(
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<any>> {
        this.progressService.start();
        const currentUser = this.userPersistenceService.getUser();
        if (currentUser) {
            request = this.addHeaders(request, currentUser);
        }

        return next.handle(request).pipe(
            catchError((err: HttpErrorResponse) => {
                if (
                    err instanceof HttpErrorResponse && err.status === 401
                ) {
                    return this.handle401Error(request, next);
                } else {
                    return throwError(() => err);
                }
            }),
            finalize(() => this.progressService.complete())
        );
    }

    private handle401Error(request: HttpRequest<any>, next: HttpHandler) {
        const currentUser = this.userPersistenceService.getUser() ?? new User();
        if (!this.isRefreshing) {
            this.isRefreshing = true;
            this.refreshTokenSubject.next(null);
            return this.authenticationService
                .refreshToken(
                    new TokenModel(currentUser.token, currentUser.refreshToken, currentUser.ipAddress)
                )
                .pipe(
                    switchMap((tokenModel: TokenModel) => {
                        this.isRefreshing = false;
                        currentUser.token = tokenModel.token;
                        currentUser.refreshToken = tokenModel.refreshToken;
                        this.userPersistenceService.setUser(currentUser);
                        this.refreshTokenSubject.next(tokenModel.token);
                        if (request.url.indexOf(endpoints.auth.logout) >= 0 && request.body instanceof TokenModel) {
                            return next.handle(this.addHeaders(this.handleLogout(request, tokenModel), currentUser));
                        }
                        return next.handle(this.addHeaders(request, currentUser));
                    })
                );
        } else {
            return this.refreshTokenSubject.pipe(
                filter((token) => token != null),
                take(1),
                switchMap((jwt) => {
                    return next.handle(this.addHeaders(request, jwt));
                })
            );
        }
    }

    private handleLogout(request: HttpRequest<any>, tokenModel: TokenModel) {
        var logoutTokenModel = request.body as TokenModel;
        logoutTokenModel.refreshToken = tokenModel.refreshToken;
        var newRequest = request.clone({
            body: logoutTokenModel
        });
        return newRequest;
    }

    private addHeaders(request: HttpRequest<any>, user: User) {
        if (request.url.indexOf(endpoints.auth.refreshToken) >= 0) {
            return request.clone();
        }
        return request.clone({
            setHeaders: {
                Authorization: `Bearer ${user.token}`
            },
        });
    }
}

Besides the Authentication Interceptor, you can find another one that is handling errors.

Services

In this section, we can find different files that will help us through the application.

Authentication Service

We use this service to make calls to the Web API.

@Injectable({
  providedIn: 'root'
})
export class AuthenticationService {

  constructor(private http: HttpClient) { }

  public login(loginCredentials: LoginCredentials) {
    return this.http.post<any>(
      `${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.authenticate}`,
      loginCredentials
    );
  }

  public register(entity: RegisterUser) {
    return this.http.post(
      `${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.register}`,
      entity
    );
  }

  public refreshToken(tokenModel: TokenModel) {
    return this.http.post<TokenModel>(
      `${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.refreshToken}`,
      tokenModel
    );
  }

  public logout(tokenModel: TokenModel) {
    return this.http.post<string>(
      `${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.logout}`,
      tokenModel
    );
  }

  public logoutEverywhere() {
    return this.http.post<string>(
      `${environment.apiUrl}/${endpoints.auth.main}/${endpoints.auth.logoutEverywhere}`,
      undefined
    );
  }
}

User Persistence Service

When users are logging in we are keeping their data in the browser’s session storage. This service is taking care of that.

@Injectable({
  providedIn: 'root'
})
export class UserPersistenceService {
  user?: User;
  @Output() changeUser: EventEmitter<User> = new EventEmitter();

  constructor() { }

  public getUser(): User | undefined {
    var sessionUser = sessionStorage.getItem('user');
    if (sessionUser && sessionUser != null) {
      this.user = JSON.parse(sessionUser);
    } else {
      this.user = undefined;
    }

    return this.user;
  }

  public setUser(user: User) {
    sessionStorage.setItem('user', JSON.stringify(user));

    this.user = user;

    this.changeUser.emit(this.user);
  }

  public removeUser() {
    sessionStorage.removeItem('user');
    this.user = undefined;
  }
}

Features Module

Here we find all pages and what the user will see in the browser. In the features section, we keep the routing as well. I chose to split the features module between multiple modules, one for each major section of the application. Each of the sub-modules is then registered as a route in the routing module. Here is the features routing module:

const routes: Routes = [
    {
        path: '',
        component: MainLayoutComponent,
        canActivate: [AuthGuard],
        children: [
            { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
            {
                path: '',
                loadChildren: () =>
                    import('./dashboard/dashboard.feature.module').then(
                        (m) => m.DashboardFeatureModule
                    ),
            },
        ],
    },
    {
        path: 'auth',
        component: AuthLayoutComponent,
        children: [
            {
                path: '',
                loadChildren: () =>
                    import('./auth/auth.feature.module').then(
                        (m) => m.AuthFeatureModule
                    ),
            },
        ],
    },
    {
        path: 'error',
        component: ErrorLayoutComponent,
        children: [
            {
                path: '',
                loadChildren: () =>
                    import('./errors/errors.feature.module').then(
                        (m) => m.ErrorsFeatureModule
                    ),
            },
        ],
    },
    { path: '**', redirectTo: 'error/not-found' }
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
})
export class FeaturesRoutingModule { }

Resources

  1. Source Code: https://github.com/relaxedcodercom/AngularIdentity
  2. How To Implement DotNET Core OAuth2 For An API
  3. .NET Core Web API for OAuth 2.0: https://github.com/relaxedcodercom/IdentityMicroservice

Conclusions

We have implemented an Angular application that is allowing us to authenticate, register new users, and log out. The application is communicating with a .NET Core Web API using OAuth 2.0. If you have any questions or need any clarifications please use the comments section below.