Sekrab Garage

Angular Firebase Authentication

I - Setting up AngularFire with Authentication

Angular January 2, 24
Subscribe to Sekrab Parts newsletter.
Series:

AngularFire

Previously we covered Authentication in Angular and why it was hard to wrap our heads around. To pick up from there, in this series of articles we are going to adopt Firebase Auth as a third party, and investigate different options: managing users remotely, and using Firebase for Authentication only.
  1. 10 months ago
    I - Setting up AngularFire with Authentication
  2. 10 months ago
    II - Firebase User Management in Angular
  3. 10 months ago
    III - Firebase For Authentication only in Angular

This series is not a tutorial for AngularFire per se. We are interesting in knowing how to connect the dots in a true Angular application.

I recommend this YouTube tutorial to integrate AngularFire. In addition to the official Firebase documentation.

anchorSetup and standalone

First, we need to setup AngularFire as documented, or run the following:

npm install @angular/fire firebase

Follow the documentation to add it to module-based app. Here is how we should add it to a standalone application.

// main.ts

import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getAuth, provideAuth } from '@angular/fire/auth';

const firebaseProviders: EnvironmentProviders = importProvidersFrom([
  // firebaseConfig is the json extracted for client-side web app
  provideFirebaseApp(() => initializeApp(firebbaseConfigHere)),
  provideAuth(() => getAuth()),
]);

bootstrapApplication(AppComponent, {
  providers: [
    // provide few things like interceptors and routes
    ...CoreProviders,
    ...AppRouteProviders,
    firebaseProviders
  ],
});

To customize it (Firebase customize dependencies), we can do the following:

// main.ts custom depdencies
// ...
const fbApp = () => initializeApp(firebaeConfigHere);
const authApp = () => initializeAuth(fbApp(), {
  persistence: browserSessionPersistence,
  popupRedirectResolver: browserPopupRedirectResolver
});

const firebaseProviders: EnvironmentProviders = importProvidersFrom([
  provideFirebaseApp(fbApp),
  provideAuth(authApp),
]);

// ...

A side note about SSR

Although SSR via Angular Universal is quite capable, but the reasoning behind implementing one keeps escaping me! For content-based websites, normal HTML pages proved to be superior, and in apps behind authentication walls, SSR does not matter much. In addition to the fact that search bots are getting better. That being said, if you have an SSR with Auth, in standalone mode, here is how to do it:

// server.ts

const fbApp = () => initializeApp(Config.Auth.firebase);
const authApp = () => initializeAuth(fbApp(), {
  persistence: browserSessionPersistence,
  popupRedirectResolver: browserPopupRedirectResolver
});

const firebaseProviders: EnvironmentProviders = importProvidersFrom([
  provideFirebaseApp(fbApp),
  provideAuth(authApp),
]);

const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ServerModule),
   ...CoreProviders,
   ...AppRouteProviders,
    firebaseProviders
  ],
});

// export the bare minimum, let nodejs take care of everything else
export const AppEngine = ngExpressEngine({
  bootstrap: _app
});
Note: you can find all functions in the modular tree-shakable version documented on Firebase Auth Modular.

anchorThe use case

The main use case we want to try to implement involves the following:

  • Login with Password authentication, and Google authentication
  • Create user with access (example: admin)
  • Add attribute to user: (example: bloodType)
  • Connect to our API (not Firebase apps, nor Google API), and properly maintain a healthy token for all Http requests

anchorThe bare minimum: AuthService

The difference between password and social login is that we need to create the user in Firebase manually. Thus Firebase provides two methods: signInWithEmailAndPassword and createUserWithEmailAndPassword. We will attempt to continue building up on our original AuthService. So the login component should not change. The register sequence is almost identical, we just need to pass extra custom attributes to deal with later.

Here is a link to the original StackBlitz project that we will attempt to recreate with Firebase, I am not revealing the new project just yet, because it's too premature.
// app/services/auth.service

import { Auth, signInWithEmailAndPassword } from '@angular/fire/auth';
// ...
export class AuthService {
  constructor(private auth: Auth) {  }
  
