authentication

Karine Samyn, Benjamin Vertonghen, Thomas Aelbrecht, Pieter Van Der Helst

"The best way to get the right answer on the internet is not to ask a question; it’s to post the wrong answer.” - Cunningham’s Law

overview

  1. intro
    what do we need at the frontend
  2. authentication service
    provide access to all the backend routes involving authentication
  3. register form
    a form where users can create an account
  4. server side validation
    something new w.r.t. forms, do validation at the server side
  5. authorization headers
    how to pass the jwt token with each request
  6. authentication guard
    make parts of the application inaccessible for users who are not logged in

what do we need?

  • our backend has a login and register route, and all but the GET routes are protected with jwt token authorization
  • at the frontend we need:
    • an authentication service to talk to these routes
    • a register and login component
    • show in our navigation bar who's logged in
    • a way to limit access to parts of our app which the users can't use if they're not logged in anyway
  • since most stuff involving the forms and service are old hat by now, we'll only highlight the authentication specific parts in these slides, the full working code can, as always, be found at the recipeapp github
  
~/recipeapp/$ ng g m User --module=app
~/recipeapp/$ ng g s user/Authentication
~/recipeapp/$ ng g c user/Login --module=user
~/recipeapp/$ ng g c user/Register --module=user 
            
lets put everything concerning users and authentication in its own module just like we put all our recipe http calls in a separate service, we'll add an authentication service to do all our user http calls let's add a login and register component already too, we'll need them soon

authentication service

src/app/user/authentication.service.ts
                function parseJwt(token) {
                  if (!token) {
                    return null;
                  }
                  const base64Token = token.split('.')[1];
                  const base64 = base64Token.replace(/-/g, '+').replace(/_/g, '/');
                  return JSON.parse(window.atob(base64));
                }
              
the authentication service will do our login / register backend calls, but we'll also manage our jwt here we can't do anything with the encrypted (signed) part, only the backend knows the secret, but we can get to the other info stored there: username and expire date so first, let's add a function to parse the token, which is base64 encoded

authentication service

