DEV Community

Cover image for Authentication in Angular: Part IV: Redirect feature and Account extra service
Ayyash
Ayyash

Posted on • Originally published at garage.sekrab.com

Authentication in Angular: Part IV: Redirect feature and Account extra service

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,

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.

// 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);
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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);
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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);
}
Enter fullscreen mode Exit fullscreen mode

Redirect

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

// 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');
}
Enter fullscreen mode Exit fullscreen mode

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

// app.comonent
export class AppComponent {
    Logout() {
      //...
      // also remove redirect
    this.authState.redirectUrl = null;
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

// 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)),
) // ...
Enter fullscreen mode Exit fullscreen mode

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

// 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;
        })
    );
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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
}
Enter fullscreen mode Exit fullscreen mode

I am assuming by now you know how to set your own RxJS based state, and I will not dive into it too much. Here is what the application root would look like

// 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);
      },
    });
}
Enter fullscreen mode Exit fullscreen mode

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

Latest comments (0)