Angular Firebase Authentication - Implement Auth in Minutes
Angular Firebase Authentication - Implement Auth in Minutes

In this post, you'll discover Angular Firebase authentication, its benefits, and how to set it up properly in an Angular project.

What Is Firebase?

Firebase is a service provided by Google that allows us to build projects without needing to worry about setting up a backend or database. For instance, in a previous post, I demonstrated how to utilize Firebase to create a database and integrate it into an Angular application.

Now, we're taking the next step by implementing authentication using Firebase. This means we can write our code within Angular without the need for backend or database implementation, as Firebase handles it all. This makes it incredibly simple to develop projects without requiring expertise in areas you may not be familiar with yet.

Moreover, Firebase authentication offers various features such as email verification, registration, login, password recovery, and more, all implemented correctly within the Firebase platform.

Creating Project in Firebase

First, create an account on Firebase. Then, proceed to create a new project.

creating a project

Once the project is created, select "No" when prompted to enable Google Analytics. Next, navigate to the "Authentication" section under the "Build" tab on the left. Here, you'll find various authentication providers such as Email, Google, Facebook, and Twitter. For this post, we'll focus solely on email/password authentication. Enable email/password authentication but refrain from enabling passwordless sign-in.

After enabling email/password authentication, you can proceed to the "Users" tab and create your first user.

creating a user

Great! Your first user is now saved in Firebase.

The project

With the configuration complete, you're now ready to integrate Firebase into your Angular project. Let's get started!

Our project

Our project now consists of two pages: a registration page and a login page, each containing simple forms. At this stage, both components lack any additional logic.

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  standalone: true,
  imports: [ReactiveFormsModule],
})
export class RegisterComponent {
  fb = inject(FormBuilder);
  http = inject(HttpClient);
  router = inject(Router);

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

  onSubmit(): void {
    console.log('register');
  }
}

Here's what the Register component looks like.

Configuring Firebase

The initial step is to install our dependencies.

npm i @angular/fire

The next step is to configure the Angular library for Firebase. To do this, we need to obtain the API keys from Firebase. Begin by clicking on "Create a new web project" and enter any desired name. Then, click on "Register app".

creating app

After completing these steps, you'll receive a firebaseConfig object that you can directly copy into your application. This configuration contains the necessary API keys and other settings required for Firebase integration.

// src/app.config.ts
const firebaseConfig = {
  apiKey: 'API_KEY',
  authDomain: 'AUTH_DOMAIN',
  projectId: 'PROJECT_ID',
  storageBucket: 'STORAGE_BUCKET',
  messagingSenderId: 'MESSAGING_SENDER_ID',
  appId: 'APP_ID'
}
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes), 
    provideHttpClient(),
    importProvidersFrom([
      provideFirebaseApp(() => initializeApp(firebaseConfig)),
      provideAuth(() => getAuth())
    ])
  ],
};

Here, we've initialized the Firebase application and Firebase authentication. We then provided this configuration, including the correct firebaseConfig, to our appConfig.

Registering a User

The initial step is to create an interface for the user object that we'll be working with.

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

Now, we're prepared to create a service that will interact with Firebase.

// src/auth.service.ts
export class AuthService {
  firebaseAuth = inject(Auth)

  register(email: string, username: string, password: string): Observable<void> {
    const promise = createUserWithUsernameAndPassword(
      this.firebaseAuth,
      email,
      password
    ).then(response => updateProfile(response.user, {displayName: username}))
    return from(promise)
  }
}

Firstly, we injected firebaseAuth into our AuthService. This service stores all our API keys, allowing us to make requests through it. We've also added a register method that must return an Observable. Since Firebase returns a Promise, we convert it to an Observable to align with Angular's preferred pattern.

Since it's not possible to set a username when creating a user, we use the updateProfile method upon success to achieve this.

Next, we'll utilize this function in our RegisterComponent.

export class RegisterComponent {
  authService = inject(AuthService)
  ...
  onSubmit(): void {
    const rawForm = this.form.getValue()
    this.authService
      .register(rawForm.email, rawForm.username, rawForm.password)
      .subscribe(() => {
        this.router.navigateByUrl('/')
      })
  }
}

register

Now, you can navigate to our registration page, sign up, and be directed to the homepage upon successful registration. Checking your Firebase admin panel will reveal a new user created there. This confirms that our registration code is functioning correctly.

Adding Validation

It seems we have encountered an issue.

register error

