Skip to content

7. Authentication

Andrei Antal edited this page Oct 24, 2024 · 2 revisions

Challenge 14 - Add role based authentication

  • We'll start by creating an auth service:
ng generate service services/auth
  • Next we'll define the Role enum in the auth.service file:
export enum Role {
  Admin = 'Admin
  User = 'User',
}
  • We'll define a few mocks we'll use in the auth process (normally these should be fetched from a server):
const MOCK_USER = {
  user: 'user',
  password: 'user',
};
const MOCK_ADMIN = {
  user: 'admin',
  password: 'admin',
};

const MOCK_TOKEN = '1234567890';
  • We'll define the internal variables that will hold the state for the auth service:
// internal state
#isAuthenticated = signal(false);
#token = signal<string | undefined>(undefined);
#role = signal<Role | undefined>(undefined);

// public signals
isAuthenticated = this.#isAuthenticated.asReadonly();
token = this.#token.asReadonly();
role = this.#role.asReadonly();
  • We'll define the login and logout methods that will be used to authenticate the user:
login(user?: string, pass?: string): Observable<{ token: string }> {
  // check if user and password are provided and match the mocks
  // save auth state and role
  // return token as observable and store it as well
}

logout() {
  // clear auth state and token
}
  • In the App component we'll add a logout button that will call the logout method from the AuthService:
export class AppComponent {
  authService = inject(AuthService);
  #router = inject(Router);
  ...
  logout() {
    this.authService.logout();
    this.#router.navigate(['/']); // navigate back to home page
  }
}
  • In the Home component we'll add a login form that will be displayed only if the user is not authenticated. If the user is authenticated, we show the link as it's displayed now:
@if (authService.isAuthenticated()) {
...
} @else {
<div class="card" style="width: 30vw" [formGroup]="loginForm">
  <form class="card-body">
    <h3 class="card-title">Movies app login form</h3>
    <div class="mb-3">
      <label class="form-label"> User </label>
      <input type="text" class="form-control" formControlName="user" />
    </div>
    <div class="mb-3">
      <label class="form-label"> Password </label>
      <input type="password" class="form-control" formControlName="password" />
    </div>
    <button href="#" class="btn btn-primary" (click)="submit()">Log in</button>
  </form>
</div>
}
  • Next we need to define the loginForm in the HomeComponent class:
loginForm = this.#fb.group({
  user: '',
  password: '',
});
  • We'll define the submit method that will be called when the user clicks the Log in button. This method will call the login method from the AuthService:
submit() {
  const { user, password } = this.loginForm.value;
  this.authService.login(user, password).subscribe({
    next: () => {
      this.error.set(undefined);
      this.#router.navigate(['/movies']);
    },
    error: (err) => {
      this.error.set(err);
    },
  });
}
  • We'll also show the error message if user/password combination is not correct:
@if(error()) {
<div class="alert alert-danger" style="width: 30vw">
  {{ error() }}
</div>
}
  • Try the user/password combinations form the mocks to see if the login works as expected.

Diffs

Challenge 15 - Structural directives

For this challenge we will create a structural directive that will be used to show/hide elements based on the user role. The directive will be applied to an element and will take a role as an input. If the user has the role, the element will be displayed, otherwise it will be removed from the DOM.

  • Let's create a hasRole directive using the CLI command:
ng generate directive movies/directive/has-role --flat=false
  • Next we'll define, as for every structural directive, a TemplateRef and a ViewContainerRef as properties of the directive class:
export class HasRoleDirective {
  #template = inject(TemplateRef);
  #view = inject(ViewContainerRef);
}
  • We'll also define an input signal property for the role. This will be the role that the user needs to have in order to display the element:
  ngmHasRole = input<keyof typeof Role | undefined>(undefined); // Role is imported form auth.service
  • Finally, when we initialize the directive we'll make use of the AuthService to check if the user has the role and based on that we'll either display the element or remove it from the DOM:
  ngOnInit() {
   // check if user is authenticated and provided role through the Input is the user role
   // if true, display the element, otherwise remove it from the DOM
  }
  • Apply the directive on the MovieItemComponent templates to show the edit and delete buttons only if the user has the admin role and the rating component only if the user has the user role. And also on the MovieListComponent template to show the New Movie button only if the user has the admin role.
