Authentication in Angular: Part IV: Redirect feature and Account extra service
Tags: webdev, angular, tutorial, auth
Published: 2023-02-21 18:49:05

Picking up as we move on, there are a couple more features we want to implement to close the circle. Today we will talk about redirect URLs. Then let's rant about one of the recurring scenarios in SPA authenticated apps.

And of course, the [StackBlitz project](https://stackblitz.com/edit/auth-angular),

Redirect back to where user came from
----------------------------------------------------------------------------------------------------------------------------------------------------------------------

A better user experience is to return the user to their last route before they were expelled for the lack of authentication. It is a simple property that keeps track of the route, in the `AuthGuard` itself.

### Sub ranting about where to place that property

There is no silver bullet. Initially it looks like a property of the `IAuthInfo` model, since it will be treated the same way, but it isn't. This is a strictly client side property.

We might also think that the private methods that deal with `localStorage` in `AuthState` service, should live in their own service, and we might add this new property to it. That would make sense, **but it is an overkill**.

Another overkill is to treat this property as part of a state. The places it is set and retrieved are already clear calls, and live around other state managed elements. This should be straightforward.

I choose a simple solution. This property needs only a getter and a setter, to save in `localStorage` and retrieve from it. And it does that in combination with `AuthState`. So I am going to add a public getter and a public setter for the `redirectUrl` in `AuthState`.

```ts
// services/auth.state service
// update to create a new property getter and setter
get redirectUrl(): string {
  return localStorage.getItem('redirectUrl');
}
set redirectUrl(value: string) {
  localStorage.setItem('redirectUrl', value);
}
```

Save URL snapshot
------------------------------------------------------------------------------------------------------------------------------

The obvious location to save a redirect URL is in the `AuthGuard` since it knows the route the user is trying to access last.

```ts
// services/auth.guard
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
  // save snapshot url
  this.authState.redirectUrl = state.url;
  return this.secure(route);
}

canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
  this.authState.redirectUrl = state.url;
  return this.secure(route);
}
```

### CanMatch (CanLoad) URL segments

If we implement `CanMatch` (or the deprecated `CanLoad`) method, there is no `RouterStateSnapshot` provided, rather `UrlSegments[]`. To build the path from it, we reduce:

```ts
// canMatch implementation if needed
canMatch(route: Route, segments: UrlSegment[]):  Observable<boolean> {
  // create the current route from segments	
  const fullPath = segments.reduce((path, currentSegment) => {
     return `${path}/${currentSegment.path}`;
  }, '');

  this.authState.redirectUrl = fullPath;

  return this.secure(route);
}
```

### Redirect

To use it, there are a couple of places that come to mind: immediately after login, and in `loginResolve`.

```ts
// components/public/login
// inject authState
this.authService.Login(_user.username, _user.password)
  .subscribe(
  {
  	next: result => {
  		// placing urls in configuration is a good practice, leaving it to you
  	  this.router.navigateByUrl(this.authState.redirectUrl || '/private/dashboard');
  	}
  }
);

// in services/login.resolve
// LoginResolve class
if (user) {
  this.router.navigateByUrl(this.authState.redirectUrl || '/private/dashboard');
}
```

There aren't enough routes in our StackBlitz project, you're gonna have to take my word for it. Notice that when a user logs out intentionally by clicking on the logout button, you should choose how you want to treat that depending on your project requirements. Some projects you want to maintain the redirect URL. But in most cases, you want the user to be relogged into their dashboard. So after logout click, let's remove the `redirectUrl`

```ts
// app.comonent
export class AppComponent {
	Logout() {
	  //...
	  // also remove redirect
    this.authState.redirectUrl = null;
  }
}
```

Account service extra details
------------------------------------------------------------------------------------------------------------------------------------------------------

A recurring scenario in apps that connect to authentication servers, is the need to call the local server for extra account details. The `AccountService` is a separate service that is built on top of the `AuthService` with the purpose of getting extra details about the user from our server (as opposed to the authentication server). Depending on the type of project we're serving, there are a lot of different solutions. The one key element that I keep reminding myself of is:

> In any of the injected services in the Http interceptor, no Http requests can be made in the constructor.

I am not going to dive into the different types of projects, but requirements are generally around one of the following:

### 1\. Extra details can be saved

If extra information is of user details, like name, position, and title, it can be saved in `localStorage`. This is treated like the `AuthState`. What I would do is enrich the `payload` property of the `IAuthState`, then pipe the `Login` request to a second request to add the missing information:

```ts
// in components/public/login
this.authService.Login(_user.username, _user.password).pipe(
  // pipe a second call to get user extra account details
  switchMap(result => this.accountService.GetAccount(result)),
  catchError(error => this.toast.HandleUiError(error)),
) // ...
```

Then update `payload` and save the session as we normally do:

```ts
// hypothetical account.service
GetAccount(auth: IAuthInfo): Observable<IAccount> {
  //...
	return this.http.get('/account/user').pipe(
	    map(response => {
	      // map to a new model
	      const resUser: IAccount = Account.NewInstance(<any>response);
	      // assign the payload, you might want to be picky about what to assign
	      const _auth = {...auth, payload: resUser};
        // then save session again
	      this.authState.SaveSession(_auth);
	      return resUser;
	    })
	);
}
```

### 2\. Extra details can sit stale for the duration of the session

Information you want to notify the user about, but you do not want to bother them about changes. A simple example is whether this user is a new visitor across all devices.

In such cases, we want to get the information when the user logs in, and try to get it every time the user launches the application. **Not too early (wait for authentication), and not too late (before other modules and routing occurs)**. But we only need to try once. That can be accomplished in application **root**, after checking `AuthState`: when it becomes available, we make the call and set Account state. This is by far the most logical and safest way.

```ts
// account.service

// with one property, whether user is new across devices
export interface IAccount {
  id: string;
  newUser?: boolean;
}

@Injectable({ providedIn: 'root' })
export class AccountService {
  constructor(private _http: HttpClient) {}

  GetAccount(): Observable<IAccount> {
    return this._http.get('/account').pipe(
      map((response) => {
        return Account.NewInstance(response);
      })
    );
  }
  // also a SaveAccount to set newUser flag to false
  // would leave that to you
}
```

I am assuming by now you know how to set your own [RxJS based state](https://garage.sekrab.com/posts/rxjs-based-angular-state-management), and I will not dive into it too much. Here is what the application root would look like

```ts
// app.component
export class AppComponent {
  constructor(
    private authState: AuthState,
    private accountService: AccountService
  ) {
  // in here, tap into authstate to watch when it becomes available,
  // and request more info from account
  // do it just once throughout a session
  this.authState.stateItem$
    .pipe(
      first((state) => state !== null),
      switchMap((state) => this.accountService.GetAccount())
    )
    .subscribe({
      next: (response) => {
        console.log('received', response);
        // here you should set account state if you have it
        // then you can use it elsewhere
        // this.accountState.SetState(response);
      },
    });
}
```

Testing. Routing. Logging out. And in again. Refreshing. I can see that the account service is called once per session, and immediately after login. When use logs out however, the state is not invalidated. Now the choice is yours. You can keep it like that for non sensitive information (there is nothing saved in `localStorage`, it's just a JS state object). Or you can `filter` instead of `first` in application root, to make sure it gets called every time the `AuthState` changes.

Authorization: user roles in Auth guards
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Let's fix another use case where the auth guard is dependent on user role. To make this easy we need to let this all sink in, and wait till next episode. 😴

Thank you for reading this far, I hope you learned something today. Anything?

### RELATED POSTS

[RxJS based state management in Angular - Part I](https://garage.sekrab.com/posts/rxjs-based-angular-state-management)