src/app/user/authentication.service.ts
    @Injectable()
    export class AuthenticationService {
      private readonly _tokenKey = 'currentUser';
      private _user$: BehaviorSubject<string>;
    
      constructor(private http: HttpClient) {
        let parsedToken = parseJwt(localStorage.getItem(this._tokenKey));
        if (parsedToken) {
          const expires = new Date(parseInt(parsedToken.exp, 10) * 1000) < new Date();
          if (expires) {
            localStorage.removeItem(this._tokenKey);
            parsedToken = null;
          }
        }
        this._user$ = new BehaviorSubject<string>(parsedToken && parsedToken.unique_name);
      }      
when a user succesfully logs in, we'll store the token in the localstorage, to remember the user so in our constructor we check the localstorage for a token, and parse it using the parse method from the previous slide we check for an expire date here too, this doesn't make our app secure (backend is the only one who can check for valid tokens) this is merely a UX issue, we don't want to imply the user is logged in until she makes a first backend call we set the user as a BehaviorSubject, similar to a Subject, but a BehaviorSubject always has a value, even if it was set before you subscribed so every other component can subscribe to our user$ and will always see the logged in user

login method

src/app/user/authentication.service.ts
    login(email: string, password: string): Observable<boolean> {
      return this.http.post(
          `${environment.apiUrl}/account`,
          { email, password },
          { responseType: 'text' }
        ).pipe(
        map((token: any) => {
          if (token) {
            localStorage.setItem(this._tokenKey, token);
            this._user$.next(email);
            return true;
          } else {
            return false;
          }
        })
      );
    }        
                
we'll post to the /api/account route with a body containing the email and password keys note the ES2015 object initializer syntax, when key and value are the same (this is like typing {email: email, password: password}) if the login is succesful, our backend returns the jwt token as a simple string, so we can't use the default json response type anymore, we need to specify in our http request that we expect a text response we'll also signal through our BehaviorSubject that a new user is logged in

register method

src/app/user/authentication.service.ts
              register(
                firstname: string, lastname: string, email: string, password: string
              ): Observable<boolean> {
                return this.http
                  .post(
                    `${environment.apiUrl}/account/register`,
                    {
                      firstname, lastname,
                      email, password, 
                      passwordConfirmation: password
                    },
                    { responseType: 'text' }
                  )
                  .pipe(
                    map((token: any) => {
                      if (token) {
                        localStorage.setItem(this._tokenKey, token);
                        this._user$.next(email);
                        return true;
                      } else {
                        return false;
                      }
                    })
                  );
              }
                
the register method is very similar, we have a few more fields (first-, lastname) and now we post to the register url

logout method

src/app/user/authentication.service.ts
    logout() {
      if (this.user$.getValue()) {
        localStorage.removeItem('currentUser');
        this._user$.next(null);
      }
    }
                
logging out is achieved by removing the token, and signaling we have no user anymore

register form

src/app/user/register/register.component.ts
    export class RegisterComponent implements OnInit {
      public user: FormGroup;
    
      ngOnInit() {
        this.user = this.fb.group({
          firstname: ['', Validators.required],
          lastname: ['', Validators.required],
          email: [
            '', [Validators.required, Validators.email],, 
            serverSideValidateUsername(this.authService.checkUserNameAvailability)
          ],
          passwordGroup: this.fb.group({
            password: ['', [Validators.required, Validators.minLength(8), patternValidator(...)]],
            confirmPassword: ['', Validators.required]
          }, { validator: comparePasswords })
        });
      }
                
next up, the register form, we'll create a standard register form, the user's name and firstname, an email address (which will function as username) and both password and confirm password fields in the code on github I added password validation to make sure there's a number, and an upper- and lowercase letter; as that is what our backend requires (although this is microsoft defaults being old school, length beats all when considering a password to be good or not) we put both passwords in a group, so that we can add a custom validator to that group, which will check that both passwords are the same we're also using server side validation for the first time this async validator function will be called if the other validators succeeded, we'll check for uniqueness of our username using a function (we still need to add) from our authentication service

custom validators

src/app/user/register/register.component.ts
    function comparePasswords(control: AbstractControl): ValidationErrors {
      const password = control.get('password');
      const confirmPassword = control.get('confirmPassword');
      return password.value === confirmPassword.value ? null 
        : { 'passwordsDiffer': true };
    }
                
we've seen custom validators, they are simply functions which take a control as parameter and return null if the validation succeeded or they return a ValidationErrors object if there's an error (which is basically a key:value field, remember our {required: true} error from last time)

server side validation

src/app/user/register/register.component.ts
                function serverSideValidateUsername(
                  checkAvailabilityFn: (n: string) => Observable<boolean>
                ): ValidatorFn {
                  return (control: AbstractControl): Observable<ValidationErrors> => {
                    return checkAvailabilityFn(control.value).pipe(
                      map(available => {
                        if (available) {
                          return null;
                        }
                        return { userAlreadyExists: true };
                      })
                    );
                  };
                }
                
our server side validation needs the check function from our authentication service passed as a parameter the signature is the same of a regular validator, but now we're async, so we return an Observable of such a ValidationErrors object we'll add a function to our service to check for user name availability

user name availability at the backend

 RecipeApi/Controllers/AccountController.cs 
              [AllowAnonymous]
              [HttpGet("checkusername")]
              public async Task<ActionResult<Boolean>> 
                CheckAvailableUserName(string email)
              {
                  var user = await _userManager.FindByNameAsync(email);
                  return user == null;
              }
                
before we can add it to the service, we need a route on our server, so let's add a checkusername route we'll simply return a boolean indicating if the user exists already or not

user name availability in service

src/app/user/authentication.services.ts
    checkUserNameAvailability = (email: string): Observable<boolean> => {
      return this.http.get<boolean>(
        `${environment.apiUrl}/account/checkusername`,
        {
          params: { email }
        }
      );
    }
                
our service then, simply needs to call this route with the email address as url parameter and return the response we use the arrow function notation for our member function so that this binding will work when this function is passed as a parameter (you learned this in web2, yes, really, you did) if you got a class to represent the type, or if its a built in type like we do here, you can pass this as a generic parameter to the get, and i.s.o. Observable<any> the get function will return an Observable<boolean>

register html


            <form [formGroup]="user" (ngSubmit)="onSubmit()">
              <mat-card-header>
                <mat-card-title>Register</mat-card-title>
              </mat-card-header>
              <mat-card-content fxLayout="column">
                <span fxLayout="row" fxLayoutGap="2%">
                  <mat-form-field>
                    <input
                      matInput
                      placeholder="first name"
                      aria-label="first name"
                      data-cy="register-firstname"
                      formControlName="firstname"
                    />
                    <mat-error
                      *ngIf="
                        user.get('firstname').errors && user.get('firstname').touched
                      "
                    >
                      {{ getErrorMessage(user.get('firstname').errors) }}
                    </mat-error>
                  </mat-form-field>
                  <!-- ... and so on -->
                
in our html we simply need to link the three input fields to their controls and add a whole lot of error divs let's check this out

authorization headers

  • many of our backend routes are properly protected, we'll have to add the jwt token to all requests needing authorization or we'll get a '401 unauthorized' error
  • this is done using (standard) authorization headers with a Bearer token

http interceptor

  • while you could add a headers object to each request, the HttpClientModule added http interceptors which make this a lot more convenient
  • as their name implies, they intercept every request and then you can alter / log / ... them
  • we'll simply clone each request and add an authorization header with our token if we're logged in

AuthenticationInterceptor

 src/app/http-interceptors/AuthenticationInterceptor.ts 
                @Injectable()
                export class AuthenticationInterceptor implements HttpInterceptor {
                  constructor(private authService: AuthenticationService) {}
    
                  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
                    if (this.authService.token.length) {
                      const clonedRequest = req.clone({
                        headers: req.headers.set('Authorization',`Bearer ${this.authService.token}`)
                      });
                      return next.handle(clonedRequest);
                    }
                    return next.handle(req);
                  }
                }
                
we'll create a new class for this, which implements the HttpInterceptor interface we have to override the intercept method, which gets two parameters, the request and the standard handler simply calling this handler on the request is the standard behavior, as if you didn't write an interceptor at all so we'll simply check if we have a valid token in our authentication service, and add it to the headers if we do after which the next handlers can process this new request, with a token

AuthenticationInterceptor

frontend/src/app/http-interceptors/index.ts
    import { HTTP_INTERCEPTORS } from '@angular/common/http';
    import { AuthenticationInterceptor } from './AuthenticationInterceptor';
    
    export const httpInterceptorProviders = [
      {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthenticationInterceptor,
        multi: true
      }
    ];
              
Because interceptors are dependencies of the HttpClient service, you must provide them in the same injector that provides HttpClient you could provide these directly in the providers list of your module, but it's rather verbose, so it's considered 'best practice' to create a 'barrel' file that gathers all interceptors so we'll create a small file to declare our interceptor provider

AuthenticationInterceptor

src/app/app.module.ts
                  import { httpInterceptorProviders } from '../http-interceptors/index';
                  
                  @NgModule({
                    declarations: [...],
                    imports: [...],
                    providers: [httpInterceptorProviders],
                    bootstrap: [AppComponent]
                  })
                  export class AppModule {}
              
which we'll add to the providers list of our AppModule, as our recipe data service is provided in the root, we need our interceptor available at the same 'level' this will result in all http.get / .post / ... calls done from AppModule to get the token added if one is available

auth guard

src/app/app-routing/app-routing.module.ts
    const appRoutes: Routes = [
      {
        path: 'recipe',
        canActivate: [ AuthGuard ],
        loadChildren: '../recipe/recipe.module#RecipeModule'
      },
      { path: '', redirectTo: 'recipe/list', pathMatch: 'full'},
      { path: '**', component: PageNotFoundComponent}
    ];
                
one thing we still want is to 'lock' parts of our frontend, only show them to authorized users you do this by adding guards, we'll put our full recipe module behind a guard (so only logged in users can access anything)

AuthGuard

src/app/user/auth-guard.service.ts
    import { CanActivate } from '@angular/router';
    
    @Injectable()
    export class AuthGuard implements CanActivate {
      canActivate(route: ActivatedRouteSnapshot, 
            state: RouterStateSnapshot): boolean {
        if (this.authService.user$.getValue()) {
          return true;
        }
        this.authService.redirectUrl = state.url;
        this.router.navigate(['/login']);
        return false;
      }
    }
                
create the guard using the cli ng g guard user/auth we added a CanActivate guard, so we implement the CanActivate interface guards return a boolean, now we simply need to return true or false if we're logged in or not if we're not logged in we'll redirect to the login page we'll do one more thing though, we remember which url we were trying to access, after a successfull login we'll "continue" to the page

login component


    onSubmit() {
      this.authService.login(this.user.value.username, 
              this.user.value.password).subscribe(val => {
        if (val) {
          if (this.authService.redirectUrl) {
            this.router.navigateByUrl(this.authService.redirectUrl);
            this.authService.redirectUrl = undefined;
          } else {
            this.router.navigate(['/recipe/list']);
          }
        }
      }, err => this.errorMsg = err.json().message);
    }
                
the login component then, needs to redirect to this url if login succeeds that's it, let's check this out

summary

  • if your backend properly supports authorization updating your frontend is not that much work
  • most work is setting up proper register and login forms, and providing access to these in your application
  • authorizing your backend requests themselves became very simple starting in Angular4+ using interceptors
  • authentication guards make it pretty easy to shield certain parts of your application for users who are not logged in (but remember: this is good for UX, not for security; always properly shield all routes, even if they're only accessible from a 'shielded' part of your app)