*ngmHasRole="'Admin'"

*ngmHasRole="'User'"

Diffs

DOCUMENTATION

Structural directives

Challenge 16 - Route guards

  • In the first part of the challenge challenge we'll create a route guard that will check if the user is authenticated and has the role to access the route. First we'll create a canActivate function that will take the route and the state as parameters and will return a boolean or a UrlTree:
ng generate guard movies/guards/is-authenticated
export const isAuthenticatedGuard: CanActivateFn = () => {
  const router = inject(Router);
  const authService = inject(AuthService);
  if (!authService.isAuthenticated) { // check if user is (not) authenticated
    return router.parseUrl('/'); // route back to home page
  }
  return true; // allow navigation
};

Next we'll add the guard to the routes that require authentication. In app.config we need to make sure that we restrict only access to movies only to authenticated users:

{
    path: 'movies',
    canActivate: [isAuthenticatedGuard],
    ...
}
  • In the second part of the challenge we'll create a route guard that will check if the user is authenticated and has the role to access the route. First we'll create a canActivate function that will take the route and the state as parameters and will return a boolean or a UrlTree:
ng generate guard movies/guards/has-role
  • Add a data property to the routes that require roles in the movies.routes file:
{
    path: 'new',
    ...
    data: {
      roles: [Role.Admin],
    }
},
{
    path: ':id',
    ...
    data: {
      roles: [Role.Admin],
    },

},
  • Add conditions to the has-role guard:
export const hasRoleGuard: CanActivateFn = (route, state) => {
  const router = inject(Router);
  const authService = inject(AuthService);
  const roles: Role[] = route.data['roles']; // get roles from route data
  if (roles?.includes(authService.role!)) { // check if user has the required role
    return true; // allow navigation
  }
  return router.parseUrl('/');  // route back to home page
};
  • Add the guard to the routes that require authentication. In movies.routes we need to make sure that we restrict access to the new and :id routes that require roles only to users that have the required role:
canActivate: [canActivateGuard]

Diffs

DOCUMENTATION

Route guards

Challenge 17 - Unsaved changes confirmation

  • First, revert the changes made in the MovieDetailComponent to use the movies$ so we can edit the movies:
@for (movie of movies$(); track movie.id) {
  • Next, in the MovieDetailComponent add a confirmCancel that will show a confirm dialog if form changes are not saved.
confirmCancel(): Observable<boolean> {
  if (!this.#changesSaved && this.movieForm.dirty) {
    return of(
      window.confirm('You have unsaved changes. Do you really want to leave?')
    );
  }
  return of(true);
}
  • Use the canDeactivate guard in the movies.routes file to check if the user wants to leave the page without saving the changes. You can use the confirmCancel method on the provided component:
canDeactivate: [
  (component: MovieDetailComponent) => component.confirmCancel(),
],

Diffs

Challenge 18 - Http interceptors

  • Create an authInterceptor that will add the Authorization header to the request:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
    const authService = inject(AuthService);
    const token = authService.token; // Get token form auth service
    const authReq = req.clone({
        headers: req.headers.set('Authorization', `Bearer ${token}`), // Add token to request headers
    });
    return next(authReq);  // return forward updated request body
}
  • in app.config add the withInterceptors function to the provideHttpClient provider that will take an array of interceptors and will return a HttpClient configuration object that will be used to update the HttpClient configuration:
export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])), // update http client configuration
    ...
  ],
};

Diffs

DOCUMENTATION

Interceptors

Clone this wiki locally