III - Firebase For Authentication only in Angular
Tags: webdev, angular, firebase, tutorial
Published: 2024-01-16 10:00:00

The middle ground is to use Firebase Auth, for authentication, and use our own API for user management.

> Find the [project on Github](https://github.com/ayyash/garage-fb-auth/tree/III-middleground).

## Express API routes

The idea of having two servers, one for authentication and one for profiling is to deal with the profile server (our API) as a normal data source, with authentication required. The authentication server is expected to send in an access token, which is verified first, before it moves down the pipe of creating and editing user profile. So after signing in and signing up with password, or after signing in with Google, we need to **send the token to our Express server** to create the user, fetch user, or patch user.

```js
// server/routes
router.post('/auth/login', function (req, res) {
  // if id does not exist create user, else update user
  const payload = req.body;
  const token = payload.token;
  // we need to first verify token
  sdk.auth().verifyIdToken(token).then(function (decodedToken) {
    // then find user by id
    const id = decodedToken.uid;
    // find profile in db, example
    const profile = {...};

    // if user does not exist, create one
    if (!profile) {
      // save new user to db then return
      // we don't have bloodType just yet
      const _newuser = {
        id: decodedToken.uid,
        picture: decodedToken.picture,
        // use the decodeToken for now
        email: decodedToken.email,
        // some hard coded attributes
        admin: true,
        // this flag is important
        newUser: true};
      // save _newuser in db here, then return
      // ...
      res.json(_newuser);
    } else {
      // return existing user with full profile, including bloodtype
      // watch out for client-side gap, bloodType may still not exist
      res.json(profile);
    }

  }).catch(function (error) {
    // 401
  });
});

// update logged in user
router.patch('/user', function(req, res) {
  // get from middleware
  const user = res.locals.user;

  if (user) {
    // update user in db with body
    const payload = req.body;
    let profile = {...user};
    profile.bloodType = payload.bloodType;
    // switch off newUser
    profile.newUser = false;
    // save to db
    // ...
    // update locals
    res.locals.user = profile;

    // return profile
    res.json(profile);
  } else {
     res.status(401).json({
      message: 'Access denied',
      code: 'ACCESS_DENIED'
    });
  }
});
```

As you can see I have saved the `newUser` flag in our server, because it is crucial to close a UI gap, we'll cover later.

Sign in with password in `AuthService` would call the first route.

```ts
// services/auth.service
// login changed, we'll map to IAuthInfo later
Login(email: string, password: string): Observable<IAuthInfo> {
  const res = () => signInWithEmailAndPassword(this.auth, email, password);
  return defer(res).pipe(
    // get the token to force a fresh one
    switchMap(auth => auth.user.getIdToken()),
    // login user in our API
    switchMap(token => this.LoginUser(token))
  );
}

LoginUser(token: string): Observable<IAuthInfo> {
  return this.http.post('/auth/login', { token });
  // we need to later save into state
}
```

Sign up with password would call the first route, then request the `bloodType` from the user, then call the `PATCH /user` next.

```ts
// services/auth.service
// sign up changed
SingUp(email: string, password: string, custom: any): Observable<IAuthInfo> {
  // here send a sign up request, with extra params
  const res = () =>
    createUserWithEmailAndPassword(this.auth, email, password);

  return defer(res).pipe(
    // first new token
    switchMap(auth => auth.user.getIdToken()),
    // log user in and request extra information
    switchMap(token => this.LoginUser(token)),
    // then patch
    switchMap(_ => this.UpdateUser(custom)
  );
}

UpdateUser(custom: any): Observable<IAuthInfo> {
  return this.http.patch('/user', custom).pipe(
    // we need to update state again here
  );
}
```

In our components so far nothing has changed. But we can do better. In a minute. First, let's see what changed on Sign in with Google.

Remember that we faced an issue with `null` tokens before, so here we `getIdToken` before any action to API.

## Sign in with Google

In the previous episode we depended on the Firebase service to tell us whether the user is new or not using `getAdditonalUserInfo`, before we proceeded to show the sign up form. Now we need to log the user in our API server first. Our API server can take on the job and check if `uid` already exists, and return `newUser` from our data source. Initially the service call looks like this

```ts
// serivces/auth.service

// Login by Google initially:
LoginGoogle(): Observable<IAuthInfo> {
  const provider = new GoogleAuthProvider();
  const res = () => signInWithPopup(this.auth, provider);

  return defer(res).pipe(
    switchMap(auth => auth.user.getIdToken()),
    // call our API and check for uid
    switchMap(token => this.LoginUser(token))
  );
}
```

We can use the `IAuthInfo` model to check if user is new (remember the `/auth/login` returns `newUser` flag).

```ts
// components/public/login.component

// login with Google
loginGoogle() {
  this.authService.LoginGoogle()
  .subscribe({
    next: (user) => {
      // read newUser from our API
      if (!user.newUser) {
        this.router.navigateByUrl('/private/dashboard');
      } else {
        // show the sign up field (somehow)
        this.showMeForm = true;
      }
    },
  });
}
// finish sign up by google
update() {
  // grab form bloodType, example: B+
  this.authService.UpdateUser({ bloodType: 'B+' }).subscribe({
    next: (user) => this.router.navigateByUrl('/private/dashboard');
  });
}
```

If user is new, after submitting and calling `update` we proceed normally to call `UpdateUser({bloodType})` to `PATCH` user.

### The new user flag

The first thing we need to fix is this gap between `Login` and `Sign Up`, if user does not finish sign up and comes back later to try with the same Google email, Firebase will return an existing user. But that user never provided their `bloodType`. To fix that, we need to keep track of the `newUser` flag. The flag is turned off only when user finishes the sign up and provide `bloodType`. Which is in the `PATCH /user` route.

This is one of the two issues I promised we'd fix in the previous episode.

### Request Email

The second issue to fix is the email. If you do not need an email in your application this should be fine. But if you, like the rest of us, would like to annoy your customers with [endless propaganda emails](https://jackpoulson.substack.com/p/inside-the-pro-israel-information), then you need the email. First we `addScope`:

```ts
 // serivces/auth.service
LoginGoogle(): Observable<IAuthInfo> {
  const provider = new GoogleAuthProvider();
  provider.addScope('email');
  // ...
}
```

The returned token does not have the email. Nice going Firebase! The extra piece of information is found in two places,

-   `userCredential.user.providerData[]`. An array of well-formed data (`UserInfo[]`)
-   `AdditionalUserInfo.profile`. The format depends on the provider, needs to be requested via `getAdditionalUserInfo` but also has a very important piece of information: `isNewUser`. But we can do without it now.

For the particular case of Google sign in, we'll rely on the first method. But you might want to investigate further for X (Twitter) provider, and no, not Facebook, we're [boycotting Facebook](https://www.youtube.com/watch?v=cnj0rmnsAdI).

The change affects three places: `LoginGoogle`, `LoginUser`, and the route `/auth/login`:

```ts
// services/auth.service
// catch email in google, and send it to API
LoginGoogle(): Observable<IAuthInfo> {
 //...

  return defer(res).pipe(
      switchMap(auth => auth.user.getIdToken()),
			// login with provider email
      switchMap(token => this.LoginUser(token, this.auth.currentUser.providerData[0].email))
  );
}
// catch email and send to API
LoginUser(token: string, email?: string): Observable<IAuthInfo> {
    return this.http.post('/auth/login', { token, email });
}
```

On the server:

```js
// in server/routes.js
router.post('/auth/login', function (req, res) {
  
  const payload = req.body;
  const token = payload.token;
 
  sdk.auth().verifyIdToken(token).then(function (decodedToken) {
    //...
    if (!profile) {
      // ...
      // read email from payload instead of decodedToken
      const _newuser = {
        email: payload.email
        // ...
      } 
      // ...
      res.json(_newuser);
    }
    //...
  })
});
```

Which means, we need to send the email with **Sign in with Password.** Let's also do the promised enhancement of combining sign in and sign up with password.

## Combine sign in and sign up

For best user experience, we should allow user to **sign in first**, and if the user is new, **request additional information for sign up**. This matches the sequence of events handled with Google login, or any third party login.

In our component we need just one `login` call, that switches to **sign up** if the user is new.

```ts
// components/public/login.component
// change to login to allow sign up
login() {
  this.authService
    .Login('email@address.com', 'valid_firebase_password')
    .pipe(catchError...)
	  .subscribe({
      next: (user) => {
        // read newUser from our API
        if (!user.newUser) {
          this.router.navigateByUrl('/private/dashboard');
        } else {
          // show the sign up field
          this.showMe = true;
        }
      },
   });
}
```

This looks exactly like the `loginGoogle` method. We are on the right track.

In our `AuthService`, the `Login` needs to change a bit, it will catch the **invalid credentials** error, to switch to `SignUp`.

```ts
// services/auth.service
// Login first
Login(email: string, password: string): Observable<IAuthInfo> {
  const res = () => signInWithEmailAndPassword(this.auth, email, password);
  return defer(res).pipe(
    switchMap(auth => auth.user.getIdToken()),
    // send email
    switchMap(token => this.LoginUser(token, email)),
    catchError(err => {
      // catch invalid credentials to sign up
      if (err.code === 'auth/invalid-credential') {
        return this.SingUp(email, password);
      }
      // throw everything else
      return throwError(() => err);
    })
  );
}
// remove custom attributes
SingUp(email: string, password: string): Observable<IAuthInfo> {
  const res = () =>
    createUserWithEmailAndPassword(this.auth, email, password);
  return defer(res).pipe(
    switchMap(auth => auth.user.getIdToken()),
    // stop here and return, send email as well
    switchMap(token => this.LoginUser(token, email)),
  );
}
```

Now the `update` and `UpdateUser` take care of patching the `bloodType`, just as in the Google sign in sequence. Great. Now what?

## Authentication header

In order for the `PATCH /user` call to work, we need to pass a fresh token into the header. The following is the authentication middleware, nothing fancy, we just get the user from data source first, match to the `uid` returned by Firebase token.

```js
// server/auth.middleware.js
// update to find profile by uid first
sdk.auth().verifyIdToken(authheader).then(function (decodedToken) {
  // example:
  let profile = profiles.find((profile) => profile.id === decodedToken.uid);
  // if found, set, else nullify
  if (profile){
    res.locals.user = profile;
  } else {
    res.locals.user = null;
  }
  // next
  next();
})
// ...
```

Our http interceptor is the same we created in the last episode. It too will have a 401 error catch, that will request a fresh token from Firebase. Let's dig in the `AuthState` and user model to see how we can save the token.

## Maintaining state

The end result we want for the user state is to be able to display the information based on a state item (like the one we developed in our [Angular Authentication series](https://garage.sekrab.com/posts/authentication-in-angular-why-it-is-so-hard-to-wrap-your-head-around-it)).

```ts
// components/some component
// template
`<div *ngIf="status$ | async as s">
  {{ s.email }} {{ s.bloodType }}
</div>`

status$: Observable<IAuthInfo>;
constructor(private authState: AuthState) {}
ngOnInit(): void {
  // watch auth state item
  this.status$ = this.authState.stateItem$;
}
```

Here are the ingredients:

-   An `IAuthInfo` to model our local user model
-   Update state after calling `LoginUser` and `UpdateUser` in `AuthService`
-   An `AuthState` service to keep track of user state
-   An `AuthGuard` to protect private route
-   Initiate the `token` property directly from Firebase

### Auth user model

The `IAuthInfo` has the basic attributes, and a public method to map our user

```ts
// services/auth.model
export interface IAuthInfo {
  id: string;
  bloodType?: string;
  admin?: boolean;
  // some properties from firebase
  picture?: string;
  email?: string;
  // a place for the token
  token?: string;
  // a boolean for newUser
  newUser?: boolean;
}

export const MapAuth = (auth: any): IAuthInfo => {
  // map incoming from db with our user
  // this isn't required, but preferable
  // token is not mapped here
  return {
    id: auth.id,
    email: auth.email,
    admin: auth.admin,
    bloodType: auth.bloodType,
    picture: auth.picture,
    newUser: auth.newUser
  }
}
```

The token is not mapped directly from API, it will be added from Firebase.

### Update service

After every visit to the API, we need to update the state, preferably by mapping to our local model, passing the fresh token coming back.

```ts
// services/auth.service
// update state after API calls
LoginUser(token: string, email?: any): Observable<IAuthInfo> {
  return this.http.post('/auth/login', { token, email }).pipe(
    map((auth: any) => {
      // map and save user in localstorage here, including token
      const _user = MapAuth(auth);
      this.authState.UpdateState({ ..._user, token });
      return _user;
    }),
  );
}
UpdateUser(custom: any): Observable<IAuthInfo> {
  return this.http.patch('/user', custom).pipe(
    map(auth => {
      // now update localstorage again
      const _user = MapAuth(auth);
      this.authState.UpdateState({..._user});
      return _user;
	  })
  );
}
```

### AuthState service

The main elements of our `AuthState` besides the constructor is a state item `BehaviorSubject` and a proper `UpdateState` method that updates the **Subject** and saves into **local storage**. It should inject the AngularFire `Auth` service as well. We should add the `GetToken` and `RefreshToken` to use in our http interceptor. Here it is 🔻

```ts
@Injectable({ providedIn: 'root' })
export class AuthState {
  // create an internal subject and an observable to keep track
  private stateItem: BehaviorSubject<IAuthInfo | null> = new BehaviorSubject(null);
  stateItem$: Observable<IAuthInfo | null> = this.stateItem.asObservable();
  constructor(
    // inject from '@angular/fire/auth'
    private auth: Auth
  )
  {
    // TODO: initiate state
  }

  UpdateState(item: Partial<IAuthInfo>): Observable<IAuthInfo> {
    // update existing state
    const newItem = { ...this.stateItem.getValue(), ...item };
    this.stateItem.next(newItem);
    // save into a key in localStorage
    localStorage.setItem('user', JSON.stringify(newItem));
    // return observable
    return this.stateItem$;
  }

  GetToken() {
    // return token as is
    const _auth = this.stateItem.getValue();
    return _auth?.token || null;
  }
  RefreshToken() {
    // refresh by calling getIdToken with `true`
    return defer(() => 
      this.auth.currentUser.getIdToken(true)).pipe(
      switchMap(token => {
        // update state then return an observable to pipe to
        return this.UpdateState({ token });
      })
    );
  }
}
```

> Read more about a `localStorage` [Angular wrapper](https://garage.sekrab.com/posts/localstorage-wrapper-service-in-angular), and [RxJS based state management](https://garage.sekrab.com/posts/rxjs-based-state-management-in-angular-part-iv) to have a fuller solution.

### [](https://garage.sekrab.com/posts/iii-firebase-for-authentication-only-in-angular#Auth-route-guard)Auth route guard

The guard now reads directly from our state item.

```ts
// services/auth.guard

export const AuthCanActivate: CanActivateFn = (...): Observable<boolean> => {
  // inject our auth state
  const auth = inject(AuthState);
  const router = inject(Router);

  const role = route.data.role;

  // watch user
  return auth.stateItem$.pipe(
    map(_user => {
     // if user exists let them in, else redirect to login
      if (!_user) {
        router.navigateByUrl('/public/login');
        return false;
      }
       // user exists, match property to route data
      if (!_user.hasOwnProperty(role)) {
        router.navigateByUrl('/public/login');
        return false;
      }
      return true;
    })
  );
};
```

### Initiate the token

When the application is launched, we can initiate the state from the local storage, through the same `APP_INITIALIZER` factory we already have. The constructor looks like this

```ts
// services/auth.state

constructor(...) {
  // initialize state directly from localStorage
	const _localuser: IAuthInfo = JSON.parse(localStorage.getItem('user'));
  if (_localuser) {
    this.UpdateState(_localuser);
  }
}
```

The most extreme case is when user lands on a protected route, and that user already has a saved state in local storage. The synchronous way of setting the initial state means the Auth Guard will have something to use. But the token used, is not only stale, but expired. *What does that mean?*

## The curious case of an expired token

Should we expel user? Given the fact that Firebase token is designed around the idea of refreshing itself in an hour, **we should not worry about it**. Also remember, the route guard is cosmetic, the real security is on API. The next API call will throw a 401, which will initiate a refresh sequence.

We can also make sure that 401 doesn't happen as often, if we update the token on initialization at least once:

```ts
// services/auth.state

constructor(...)
{
  // .. update localStorage first
  // take just 1, this will only get a new one if it's expired
  idToken(this.auth).pipe(
    take(1),
  ).subscribe({
    next: (token) => {
      if (token) {
        this.UpdateState({ token });
      }
    }
  });
}
```

If changes occur off the system, like user changes their password, we simply have to wait till it expires. Or again, we can guard important actions with our own "forced" token. For example, to request a list of codes only after user is logged in, we can do this

```ts
// example of a tighter security call
GetSafeCodes(): Observable<something> {
  // first get token with force flag
  return defer(() => this.auth.currentUser.getIdToken(true)).pipe(
    switchMap(token => {
      // then call API if token exists\
      return this.GetCodes();
    }),
    catchError(...)
  );
}
```

## Logout

Last bit to add is the logout, it is exactly like the one we had last episode, with the addition of cleaning `localStorage` in `AuthState`

```ts
// services/auth.state
Logout() {
  this.stateItem.next(null);
  localStorage.removeItem('user');
}
```

## Conclusion

So this is it. [Exposed](https://www.youtube.com/watch?v=4vKuB1su43c) and laid down. Here are some extra points to mention

-   Firebase Auth is a front layer that hides a lot of operations that take care of authentication, especially with third party
-   It is mostly asynchronous calls, returns Promises. This is a bit annoying, as you lose information down the pipe of multiple promises.
-   Use the modular SDK, for tree-shaking
-   The documentation will make you lose few pounds, or a few years of your life expectancy, if you are working with web, you have go through only these:
    -   [Authentication for Web](https://firebase.google.com/docs/auth/web/start) (And [reference of auth JS functions](https://firebase.google.com/docs/reference/js/auth))
    -   [Admin SDK (with NodeJS)](https://firebase.google.com/docs/auth/admin) (And [reference of Nodejs functions](https://firebase.google.com/docs/reference/admin/node))
-   The decoded token of Firebase in Admin SDK has a different model than the one returned after user sign-in on client-side. For example, `picture`, is for `photoUrl`. There is no `displayName` in Admin SDK. There is a `name` property, but it is not [documented](https://firebase.google.com/docs/reference/admin/node/firebase-admin.auth.decodedidtoken.md#decodedidtoken_interface)!

#### In summary:

-   We created a **NodeJs Express** server to handle Firebase verification
-   We created a service that handles sign in, and sign up, with Password and Google, that have the **same sequence**
-   We saved information in our **local storage**
-   We hunted down the **Firebase token**, refreshed it when needed, and relaxed when it was okay, relied on 401 when it made sense
-   We looked into two different implementations, one is more recommended than the other, and it is my personal go for

[Happy 2024.](https://gaza23.com/) Are we there yet?

Thanks for reading this far, if you have comments and questions, let me know. 🔻

### RESOURCES

-   [Firebase Docs](https://firebase.google.com/docs/auth/web/start)
-   [Firebase Admin SDK](https://firebase.google.com/docs/auth/admin)
-   [Project on GitHub](https://github.com/ayyash/garage-fb-auth/tree/III-middleground)

### RELATED POSTS
{% embed https://garage.sekrab.com/posts/authentication-in-angular-why-it-is-so-hard-to-wrap-your-head-around-it %}

{% embed https://garage.sekrab.com/posts/localstorage-wrapper-service-in-angular %}

{% embed https://garage.sekrab.com/posts/rxjs-based-angular-state-management %}