  Login(email: string, password: string): Observable<any> {
    const res = () => signInWithEmailAndPassword(this.auth, email, password);
    // build up a cold observable
    return defer(res);
  }
   // the sign up uses createUserWithEmailAndPassword
  Signup(email: string, password: string, custom: any): Observable<any> {
    const res = () => createUserWithEmailAndPassword(this.auth, email, password);
    // it also accepts an extra attributes, we will handle later
    return defer(res)
  }
  LoginGoogle(): Observable<any> {
    const provider = new GoogleAuthProvider(); // from @angular/fire/auth
    const res = () => signInWithPopup(this.auth, provider);
    return defer(res);
  }
}

The public login page we'll keep to the bare minimum:

// components/public/login.component
@Component({ ... })
export class PublicLoginComponent  {
  
  constructor(private authService: AuthService, private router: Router) {}
  login() {
    this.authService
      .Login('email@address.com', 'valid_firebase_password')
      .pipe(catchError...)
      .subscribe({
        next: (user) => {
          // redirect to dashbaord
          this.router.navigateByUrl('/private/dashboard');
        }
      });
  }

  // example register with email
  signUp() {
    const rnd = Math.floor(Math.random() * 1000);

    this.authService
      .Signup(`user${ rnd }@email.com`, 'valid_firebase_password', { bloodType: 'B+' })
      .pipe(catchError...)
      .subscribe({
        next: (user) => {
          this.router.navigateByUrl('/private/dashboard');
        }
      });
  }
  // example login with google, later we need to figure out the new user
  loginGoogle() {
    this.authService
      .LoginGoogle()
      .pipe(catchError...)
      .subscribe({
        next: (user) => {
          this.router.navigateByUrl('/private/dashboard');
        }
      });
  }
}

The returned object from Firebase looks like this

// User model from firebase containst those basic information
// <userCredentials.User>
{ 
  refreshToken
  ,displayName
  ,email
  ,uid
  ,providerData
}

Note that we will use .then() syntax to be able to catch errors in UI, but you can choose to use asycn await with try catch block instead. We also returned the Promise as an Observable using RxJS defer() operator, to keep our solution as smooth as it can be.

You can also use RxJS from() but remember it is a hot observable, and will be emitting a value whether subscribed to or not. A subscription will not make it run again though.

AngularFire provides three observables to watch changing tokens and auth state:

// AngularFire observables

const auth = inject(Auth); // from @angular/fire/auth
user(auth);
authState(auth);
idToken(auth);

The difference is that authState observable is not triggered during refresh token. However, the idToken is what we will be using most of the time.

Now what?

If you are using the application with other Firebase apps, like Firestore, or Realtime Database, there is nothing you aught to do besides catching errors, and creating users. The authenticated user is sent via request.auth, and you can manage access directly in Firebase console, using rules. This article is not about that. If you, like the rest of us, connect to your own API, then stick around.

anchorThe way forward

In Firebase Documentation:

..., you can retrieve an ID token from a client application signed in with Firebase Authentication and include the token in a request to your server. Your server then verifies the ID token and extracts the claims that identify the user (including their uid, the identity provider they logged in with, etc.). This identity information can then be used by your server to carry out actions on behalf of the user.

We only need proper authorization for our API, no third-party provider communication is needed.

Speaking directly to a Google API, or a Twitter API, would request knowing the provider, and passing the correct access token given by the provider, not by Firebase Auth.

We have three ways to build our application:

  1. Keep it foreign: let all authentication and user management be on Firebase
  2. Bring it home: recreate the users on our server, only use Firebase Auth for social login
  3. Middle ground: Use Firebase for authentication, and map to a local user (recommended)

Here is a general outline for each:

anchorI. Keep it foreign

We can continue to use the same user returned from Firebase, and completely rely on Firebase to manage users. we can add extra attributes using Admin SDK: custom claims.

Pros: one place to manage users, Cons: one place to manage users, that isn’t our place. It’s not an easy decision to let a third party take control of all user management, it is a pattern to have the authentication on one server, and user management on another. This also involves having to create our own in house admin platform to manage users.

