Authentication in Angular: Part III
Tags: angular, webdev, auth, tutorial
Published: 2023-02-14 07:32:12

Today let me add a proper logout, and before we move forward to more features, let's clean up and apply some of the lessons we learned before.

> Follow along on [StackBlitz](https://stackblitz.com/edit/auth-angular)

## Logout, in the right places

Looking back at our Http interceptor, a 401 might happen more than once, signaling a bad refresh token. When that happens we can either toast the user about a dramatic failure, and give them the chance to login again, or we can force a redirect to the login page ourselves. May the force be with us.

The solution is to reroute in the `Logout` function in `AuthState`. We'll make it optional because the `Logout` function is called when a user fails to login, and when the `AuthState` returns an invalid access token on a page that does not necessarily need authentication.

```ts
// services/auth.state

// reroute optionally
Logout(reroute: boolean = false) {
  // remove leftover
  this.RemoveState();
  // and clean localstroage
  localStorage.removeItem('user');

  if (reroute) {
    this.router.navigateByUrl('/public/login');
  }
}
```

In the `AuthState` constructor, do not reroute. If we do, we needlessly reroute users in safe routes. When it fails after a refresh token, that is a good place to redirect. With a but.

```ts
// http
return this.authService.RefreshToken()
  .pipe(
     switchMap((result: boolean) => {
        if (result) {
          //...
        }
     }),
     catchError(error => {
        // exeption or simply bad refresh token, logout an reroute
        this.authState.Logout(true);
        return throwError(() => error);
     }),
    // ...
  );
```

Another location we want to logout and reroute is when the user clicks the Logout button intentionally.

```ts
// app.component
Logout() {
  // logout and reroute
  this.authState.Logout(true);
}
```

### What if we are on a safe route already?

There is a scenario where the API call needs user authentication but the page it displays itself, has a public version of it. For example, if you route to a public Twitter account while you have your own login, you can see the like button and use it. If for some reason the refresh token is no longer valid, then clicking on the like button, should somehow warn users of the lack of authentication, and it should ask the user if they want to login again. In situations like that, having **a global redirect in the Http interceptor is not ideal**. The solution is contextual. Some API calls need to redirect, and some need to toast only. Here is an example of a like button click.

```ts
// example
this.tweetService.CreateLike(params).pipe(
   catchError(error => HandleSpecificError(error))
);

// somewhere in our common functions, or toast service
HandleSpecificError(error: any): Observable<any> {
	// if error is of http response 401, show a toast with a button to relogin
  if (error instanceof HttpErrorResponse && error.status === 401) {
    ShowToastWithLogin();
    return of(null);
  } else {
    // handle differently or rethrow
    return throwError(() => error);
  }
}
```

I would choose which way to go according to the type of project I am developing. There is no silver bullet. (You may use [HttpContext](https://angular.io/api/common/http/HttpContext) token for that.)

> We wrote about [toast messages in Angular previously](https://garage.sekrab.com/posts/catching-and-displaying-ui-errors-with-toast-messages-in-angular).

## Clean up

Before we move on I would like to clean up the `AuthState` to allow extra properties. We are going to still use this service to manipulate the `localStorage` of the browser. You might be tempted to create a new service for that, but I see no great value because we only need private members for the authenticated user information. Accessing the `localStorag` directly is not ideal, but let's keep going.

```ts
// authState update, add all necessary functions to deal with localStorage

private _SaveUser(user: IAuthInfo) {
  localStorage.setItem(
   ConfigService.Config.Auth.userAccessKey,
   JSON.stringify(user)
  );
}
private _RemoveUser() {
  localStorage.removeItem(ConfigService.Config.Auth.userAccessKey);
}

private _GetUser(): IAuthInfo | null {
  const _localuser: IAuthInfo = JSON.parse(
    localStorage.getItem(ConfigService.Config.Auth.userAccessKey)
  );
  if (_localuser && _localuser.accessToken) {
    return <IAuthInfo>_localuser;
  }
  return null;
}
```

Then we are going to tidy up the other methods to use these. We need a way to `SaveSession`, and `UpdateSession`. We also are going to write the logic for `CheckAuth`. Let's first go back to the `IAuthInfo` and talk about the `expiresAt` property,

## Expires At

When the information comes back from the authorization server, it usually has a lifetime, rather than an exact date. This is easier to manage given different time zones on server and client. In the client, however, we need to determine the exact time it expires at. A pretty close one, and good enough for our use.

So the expected return model is:

```ts
// return from server upon login
{
	accessToken: 'access_token',
	refreshToken: "refres_token",
	payload: {
		name: 'maybe name',
		id: 'id',
		email: 'username'
	},
	// expires in is an absolute lifetime in seconds
	expiresIn: 3600
}
```

In our model, it's time to properly map to our internal model

```ts
// in auth.model we need to properly map the expires at
export const NewAuthInfo = (data: any): IAuthInfo => {
  return {
    payload: {
      email: data.payload.email,
      name: data.payload.name,
      id: data.payload.id,
    },
    accessToken: data.accessToken,
    refreshToken: data.refreshToken,
    // map expiresIn value to exact time stamp
    expiresAt: Date.now() + data.expiresIn * 1000,
  };
};
```

And now in our `Login` in `AuthService` we properly map

```ts
// services/auth.service
Login(username: string, password: string): Observable<any> {
  return this.http.post(this._loginUrl, { username, password }).pipe(
    map((response) => {
      // use our mapper
      const retUser: IAuthInfo = NewAuthInfo((<any>response).data);
      // ...
    })
  );
}
```

Now checking the authentication whenever we need, is a simple extra layer of precaution. We already send API calls with null tokens, and let the server handle it. So it is no harm to remove the token from `localStorage` whenever the browser thinks it's invalid.

```ts
// services/auth.state write the CheckAuth
CheckAuth(user: IAuthInfo) {
	// if no user, or no accessToken, something terrible must have happened
	if (!user || !user.accessToken) {
	  return false;
	}
	// if now is larger than expiresAt, it expired
	if (Date.now() > user.expiresAt) {
	  return false;
	}

	return true;
}
```

## Saving and updating session

The client-side simple solution is already in place, I call it session even though it is not really a session. This understanding will help us later figure out what to do when we implement SSR.

```ts
// services/auth.state service
// add two methods: SaveSession and UpdateSession
// new saveSessions method
SaveSession(user: IAuthInfo): IAuthInfo | null {
  if (user.accessToken) {
    this._SaveUser(user);
    this.SetState(user);
    return user;
  } else {
    // remove token from user
    this._RemoveUser();
    this.RemoveState();
    return null;
  }
}

UpdateSession(user: IAuthInfo) {
    const _localuser: IAuthInfo = this._GetUser();
    if (_localuser) {
      // only set accesstoken and refreshtoken
      _localuser.accessToken = user.accessToken;
      _localuser.refreshToken = user.refreshToken;

      this._SaveUser(_localuser);
      // this is a new function to clone and update current value
      // we will move these into their own state class later
      this.UpdateState(user);
    } else {
      // remove token from user
      this._RemoveUser();
      this.RemoveState();
    }
  }
```

Notice how the `UpdateSession` is a tad bit different. After a `RefreshToken` request, we do not need much information from the server, and some servers do not return the payload with it. So it is a good practice to read only the new tokens. To use those two methods:

```ts
// services/auth.service
// login method
Login(username: string, password: string): Observable<any> {
  return this.http.post(this._loginUrl, { username, password }).pipe(
    map((response) => {
      // ... return after savi
      return this.authState.SaveSession(retUser);
    })
  );
}

RefreshToken(): Observable<boolean> {
  return (
    this.http
      // FIX: get refresh token, not token
      .post(this._refreshUrl, { token: this.authState.GetRefreshToken() })
      .pipe(
        map((response) => {

          if (!response) {
            throw new Error('Oh oh');
          }

          // map first, then update session
          const retUser: IAuthInfo = NewAuthInfo((<any>response).data);
          this.authState.UpdateSession(retUser);

          return true;
        })
      )
  );
}
```

We can also make use of our new private methods in the constructor of `AuthState` and `Logout`

```ts
// services/auth.state
constructor(private router: Router) {
	  // use our new _GetUser
    const _localuser: IAuthInfo = this._GetUser();

    if (this.CheckAuth(_localuser)) {
      this.SetState(_localuser);
    } else {
      this.Logout(false);
    }
  }
// ...
Logout(reroute: boolean = false) {
  // use our new _RemoveUser
  this._RemoveUser();
	//...
}
```

Now we're ready for a redirect URL. Let this all sink in first, we'll do that next episode. 😴

### RELATED POSTS

[Catching and displaying UI errors with toast messages in Angular](https://garage.sekrab.com/posts/catching-and-displaying-ui-errors-with-toast-messages-in-angular)

{% embed https://garage.sekrab.com/posts/authentication-in-angular-part-iii %}