Return to route after login using navigation instruction

I’m trying to set up a failed-auth post-login redirect.

My plan is (was) that when authentication fails, the intended (i.e. requiring auth) navigation instruction is saved, and next time the user turns up logged in, then it would run the saved navigation instruction.
However I can’t find any method that allows me to either replace the navigation instruction with a saved one, or just ‘run’ the instruction.

Is this an appropriate way to approach this problem in Aurelia? If not, what would be recommended?

I did find https://stackoverflow.com/questions/42591682/how-to-return-to-view-after-authentication-in-aurelia but that didn’t help me with the query string (altho a small amount of url manipulation would solve that), but I was hoping it was possible to use the instruction directly.

Here is a cutdown version of the ts code:

export class NotAuthorizedRedirect {
    private navigationInstruction: NavigationInstruction;

    public notAuthorized(from: NavigationInstruction) {
        this.navigationInstruction = from;
        return new Redirect("login-route");
    }

    public getNavigationInstruction() {
        let instruction = this.navigationInstruction;
        this.navigationInstruction = null; // single use
        return instruction;
    }
}

@autoinject
class AuthorizeStep {
    constructor(private redirect: NotAuthorizedRedirect) { }

    run(navigationInstruction, next) {

        let isLoggedIn = authMethod(...);
        if (!isLoggedIn) {
            return next.cancel(this.redirect.notAuthorized(navigationInstruction));
  
        let preAuthNavigatonInstruction = this.redirect.getNavigationInstruction();
        if(preAuthNavigatonInstruction) {
            // do something with preAuthNavigatonInstruction that includes querystring parameters
        }

        next();
    }
}
1 Like

I’ve gone with the following in place of the ‘do something with preAuthNavigationInstruction’ comment, and so far so good. This uses the feature that params not listed in the route become appended in the query string.

let redirectUri = preAuthNavigatonInstruction.router.generate(
    preAuthNavigatonInstruction.config.name,
    Object.assign(preAuthNavigatonInstruction.params, preAuthNavigatonInstruction.queryParams),
    { replace: true });

return next.cancel(new Redirect(redirectUri));

Now I just want to add something to my login page to indicate a redirect is in progress and I’ll be happy :slight_smile:

2 Likes

Here is my solution:

  1. I add an Authorization step to the pipeline
  2. The authorization step checks if a login is required and if the user then is logged in
    2.1. The step saves the current url by taking instructions fragment and query
    2.2. A Redirect to the login page is created and returned which aborts the pipeline
  3. If no authorization is required or the user is logged in I check if a origin URL was saved
    3.1. Check If the current instruction fragment is not the login page and I have a origin
    3.2. Create a redirect to the origin and cancel the pipeline
  4. Done

Here’s are the relevant code parts of my AuthorizationStep
import {Redirect} from ‘aurelia-router’;

export class AuthorizationStep {

  static loginFragment = '/login';

  run(instruction, next) {
    return Promise.resolve()
      .then(() => this.checkAuthorization(instruction, next))
      .then(result => result || this.checkOrigin(instruction, next))
      .then(result => result || next());
  }

  checkAuthorization(instruction, next) {
    if (instruction.getAllInstructions().some(i => i.config.auth)) {
      if (!isLoggedIn()) {
        const currentUrl = instruction.fragment + (instruction.queryString ? `?${instruction.queryString}` : '');
        localStorage.setItem('origin', currentUrl);
        return next.cancel(new Redirect(AuthorizationStep.loginFragment));
      }
    }
  }

  checkOrigin(instruction, next) {
    const origin = localStorage.getItem('origin');
    // Check if we were not redirected to login page and have an origin
    if (instruction.fragment !== AuthorizationStep.loginFragment && origin) {
      localStorage.removeItem('origin');
      return next.cancel(new Redirect(origin));
    }
  }
}
4 Likes

Just wanted to add this for others that might run into the same issue I had.

This worked really well, but when using Auth0, or another authentication service that uses callbacks be sure to add that fragment to ignore along side the loginFragment or you end up getting multiple logins cycles and lose the origin page.

1 Like

Thanks for that snippet of code.

I just want to add my two-penny worth idea,
use sessionStorage instead of localStorage, for the case where multiple tabs are opened at once with different routes - each one will redirect to its own target after login.

1 Like

I got some inspiration from this article:

It comes down to using the setRoot method of the Aurelia instance where needed. By default, it is set to the app component during startup, but you can use it anywhere you want and specify any desired component, if you use dependency injection to get the Aurelia instance.

Any specified url remains intact during login. After login, Aurelia will automatically navigate to the desired page.

1 Like

I second this. Used that approach now for the second time and had very good experiences with it.

Essentially my workflow looks something along these lines:

  // main.ts
  aurelia.use.plugin("aurelia-store", {
    initialState
  });

  await aurelia.start();
  const auth = aurelia.container.get(AuthService);
  const user = await auth.getLoggedInUser();

  const store = aurelia.container.get(Store);
  store.registerAction("Set User", setUser);

  if (!user) {
    // go to login screen, while maintaining the original location in the address bar
    aurelia.setRoot("components/login");
  } else {
    await store.dispatch(setUser, user);
    // already logged in, keeping the address bar
    aurelia.setRoot("app");
  }

// login.ts
  public async login() {
    try {
      await this.auth.login(this.emailUsername, this.password);

      // redirect back to app while still keeping the location in the address bar intact
      this.aurelia.setRoot("./app");
    } catch (e) {
      // tslint:disable-next-line:no-console
      console.error(e);
    }

    return false;
  }
2 Likes