Angular Authentication and Authorization - The Correct Way
Angular Authentication and Authorization - The Correct Way

In this post you will learn how to implement Angular authentication and autherization.

In order to understand it better I prepared for us a small project.

Initial project

Here we have 2 routes with forms. Login and Register. There is zero logic behind them yet. Additionally we have a logout button on the top of the page.

Initial project

Let's look on the code now.

<div>
  <a routerLink="/login">Login</a>
  <a routerLink="/register">Register</a>
</div>
<div>
  <span (click)="logout()">Logout</span>
</div>

Inside app.component.html we have just 2 links and a span with logout function which is empty.

// src/app/login/login.component.html
<h1>Login</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
  <div>
    <input type="text" placeholder="Email" formControlName="email" />
  </div>
  <div>
    <input type="password" placeholder="Password" formControlName="password" />
  </div>
  <div>
    <button type="submit">Sign In</button>
  </div>
</form>

Inside login page we have a reactive form with email and password.

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  standalone: true,
  imports: [ReactiveFormsModule],
})
export class LogicComponent {
  fb = inject(FormBuilder);
  form = this.fb.nonNullable.group({
    email: ['', Validators.required],
    password: ['', Validators.required],
  });

  onSubmit(): void {
    console.log('login')
  }
}

And here is our login component which has a form with 2 fields and a submit method. Exactly the same code we have inside register but there is just one more property username in the field.

Real API

What we want to implement in this post is a typical Angular authentication and authorization. In order to do that we will use a public API where we can create a user, login and get user information.

Realworld

It is called Realworld project and here inside documentation we can check specific endpoints.

For example to login a user we must do a request to /api/users/login with body

{
  "user": {
    "email": "foo@gmail.com",
    "password": "123"
  }
}

Back we will get a user if this user is found with additional authorization token.

Authorization token is a special unique string that we must store on the client in order to authenticate our user.

Registration

Let's start with implementation of user registration. But before we start to work with current user it would be nice to create an interface for that.

// src/app/user.interface.ts
export interface UserInterface {
  email: string;
  token: string;
  username: string;
}

It is just some basic information about use. And again this token inside is exactly an authentication token that we will use later.

Now inside our register form we want to make a POST request for the backend.

// src/app/register/register.component.ts
export class RegisterComponent {
  http = inject(HttpClient);

  onSubmit(): void {
    this.http
      .post<{ user: UserInterface }>('https://api.realworld.io/api/users', {
        user: this.form.getRawValue(),
      })
      .subscribe((response) => {
        console.log('response', response);
      });
  }

Here we injected http and we make a POST request to this real API. If it is successful we will see a console.log with our user data.

Our next step is to store this token that we get back from the API inside local storage. Why do we need that? We want to be logged in after page reload so we must store it somewhere where it won't be removed after reloading the page.

// src/app/register/register.component.ts
...
.subscribe((response) => {
  console.log('response', response);
  localStorage.setItem('token', response.user.token);
});

We just added saving token after registration to local storage.

Now we need something which will store our user in memory and what we can access from anywhere of our application. Typically we will use serving for such thing.

// src/app/auth.service.ts
@Injectable({
  providedIn: 'root',
})
export class AuthService {
  currentUserSig = signal<UserInterface | undefined | null>(undefined);
}

Here we created an AuthService which stores just a single signal with our current user information. Most importantly we have 3 states of our current user and not two. undefined is an initial state and means that we didn't obtain user information yet. null means that user is not logged in and UserInterface means that user is logged in.

Now it allows us to save a user to our service after registration.

// src/app/register/register.component.ts
...
 .subscribe((response) => {
  console.log('response', response);
  localStorage.setItem('token', response.user.token);
  this.authService.currentUserSig.set(response.user);
});

Here we added currentUserSig.set which updates our current user across the whole application.

The last thing that we must do is to redirect user after registration to the homepage.

// src/app/register/register.component.ts
export class RegisterComponent {
  router = inject(Router);
  ...
  .subscribe((response) => {
    console.log('response', response);
    localStorage.setItem('token', response.user.token);
    this.authService.currentUserSig.set(response.user);
    this.router.navigateByUrl('/');
  });
}

This is why we injected router in our component and called navigateByUrl.

register succ

As you can see in browser are created a user and were redirected to our homepage. Most importantly we saved in local storage our token so we can authenticate a user even after page reload.

Authentication

Now we must implement the similar stuff but for login. It will be 99% the same. Just the API url will be different.

// src/app/login/login.component.ts
export class LogicComponent {
  http = inject(HttpClient);
  authService = inject(AuthService);
  router = inject(Router);