The sequence of events is as follows:

  • Sign in with username and password, or third party through Firebase.
  • For a new user, request bloodType from user
  • Send result to API with Admin SDK, to verify user. Use setCustomUserClaims for custom attributes.

anchorII. Bring it home

The opposite extreme is to verify the token to recreate another access token, then take it from there. This involves asking the user to choose a new password after signing in by social accounts. There is also the option to consult the Firebase Admin SDK to verify the token and use it to identify the user, instead of a password.

Pros: it shuts off Firebase User management completely, Cons: it involves token management on the API level. It might be a good solution if we have other providers or older users. It involves JWT creation and validation.

This is by far the least attractive solution, and it is not much about Firebase, so I will not dig deep into this one.

anchorIII. A middle ground

The middle ground is to use the Firebase Auth to create users on Firebase, then map them to local users, and depend on our state management instead of Firebase state management. This is my most favorite solution. It decouples authentication from user management without ever having to create new tokens. I am going with this option.

Here is a note on Firebase docs worthy of mentioning: Custom claims are added to the user's ID token which is transmitted on every authenticated request. For profile non-access related user attributes, use database or other separate storage systems.

So they too recommend this approach.

Here are the basic ingredients for all three.

anchorLaying the foundation

All three solutions involve Admin SDK verification. Since that is sever side matter, we are going to build a simple NodeJs and ExpressJS server, to mimic our API. We need a middleware to capture all API calls, with the token set in the header, to verify the user.

anchorFirebase Admin SDK

First, go through the documentation on how to add Firebase Admin SDK to the server.

anchorExpress middleware

Then we'll place the SDK application in its function, and pass it to the middleware, or any other route to use it.

// find this in /server/firebase.sdk.js
// API server Admin SDK app

// use firebase to verify
const admin = require("firebase-admin");
// get this json from the project settings in Firebase console
const serviceAccount = require("./path-to-service-account.json");

// initialize firebase
exports.sdk = admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});

Then we can pass it to the middleware

// server/auth.middleware.js 
// expressJs middleware on API server

module.exports = function (sdk) {
  return function (req, res, next) {
    // check header for token then verify, find user, return info
    var authheader = req.headers['authorization'];
    // if not move on with none
    if (authheader) {
      // call auth then veirfyIdToken
      sdk
        .auth()
        .verifyIdToken(authheader)
        .then(function (decodedToken) {
          // save in res locals, or fetch profile from db first... 
          // depends on choice of solution
          res.locals.user = decodedToken;

          // next
          next();
        })
        .catch(function (error) {
          res.locals.user = null;
          next();
        });
    } else {
      next();
    }
  };
};

// in the main server (server/server.js), get the sdk
const sdk = require('./firebase.sdk').sdk;
// pass it to middlewear
const verify = require('./auth.middleware')(sdk);
app.use(verify); // app is the express app created

// we can also pass it to any route that needs it
app.use('/api', require('./api/account')(sdk));

// ... other routes and and app.listen

An example Express API route that makes use of it looks like this

// example routes file for getting account information after firebase validation
const express = require('express');

module.exports = function (sdk) {
  var router = express.Router();

  router.get('/account', function (req, res) {
    // get auth from req local
    const user = res.locals.user;
    if (user) {
      res.json({
        data: user,
      });
    } else {
      res.status(401).json({
        message: 'Access denied',
        code: 'ACCESS_DENIED',
      });
    }
  });

  router.post('/user', function (req, res) {
    // use the sdk passed when needed
    sdk.auth().setCustomUserClaims(...)
    // ...
  });
  // export and use this router in the main server
  return router;
};

We can also use getAuth() directly instead of passing round the sdk application. I have no preference.

// alternatively getAuth directly in any Express file 
const { getAuth } = require('firebase-admin/auth');

// in a route:
getAuth().setUserCustomClaims...

Diving in

The first option is to rely completely on Firebase Auth, to dive into it, tune in next Tuesday inshalla. Let's get some sleep. 🔻

  1. 10 months ago
    I - Setting up AngularFire with Authentication
  2. 10 months ago
    II - Firebase User Management in Angular
  3. 10 months ago
    III - Firebase For Authentication only in Angular