It appears that when clicking "register" on the empty form, an unhandled error occurs stating that "Email is invalid". To address this, we need to catch this error and display validation messages instead.

export class RegisterComponent {
  ...
  errorMessage: string | null

  onSubmit(): void {
    const rawForm = this.form.getValue()
    this.authService
      .register(rawForm.email, rawForm.username, rawForm.password)
      .subscribe({
        next: () => {
          this.router.navigateByUrl('/')
        },
        error: (err) => {
          this.errorMessage = err.code
        }
      })
  }
}

We've added error handling to our subscribe method, capturing any errors and setting them to the errorMessage property. Now, we can render this error message in the markup.

@if (errorMessage) {
  <div>{{errorMessage}}</div>
}
...

error message

As you can observe, we're now receiving an error message instead of an uncaught error in the console.

Adding User Login

Before we proceed with implementing login functionality, we need to create a method to call Firebase correctly.


// src/auth.service.ts
export class AuthService {
  firebaseAuth = inject(Auth)
  ...

  login(email: string, password: string): Observable<void> {
    const promise = signInWithUsernameAndPassword(
      this.firebaseAuth,
      email,
      password
    )
    return from(promise)
  }
}

Here, we've implemented a similar method to the registration process, but this time we're calling signInWithUsernameAndPassword instead. As before, we're converting a Promise to an Observable.

We can now copy and paste the entire code from the registration component to the login component, as the logic is very similar.

export class LoginComponent {
  ...
  authService = inject(AuthService)
  errorMessage: string | null

  onSubmit(): void {
    const rawForm = this.form.getValue()
    this.authService
      .login(rawForm.email, rawForm.password)
      .subscribe({
        next: () => {
          this.router.navigateByUrl('/')
        },
        error: (err) => {
          this.errorMessage = err.code
        }
      })
  }
}

The concept remains consistent: we call the login method, passing the form data, and navigate the user upon successful login. In case of an error, we display an error message.

@if (errorMessage) {
  <div>{{errorMessage}}</div>
}
...

And remember to update your markup accordingly.

As demonstrated in the browser, we can now log in to our application using existing credentials.

Persisting a User

The reason why our register and login methods return void instead of user information is because the typical approach involves retrieving a user token and storing it in local storage or a cookie for persistence. However, we haven't implemented this step because Firebase automatically handles it for us.

token

As evident in IndexedDB, Firebase stores the user token, which it utilizes to fetch user information.

Firebase automatically handles the storage of this token, requiring no additional code from us.

This implies that even after a page reload, Firebase automatically authenticates our user, and all we need to do is read this information.

Firebase provides a user data stream that we can subscribe to, allowing us to receive this information with every update.

export class AuthService {
  firebaseAuth = inject(Auth)
  user$ = user(this.firebaseAuth)
  currentUserSig = signal<UserInterface | undefined>(undefined)
}

In our AuthService, we've created a user$ observable to capture all new values of the authenticated user. Additionally, we've introduced a currentUserSig signal, which we intend to utilize throughout the application. Why not simply use user$? While user$ contains various properties that we may not need, I prefer using signals for consistency across the entire project.

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

  ngOnInit(): void {
    this.authService.user$.subscribe(user => {
      if (user) {
        this.authService.currentUserSig.set({
          email: user.email!,
          username: user.displayName
        })
      } else {
        this.authService.currentUserSig.set(undefined)
      }
    })
  }
}

In our AppComponent, after each page reload, we subscribe to the user stream and update our signal accordingly based on the received data. This ensures that the entire application is notified whenever a user logs in or logs out.

Now, let's update our markup in app.component.html.

@if (!authService.currentUserSig()) {
  <div>
    <a routerLink="/login">Login</a>
    <a routerLink="/register">Register</a>
  </div>
}
@if (!authService.currentUserSig()) {
  <div>
    <span (click)="logout()">Logout</span>
  </div>
}

Now, we can render the correct links based on the user's authentication state.

Logout

The final step is to implement the logout functionality.

export class AuthService {
  logout(): Observable<void> {
    const promise = signOut(this.firebaseAuth);
    return from(promise)
  }
}

Here, we simply call the signOut method and convert it to a promise.

logout(): void {
  this.authService.logout()
}

In our app.component.ts, we can call the service when the logout button is clicked.

If you're interested in learning Angular with NgRx from an empty folder to a fully functional production application, be sure to check out my course on Angular and NgRx - Building Real Project From Scratch.

📚 Source code of what we've done