How to handle case where user is still authenticated they can skip login


#1

I have a mobile & web app with JWT authentication. The token expires every hour and is refreshed and has a total life of 10 days, so after 10 days they have to log back in. Now where I’m stuck is how to handle persisting the login so when the user comes back to the app they can skip the login screen if they are authenticated. I would also like to trigger this method when I have an error in some of my HTTP requests so user is taken back to the login page.

Here is some pseudo code I wrote

 // Check if token is null
      // Return false & pop back to root
 // Check if token is expired
    // Attempt to refresh
      // If refresh successful continue to page 1
     // Else return false & pop back to root
 // Anything else: Pop back to root

I was thinking of maybe also creating a status which is saved to storage which is either ‘loggedIn’ or null and checking that along with checking if the token is null. Not sure if its needed though.

If anybody has an examples on a good way to implement this or a smarter way that would be greatly appreciated.

Also would it be a good idea to check on every page to make sure the user is still authenticated?


#2

I don’t really like complex interactions with the navigation system, so I avoid all of that with an AuthService that exposes an authNotifier property that is a ReplaySubject<boolean> with stack depth 1. In its constructor, you could do this:

constructor(storage: Storage) {
  storage.ready().then(() => storage.get('jwt'))
  .then((jwt) => {
    this.authNotifier.next(isValid(jwt));
  });
}

In the app component, you can do this:

constructor(auth: AuthService) {
  auth.authNotifier.subscribe((authed) => {
    if (authed) {
      this.rootPage = DashboardPage;
    } else {
      this.rootPage = LoginPage;
    }
  });
}

An additional benefit of this approach is that it makes logout dead easy: inject AuthService from anywhere and call next(false) on its authNotifier. No navigation interaction needed. Wherever you are checking and noticing that the token has expired can do this as well.


Redirect to login page if response status 401
Best way to handle authenticated areas in app?
Showing tabs after authentication
New to Ionic, any advice for authentication with a rest back-end? (Solved)
Trying to use a component in two pages with lazy loading
setRoot for app.Component from other page ( ionic V2)
Can i creat a login function like this.below my code
How to set the root nav the right way?
Add an "entry" page without the Tabs of the template
Catch nav guards during tab navigation
Handling @ng-idle globally for all pages
Import of @types/q not working in my code
Conditional rootPage
This.app is undefined
#3

Wow thanks that makes a lot of sense and I can definitely make something like that work for me. That example has already got me thinking.

Any advice on how I could/should handle this part: Token cant get refreshed because its time to live is up. When this happens (or actually whenever my refresh function returns an error) I want to display a login form (as a pop up/modal) so a user can continue from where they were in the app.

For example In my app users search for products, add to cart, and each item they add to cart is saved in storage. How could I save the state so that if in the middle of a search if token cannot be refreshed a login form pops up and after successful login in they can continue from that page?


#4

If you pop a modal, once the modal is dismissed, I would expect the page to be in the state it was before, so you shouldn’t have to do anything. I wouldn’t consider this an actual logout, so the authNotifier wouldn’t fire false/true, but just stay true assuming the login modal succeeded.


#5

please to be very frank with you i am quite new in ionic 2, please who is this.authNotifier?


#6

It’s described at the beginning of that post. It is declared in an injectable service provider and injected in the app component and anywhere else that needs to interact with it.


#7

Just to expand upon @rapropos excellent idea, below is a more fleshed out version. It took me quite a bit of experimenting to get something that worked right, but here it is:

AuthService (the important parts)


@Injectable()
export class AuthService {
  // Observable to send messages to when the user is no long auth'd
  // BehaviorSubject is like a ReplaySubject with a stack depth of 1
  authNotifier: BehaviorSubject<boolean> = new BehaviorSubject(null); 
  
// Same constructor from @rapropos
constructor(storage: Storage) {
  storage.ready().then(() => storage.get('jwt'))
  .then((jwt) => {
    this.authNotifier.next(isValid(jwt)); // will return true or false
  });
} 

...
...

I had a few issues trying to let the AuthService listen (subscribe) to the authNotifier, so I moved that core logic into the master app.component.ts

...

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  @ViewChild(Nav) nav: Nav;
  // you can use the default Ionic constructor with the authService added in
  constructor(private auth: AuthService) {
    // nothing special here
  }

  // Master Auth Listener
  // After the app has loaded and almost everything else has loaded
  ngAfterContentInit() {
    console.log("APP started");
    // Listen to authNotifier
    this.auth.authNotifier
      // filter on null so our app will wait for a real response
      .filter(res => res !== null)
      .subscribe(status => {
        console.log("APP AuthNotifier said: ",status);
        if(!status){ // when not auth'd
          console.log("APP Logging out!");
          this.auth.logout().subscribe(res => { // logout and then redirect to login
            console.log("Logged out.",res);
            this.nav.setRoot('LoginPage');
          });
        }
      });
  }

...

Note: Using Ionic 3 and lazy loading pages


#9

Thank you! Another way to do this is,

// add this line in the import section at the top of your file
import 'rxjs/add/operator/distinctUntilChanged';

this.auth.authNotifier
    .distinctUntilChanged() // this removes duplicated response
    .subscribe((status) => {
        // do a === true/false to ignore JavaScript falsey values
        if(status === false) {
            this.nav.setRoot('LoginPage');
        } else if (status === true) {
            this.nav.setRoot('HomePage');
        }
    });

#10

Could you please expand on this implementation? In your AuthService I’m getting 'Cannot find name isValid' so I think I’m missing that function. I’m also getting the error 'Property 'filter' does not exist on type 'BehaviorSubject<boolean>'

Also how would I go about ensuring the user is logged in and authenticated before each page is loaded? - Redirecting to the login page if they are not.

Thanks!