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?
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();
}
}
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.
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
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
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));
}
}
}
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.
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.
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.
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;
}