II - Firebase User Management in Angular
Published: 2024-01-09 10:00:00

To recap, we need to create a sign up process using password and Google provider. We need to keep all user management on **Firebase**. We need to add role `admin`, and `bloodType` attributes for every user.

> One thing to keep in mind as we maneuver our way through Firebase Auth, you can get the token only on the **client side**, and you can set claims only on the **server side**.

Let's begin with the server routes, and see how we can set custom claims.

I had to ditch StackBlitz for this project, find the code final code on [GitHub](https://github.com/ayyash/garage-fb-auth/tree/I-keep-it-foreign).

## Setting custom claims

The page that user signs in, should have the extra field in it for `bloodType`. Let's create an API route for it: `POST /api/user`

```js
// server/routes.js route to set custom claims
// the sdk created earlier is passed or can be imported directly
// const { getAuth } = require('firebase-admin/auth');
// const sdk = getAuth();

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

  // update local user with custom claims
  router.post('/user', function(req, res) {
    // get user from middleware
    const user = res.locals.user;
    // read body params, could be an object
    const bloodType = req.body.bloodType;
    // set admin to true, set a new property bloodType
    sdk.setCustomUserClaims(user.uid, {admin: true, bloodType}).then(() => {
      // return user although we dont have to
      res.json({...user, bloodType, admin: true});
    }).catch(function(error) {
      res.status(401).json({
        message: 'Invalid token'
      });
    });
  });
  return router;
}
// pass this in main server like this
webapi.use('/api', require('./routes')(sdk));
```

In `AuthService` we pipe a `swichMap` to call this API. Let's not be too specific about typing today, thank God for `any`

```ts
// services/auth.service

// a new function to sign up, passing bloodType
Signup(email: string, password: string, custom: any): Observable<any> {
  const res = () => createUserWithEmailAndPassword(this.auth, email, password);

  return defer(res).pipe(
    // send back to API server to create user with custom claims
    switchMap(_ => this.UpdateUser(custom))
  );
}

UpdateUser(custom: any): Observable<any> {
  return this.http.post('/api/user', custom);
}
```

We could have sent the token in the body, but because updating users might be needed in a different context, we'll rely on the middleware instead. We need to add the **authorization header** to be sent for the second call. We'll do that right after we figure out Google provider.

## Sign in with Google

To Sign in with *password*, all we have to do is return the user to the client-side, and start using it. The Sign in with Google however is a bit trickier. We have **no clue whether the user exists or not**. One way to find out is check the custom attribute `bloodType`. Another way is to check user [metadata](https://firebase.google.com/docs/reference/js/auth.usermetadata), which contains `lastLoginAt`, and `createdAt`, if they are milliseconds apart, this is a new user. But the best way is to check for [additional user info](https://firebase.google.com/docs/reference/js/auth.md#getadditionaluserinfo) right after login: `getAdditionalUserInfo()`

If the property `isNewUser` is set to `true`, we need to request the `bloodType` before we move on. So our service will just return the user, and let the component decide what to do with it:

```ts
// services/auth.service

// the login with google can check for new user:
LoginGoogle(): Observable<boolean> {
  const provider = new GoogleAuthProvider();

  const res = () => signInWithPopup(this.auth, provider)
    .then((userCredential) => {
      // import getAdditionalUserInfo from '@angular/fire/auth'
      const info = getAdditionalUserInfo(userCredential);
      return info.isNewUser;
    });
	return defer(res);
}
```

In the component, how to react to show a sign-up form is beyond the scope of this article. Once we get the `bloodType`, we call an update method (on click of submit button). There is nothing extra we need to set, because the middleware will received the token, verify and unpack the user, then set custom claims and return.

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

loginGoogle() {
  this.authService.LoginGoogle().pipe(
    catchError(...)
  ).subscribe({
    next: (isNewUser) => {
      if (!isNewUser) {
      // log user in and redirect
        this.router.navigateByUrl('/private/dashboard');
      } else {
        // somehow redirect or show a sign up form
        someVarShowInput = true;
      }
    }
  });
}

// when bood type is provided, finish sign up
updateSingup() {
  this.authService.UpdateUser({ bloodType: 'B+' }).subscribe({
    next: (user) => {
     // logged in and signed up, redirect
     this.router.navigateByUrl('/private/dashboard');
    }
  });
}
```

### Putting them together

One gap we might want to think about is if user signs up with Google, but does not continue to provide the `bloodType`. The better way is to rely on our API to check if it exists or not.

Also, a better user experience for the sign in with **Password** is to sign user in first, and if an error comes back, ask for the `bloodType` and sign up, which mimics the Sign in with Google.

We will do both in the next episode.

## Adding the authorization header

The token in Firebase is wrapped inside the `idToken` subscription, but under the hood, it does not change during the session. We need to find a way to get that token at initialization, and keep it alive during the session.

We begin with the end in sight. The **http interceptor** function should have a way to add the bearer token:

```ts
// services/http.fn

// the getHeaders should add token from auth state
const getHeaders = (): any => {
  const authState = inject(AuthState);
  //  authorization here
  let headers: any = {};
  // to be implemented:
  const _auth = authState.GetToken();
  if (_auth && _auth !== '') {
    headers['authorization'] = `Bearer ${_auth}`;
  }
  return headers;
};

export const AppInterceptorFn: HttpInterceptorFn = (
  req: HttpRequest<any>,
  next: HttpHandlerFn
) => {
  const adjustedReq = req.clone({
    // ... prefix url here
    setHeaders: getHeaders(),
  });

  return next(adjustedReq);
};
```

We need an `AuthState` service with at least `GetToken` that returns a string, we can inject the Firebase `Auth` in it, and start listening to the `idToken`. We can keep the value in a `BehaviorSubject` to get the value when we need it.

```ts
// new services/auth.state
@Injectable({ providedIn: 'root' })
export class AuthState {
  // let the state have a behavior subject to keep track of value
  private token: BehaviorSubject<string> = new BehaviorSubject(null);
  token$: Observable<string> = this.token.asObservable();

  constructor(private auth: Auth) {
    // subscrine to id token changes
    idToken(this.auth).subscribe({
      next: (token) => {
        this.UpdateState(token);
      }
    });
  }

  GetToken() {
    return this.token.getValue();
  }

  // update state
  UpdateState(token: string) {
    this.token.next(token);
  }
}
```

This might look like a good idea at first, but if we immediately call the API, this method is **too slow**. The token is still `null`. Instead, we need to populate the state immediately after sign in or sign up. We need to further build the pipe with `idToken`

```ts
// services/auth.service
export class AuthService {
	constructor(
	  private http: HttpClient,
	  private authState: AuthState,
	  private auth: Auth
	) {}

	// pipe after login, or sign up, to save the token in state
	Signup(...): Observable<any> {
	// ... createUserWithEmailAndPassword

	return defer(res).pipe(
	  // with idToken
	  switchMap(_ => idToken(this.auth)),
	  tap((token) => {
	      // save state first
	      this.authState.UpdateState(token);
	  }),
		// then call backend
	  switchMap(_ => this.UpdateUser(custom)));
	}
}
```

This will ensure that the header has the correct token before calling the API. All other API's can be taken care of with calling the `AuthState` immediately at creating the application, and where better to do that than `APP_INITIALIZER`? We already had this set up before:

```ts
// main.ts

// in our root providers array
bootstrapApplication(AppComponent, {
  providers: [
    // ...
    {
      provide: APP_INITIALIZER,
      // dummy factory
      useFactory: () => () => {},
      multi: true,
      // injected depdencies, this will be constructed immidiately
      deps: [AuthState],
    },
  ],
});
```

## Auth route guard

In **AngularFire** documentation you can find a reference to built-in [Auth guard functionality](https://github.com/angular/angularfire/blob/master/site/src/auth/route-guards.md). We can make use of them, for example if we want to block a route for other than admins.

```ts
// main.ts (or in routing)
import { canActivate, hasCustomClaim } from '@angular/fire/auth-guard';
const adminOnly = () => hasCustomClaim('admin');

// define a route
{
  path: 'private',
  loadChildren: () => import('./app/routes/dashboard.route').then((m) => m.DashboardRoutes),
  // use canActivate
  ...canActivate(adminOnly)
}
```

They have other examples of mixing and matching and using claims. In our project, I would rather create **our own guard**:

```ts
// services/augh.guard
export const AuthCanActivate: CanActivateFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> => {
    const auth = inject(Auth); // @angular/fire/auth
    const router = inject(Router);

    // get data from route, for example {date: {role: 'admin'}}
    const role = route.data.role;

    // watch user (import from @angular/fire/auth)
    return user(auth).pipe(
      // switch map to get claims
      switchMap(_user => _user ? _user.getIdTokenResult() : of(null)),
      map(_user => {
        // if user exists let them in, else redirect to login
        if (!_user) {
          router.navigateByUrl('/public/login');
          return false;
        }
        // user exists, match claims to route data
        if (!_user.claims.hasOwnProperty(role)) {
          router.navigateByUrl('/public/dashboard');
          return false;
        }
        return true;
      })
    );
};
```

In routes:

```ts
// main.ts

{
  path: 'private',
  loadChildren: () => import('./app/routes/dashboard.route').then((m) => m.DashboardRoutes),
  canActivate: [AuthCanActivate],
  data: { role: 'admin'}
},
```

So by setting `claims.admin` to `true`, we gain access to the dashboard route.

## The curious case of the stale token

After calling our API to set **custom claims**, the token has changed on Firebase, but it did not reflect in our client side. This could cause problems if we depend on the custom claims, the token on the client at this stage is stale. Here are some scenarios of what could go wrong, and we need to fix them.

-   After signing up and updating claims, redirecting to a private route that depends on custom claims `admin` attribute. The token does not have it.
-   Updating user **bloodType** to **O-** then within less than an hour, refreshing the application. The token still says **B+.** This is a [life threatening](https://www.aljazeera.com/program/newsfeed/2023/12/21/israeli-forces-target-palestinian-paramedics) situation!
-   Although rare: leaving a page open for an hour without any interactions, then calling an **http** with the current token. The server verification will fail.

### Refresh after API update

In the first case, on the next `augh.guard` call , we used `user()` observable, which is wrapped around `onIdTokenChange` of Firebase (See [code of rxFire](https://github.com/FirebaseExtended/rxfire/blob/f74e8995ec4f09ea497a5ebc0ce3361f6c5c33a0/auth/index.ts)). Then we called `getIdTokenResult()`. Here is the thing:

-   `onIdTokenChange` does not refresh the token
-   `getIdTokenResult` returns a new token only if the current token has expired (or about to).

None of the **AngularFire** observables is capable of refreshing the token. To refresh it, we need to call `.getIdToken(true)`. So after an update statement, and before we redirect to a private route that uses the guard, we need to **hard refresh the token**. Since we are already listening to `idToken` in the pipe of signing up, this might create a loop. So let's decide now:

-   Use closer-to-home `getTokenId()` instead of `idToken`
-   Force a refresh with `getTokenId(true)`

Here is the [Firebase documentation](https://firebase.google.com/docs/auth/admin/custom-claims#client_side_implementation_javascript_2) of what should happen in normal JavaScript.

And here is our `Signup` and `UpdateUser`

```ts
// services/auth.service
Signup(...): Observable<any> {
   // ...

  return defer(res).pipe(
    // first token, directly from getIdToken, this is good enough
    // because it is used after a Firebase function "create user"
    switchMap(_ => this.auth.currentUser.getIdToken()),
    tap(token => {
      // save state first, to use token
      this.authState.UpdateState(token);
    }),
    // then call update
    switchMap(_ => this.UpdateUser(custom))
  );
}

UpdateUser(custom: { bloodType: string; }): Observable<any> {
  return this.http.post('/user', custom).pipe(
  	// force a new token, and wait for it
  	// it must be a switchMap
    switchMap(_ => this.auth.currentUser.getIdToken(true)),
    tap(token => {
  	    // update state for future http calls
        return this.authState.UpdateState(token)
    })
  );
}
```

The `switchMap` in the `UpdateUser` is the best way to ensure the new token is returned from the `Promise`, other operators are too asynchronous to catch in the redirect.

Any call to the `UpdateUser` should now force update the token afterwards. [Life saved](https://www.gazastudents.org/en).

### Update state after Login

After login (from Firebase), we have no problems because the token received has custom claims, we however need to keep our `AuthState` up to date, with the `idToken` in the constructor that should be enough. But always pay attention, if there is any immediate **http call** after login, it must be called in place, the `idToken` subscriber **might be too slow**.

Let's refactor our `AuthService`

```ts
// services/auth.service

// a single pipe for all
private _updateState = (force: boolean) => pipe(
  switchMap(_ => this.auth.currentUser.getIdToken(force)),
  tap((token) => {
    // save state as well
    this.authState.UpdateState(token);
  })
);

Signup(email: string, password: string, custom: any): Observable<any> {
  // ...
  return defer(res).pipe(this._updateState(false),
    switchMap((_) => this.UpdateUser(custom))
  );
}

UpdateUser(custom: any): Observable<any> {
  return this.http.post('/user', custom).pipe(this._updateState(true));
}
```

### Refresh 401 token

Instead of trying to keep the token fresh by manipulating events, it's better to let the API http call return a 401 when the token expires, then handle it.

> Having the `auth.guard` on multiple routes is good enough to keep the token fresh. But it's not good enough to update our state with the new token.

We've already had this feature implemented in our [Angular Authentication series](https://garage.sekrab.com/posts/authentication-in-angular-the-circular-dependency-in-di-issue-popping-its-ugly-head). Here it is as an **http interceptor function**, for **standalone**, instead of an interceptor class.

```ts
// http interceptor with refresh token sequence
let isBusy = false;
let recall: Subject<boolean> = new Subject();

	const handle401Error = (originalReq: HttpRequest<any>, next: HttpHandlerFn, authState: AuthState): Observable<any> => {
	if (!isBusy) {
	  isBusy = true;
	  // progress subject to false
	  recall.next(false);
    // this RefreshToken needs to be implemented
	  return authState.RefreshToken().pipe(
	    switchMap((result: any) => {
	      if (result) {
	        recall.next(true);
				  return next(
            // pass around the authstate injected
	          originalReq.clone({ setHeaders: getHeaders(authState) })
	        );
	      }
	    }),
	    catchError((error) => {
	      // logout may be? doesn't matter
	      return throwError(() => error);
	    }),
	    finalize(() => {
	      isBusy = false;
	    })
	  );
	} else {
	  return recall.pipe(
	    filter((ready) => ready === true),
	    switchMap((ready) => {
	    return next(
	        originalReq.clone({ setHeaders: getHeaders(authState) })
	      );
	    })
	  );
	}
};

export const AppInterceptorFn: HttpInterceptorFn = (
  req: HttpRequest<any>,
  next: HttpHandlerFn
) => {

  const url = 'prefixurl' + req.url;
  // inject and pass around
  const authState = inject(AuthState);
  const adjustedReq = req.clone({
    url: url,
    setHeaders: getHeaders(authState),
  });

  return next(adjustedReq).pipe(
    catchError((error) => {
      // if this is really an http error
      if (
        error instanceof HttpErrorResponse &&
        // and of 401 status
        error.status === 401
        // filter out login calls
        req.url.indexOf('login') < 0
      ) {
        return handle401Error(adjustedReq, next, authState);
      }
      // rethrow error
      return throwError(() => error);
    })
  );
};
```

The `AuthState` `RefreshToken` returns an observable that calls the `getIdToken(true)` to be sure it is fresh (though it isn't needed, we already know it will be refreshed).

```ts
// services/auth.state

RefreshToken(): Observable<any> {
  // defer, or from() have the same effect
  return defer(() => this.auth.currentUser.getIdToken(true)).pipe(
    tap((token) => {
      // update state with token for next http call
      this.UpdateState(token);
    })
  );
}
```

Tried it. And it works as expected. I had to create a page with a button and leave it for an hour till the token expired!

### Off-app updates and sensitive changes

Another scenario often referred to in Firebase documentation in a subtle way is when the token that becomes invalid for changes happening off our app. For example if we have an admin portal where we can change the `bloodType` of a certain user, while they are using the client app. The token will expire in at least an hour. According to Firebase, that is okay. And it is, most of the time.

Firebase also suggests using [re-authentication](https://firebase.google.com/docs/auth/web/manage-users#re-authenticate_a_user) for serious actions, and it actively errors out on certain sensitive actions if user is not recently logged in. This scenario is out of scope of this article, but I just wanted to drive your attention that this is indeed **a case with no solution**. You just have to [have soul](https://www.youtube.com/watch?v=MXR9nsMk3Rw).

## Getting user custom attributes

In order to read the custom claims for the logged in user, we can use `getUserTokenResult()` then read the `user.claims`. We can do that by subscribing to the `user` **AngularFire** subscriber, or we can map to our internal user model, and update it upon sign in, sign up, and user update. All roads lead to [Jerusalem](https://electronicintifada.net/blogs/tamara-nassar/settlers-go-killing-spree-west-bank). In a simple listener, we need to pipe (again):

```ts
// home.component
// an example of usning claims

// OnInit:
this.status$ = user(this.auth).pipe(
  filter(user => !!user),
  // use the promise directly, this works
  switchMap(user => user.getIdTokenResult()),
  map(token => token.claims)
);

// in the template
`{{ (status$ | async)?.bloodType }}`
```

But it is much better to wrap everything in a proper user model, that we can carry around, and scale as we go. We can place that in the `AuthState` as well, instead of wrapping the token alone, it can include all user properties. Mapping to an internal model is a subject we will dig into in the final recommended solution.

> I have no idea what the point is in burying the claims a level too low like that!

## Logout

To logout, we use the Firebase `signOut` then we need to nullify the token in our own `AuthState`. First inside the `AuthService`:

```ts
// services/auth.service

Signout(): Observable<boolean> {
  // signOut from '@angular/fire/auth'
  const res = () => signOut(this.auth).then(() => {
    this.authState.Logout();
    return true;
  });
  // catch error and return false if needed
  return defer(res);
}
```

Then in our `AuthState`

```ts
// services/auth.state
Logout() {
  this.token.next(null);
}
```

Finally in our component, a click handler:

```ts
// any component click handler
Logout() {
  this.authService.Signout().subscribe({
    next: () => {
      this.router.navigateByUrl('/public/login');
    }
  })
}
```

Let us move on to the other recommended solution. Next Tuesday inshalla. 🔻

### RESOURCES

-   [GitHub project](https://github.com/ayyash/garage-fb-auth/tree/I-keep-it-foreign)
-   [Firebase Docs - User Metadata](https://firebase.google.com/docs/reference/js/auth.usermetadata)
-   [Firebase Docs - getAdditionalUserInfo](https://firebase.google.com/docs/reference/js/auth.md#getadditionaluserinfo)
-   [AngularFire Auth Guard](https://github.com/angular/angularfire/blob/master/site/src/auth/route-guards.md)
-   [RxFire source code](https://github.com/FirebaseExtended/rxfire/blob/f74e8995ec4f09ea497a5ebc0ce3361f6c5c33a0/auth/index.ts)
-   [Firebase Docs - Custom Claims](https://firebase.google.com/docs/auth/admin/custom-claims#client_side_implementation_javascript_2)
-   [Firebase Docs - Reauthentication](https://firebase.google.com/docs/auth/web/manage-users#re-authenticate_a_user)

### RELATED POSTS

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