  form = this.fb.nonNullable.group({
    email: ['', Validators.required],
    password: ['', Validators.required],
  });

  onSubmit(): void {
    this.http
      .post<{ user: UserInterface }>(
        'https://api.realworld.io/api/users/login',
        {
          user: this.form.getRawValue(),
        }
      )
      .subscribe((response) => {
        console.log('response', response);
        localStorage.setItem('token', response.user.token);
        this.authService.currentUserSig.set(response.user);
        this.router.navigateByUrl('/');
      });
  }
}

As you can see we send a login request and on success we save a token, update user in our service and redirect a user to homepage.

Login succ

After sign in with already existed user we are redirected to the homepage.

Reading a user

After successful login or registration we want to render user information on the screen with our service.

// src/app/app.component.ts
export class AppComponent implements OnInit {
  authService = inject(AuthService);
}

Here we just injected our AuthService so we can render our signal in html.

<div *ngIf="authService.currentUserSig() === null">
  <a routerLink="/login">Login</a>
  <a routerLink="/register">Register</a>
</div>
<div *ngIf="authService.currentUserSig()">
  {{ authService.currentUserSig()?.username }}
  <span (click)="logout()">Logout</span>
</div>

If our currentUserSig is null then we render login and register links. If it is there then our username. Most importantly for initial case when we don't have user data yet we don't render any links

User info

Here we render user information directly after login. But if we try to reload the page nothing will be rendered as all our data from the service was removed. This is completely normal because every time when we reload the page we must read local storage and fetch current user from the backend again. Only then we know that user is logged in.

Getting a user

As we already have a token inside local storage we can make a get request every time when our application is initialized.

export class AppComponent implements OnInit {
  http = inject(HttpClient);

  ngOnInit(): void {
    this.http
      .get<{ user: UserInterface }>('https://api.realworld.io/api/user')
      .subscribe({
        next: (response) => {
          console.log('response', response);
          this.authService.currentUserSig.set(response.user);
        },
        error: () => {
          this.authService.currentUserSig.set(null);
        },
      });
  }
}

When our component is initialized we are doing a get request for the current user. On success we save user information in our service. If we got a 401 error we set it to null as it is not logged in.

get user

As you can see we get an error on initialize which is totally correct as our request is not authenticated and we didn't attach our local storage token here.

Angular authorization

Now you might think that we must somehow provide an authorization token to our get user request. You are totally correct but we must do even more than that. If we have this token inside local storage we want to add it to all our API calls. Why is that? Because we want to implement Angular authorization.

We want our server to always know which user we are and if we can access that data that we are requesting. So it doesn't make sense to add a header to our current user request. We need to do it once for all our requests.

In order to do that inside Angular we are using interceptors. The goal of it is to intercept any request and modify it somehow before continuing with sending it to backend.

src/app/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (request, next) => {
  const token = localStorage.getItem('token') ?? '';
  request = request.clone({
    setHeaders: {
      Authorization: token ? `Token ${token}` : '',
    },
  });

  return next(request);
};

Here we created an authInterceptor. It's just a function where we have full access to our request. When we are done with modifying it we call next to proceed. Here we get our token from local storage and add an Authorization header with value Token our-unique-token. This is exactly the format how backend wants to get it.

Now we must register it in our application.

// src/app/app.config.ts
import { authInterceptor } from './auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideHttpClient(withInterceptors([authInterceptor])),
  ],
};

In order to do that inside our provideHttpClient we must add withInterceptors.

succ request

As you can see now after page reload our request was successful because we read our local storage and added a token as Authorization header to our request.

Logout

The last thing that we are missing is logout. And it is extremely easy to implement because removing of local storage token is the only thing that we need to do.

export class AppComponent implements OnInit {
  ...
  logout(): void {
    localStorage.setItem('token', '');
    this.authService.currentUserSig.set(null);
  }
}

We clear a value from the localStorage and set currentUserSig to null to tell application that we are not authorized.

And actually if you want to learn Angular with NgRx from empty folder to a fully functional production application make sure to check my Angular and NgRx - Building Real Project From Scratch course.

📚 Source code of what we've done