Enforcing login at app startup


#1

Hi,
I have an app based on the sidemenu example.
I listen to $stateChangeStart to listen for state change and enforce login.
It works good beside the application start phase.
When I open the app the default route: app.services is selected but $stateChangeStart doesn’t fire and so the view is shown even if the user is not logged in.
Is it normal that $stateChangeStart dosen’t fire at startup? Is there any other event to listen to at startup?

I’ using Ionic 1.0 rc4 and down here my code in app.js.

Thank you for any advice!

app.js:

    angular.module('starter', ['ionic', 'starter.controllers'])

.run(function($ionicPlatform, $rootScope, $state, AuthService) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if (window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if (window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }

    $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){
      console.log("fires at startup")
      if (toState.authenticate && !AuthService.isAuthenticated()){
        // User isn’t authenticated
        $state.go("app.login");
        event.preventDefault(); 
      }
    });

  });
})



.config(function($stateProvider, $urlRouterProvider, $ionicConfigProvider, $resourceProvider, $httpProvider) {
  $ionicConfigProvider.backButton.text('').icon('ion-ios7-arrow-left');

  $resourceProvider.defaults.stripTrailingSlashes = false;


  $stateProvider

  .state('app', {
    url: "/app",
    abstract: true,
    templateUrl: "templates/menu.html",
    controller: 'AppCtrl',
    authenticate: true
  })

  .state('app.login', {
    url: "/login",
    views: {
      'menuContent': {
        templateUrl: "templates/login.html"
      }
    },
    authenticate: false
  })

  .state('app.services', {
    cache: false,
    url: "/services",
    views: {
      'menuContent': {
        templateUrl: "templates/services.html",
        controller: 'ServicesCtrl'
      }
    },
    authenticate: true
  })

  .state('app.service', {
    cache: false,
    url: "/services/:serviceId",
    views: {
      'menuContent': {
        templateUrl: "templates/service.html",
        controller: 'ServiceCtrl'
      }
    },
    authenticate: true
  })

  .state('app.issue', {
    cache: false,
    url: "/issue/:issueId",
    views: {
      'menuContent': {
        templateUrl: "templates/issue.html",
        controller: 'IssueCtrl'
      }
    },
    authenticate: true
  })

  .state('app.update', {
    cache: false,
    url: "/update/:updateId",
    views: {
      'menuContent': {
        templateUrl: "templates/update.html",
        controller: 'UpdateCtrl'
      }
    },
    authenticate: true
  })

  .state('app.updateadd', {
    url: "/issue/:issueId/updateadd",
    views: {
      'menuContent': {
        templateUrl: "templates/updateadd.html",
        controller: 'UpdateAddCtrl'
      }
    },
    authenticate: true
  })

  .state('app.updateedit', {
    url: "/update/:updateId/updateedit",
    views: {
      'menuContent': {
        templateUrl: "templates/updateedit.html",
        controller: 'UpdateEditCtrl'
      }
    },
    authenticate: true
  })

  .state('app.issueadd', {
    url: "/service/:serviceId/issueadd",
    views: {
      'menuContent': {
        templateUrl: "templates/issueadd.html",
        controller: 'IssueAddCtrl'
      }
    },
    authenticate: true
  })

  .state('app.issueedit', {
    url: "/issue/:issueId/issueedit",
    views: {
      'menuContent': {
        templateUrl: "templates/issueedit.html",
        controller: 'IssueEditCtrl'
      }
    },
    authenticate: true
  });

  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/app/services');

});

#2

Replying to myself for an update.
I solved the problem but in what I feel it’s not the most elegant or correct way.

The problem actually wasn’t the event $stateChangeStart not being fired at startup.

The problem is that it’s fired multiple times but only before the $ionicPlatform.ready function is actually ready.
From my experience when the $ionicPlatform.ready function fires the default route ($urlRouterProvider.otherwise(’/app/services’) ) has already being shown and authentication is not enforced.

As a workaround I put the “$rootScope.$on(”$stateChangeStart"…" block also immediately after the run command and outside the $ionicPlatform.ready call.
This causes a strange behaviour in the app that logs some error immediately after starting and seems to enter a loop in which the $stateChangeStart fires multiple times.
However the loop soon exits and the behaviour shown is exactly what I wanted in the beginning: the default route is never shown and the app redirects to the login screen.

Please if someone knows a more elegant or correct way to do this, don’t esitate to comment.
I’m still thinking that it’s strange that before the $ionicPlatform.ready fires the default controllers and view are already invoked.
If someone cares to clarify this behaviour I will be grateful.

Thanks


#3

It sounds like that the login page isn’t your main view. What I have found works best is making the login page the default view no matter what, and then if they are not logged in or when the app comes to the foreground you just push them back to the login state.

If you could elaborate more on how your states/views are set up we might be able to help you a little more


#4

Here are some of my authentication code that I found by doing a lot of research…

.state('app.redis', {
url: "/redis/",
cache: true,
views: {
  "menuContent": {
    controller: 'ctrlRedis as vm',
    templateUrl: "app/redis/redis.html",
    resolve: { authenticated: authenticated }
  }
}

In the .config, I use this…

  // Authentication code
  var authenticated = ['$q', 'Azureservice', function ($q, Azureservice) {
    var deferred = $q.defer();
   
    if (Azureservice.isLoggedIn()) {
      deferred.resolve();
    } else {
      deferred.reject();
    }
    return deferred.promise;
  }];

Azure service is the library that I use to do my authentication check. I don’t have to worry about any pages that uses the resolve: {authenticated…

And I also default back to login page


#5

Sorry for getting back to you so late. I missed it.
You are right, the login page is not my default view.
I made this choice because I wanted to have the experience for an already logged in user as smooth as possible.
When I tried to have the login page as the main view (login is a modal) you could alway see a fast transition (kinda ugly) when opening the app.

Here is how I structured my views:

  $stateProvider

  .state('app', {
    url: "/app",
    abstract: true,
    templateUrl: "templates/menu.html",
    controller: 'AppCtrl',
    resolve: {
      loggedIn: function (AuthService, $ionicPlatform) {
        $ionicPlatform.ready(function() {
          var loggedIn = AuthService.isAuthenticated();
          return loggedIn;
        });
      }
    },
    authenticate: true
  })

  .state('app.login', {
    url: "/login",
    views: {
      'menuContent': {
        templateUrl: "templates/login.html"
      }
    },
    authenticate: false
  })

  .state('app.configuration', {
    url: "/configuration",
    views: {
      'menuContent': {
        templateUrl: "templates/configuration.html",
        controller: 'ConfigurationCtrl'
      }
    },
    resolve: {
      locationsData: function(Locations) {
          return Locations.query().$promise;
        },
      servicesData: function(ServiceAll) {
          return ServiceAll.query().$promise;
          },
      confData: function(Configuration) {
          return Configuration.get().$promise;
      }
      },
    authenticate: true
  })

  .state('app.services', {
    cache: false,
    url: "/services",
    views: {
      'menuContent': {
        templateUrl: "templates/services.html",
        controller: 'ServicesCtrl'
      }
    },
    authenticate: true
  })

  .state('app.service', {
    cache: false,
    url: "/services/:serviceId",
    views: {
      'menuContent': {
        templateUrl: "templates/service.html",
        controller: 'ServiceCtrl'
      }
    },
    authenticate: true
  })

  .state('app.issue', {
    cache: false,
    url: "/issue/:issueId",
    views: {
      'menuContent': {
        templateUrl: "templates/issue.html",
        controller: 'IssueCtrl'
      }
    },
    authenticate: true
  })

  .state('app.update', {
    cache: false,
    url: "/update/:updateId",
    views: {
      'menuContent': {
        templateUrl: "templates/update.html",
        controller: 'UpdateCtrl'
      }
    },
    authenticate: true
  })

  .state('app.updateadd', {
    url: "/issue/:issueId/updateadd",
    views: {
      'menuContent': {
        templateUrl: "templates/updateadd.html",
        controller: 'UpdateAddCtrl'
      }
    },
    authenticate: true
  })

  .state('app.updateedit', {
    url: "/update/:updateId/updateedit",
    views: {
      'menuContent': {
        templateUrl: "templates/updateedit.html",
        controller: 'UpdateEditCtrl'
      }
    },
    authenticate: true
  })

  .state('app.issueadd', {
    url: "/service/:serviceId/issueadd",
    views: {
      'menuContent': {
        templateUrl: "templates/issueadd.html",
        controller: 'IssueAddCtrl'
      }
    },
    authenticate: true
  })

  .state('app.issueedit', {
    url: "/issue/:issueId/issueedit",
    views: {
      'menuContent': {
        templateUrl: "templates/issueedit.html",
        controller: 'IssueEditCtrl'
      }
    },
    authenticate: true
  });

  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/app/services');

#6

Thanx a lot for sharing your code.
I’ll try to define my auth variable as a promise too.
I’ll let you know how it goes.

bye


#7

Let me ask you one question.
If you default to the login page and the user is already logged in, how you transition to the logged in state smoothly?
Yo do it in resolve or in the controller?

Thank you


#8

I’m not really understanding the question. Can you be more specific?

The config code is ran every time there is a route change to check for pages that have the resolve authenticated code (in my example, the redis page). When the config code runs, Azureservice.isLoggedIn() is ran to check if you are logged in or not. If you are not logged in, then the default page is shown which in my case is the login page. So there’s nothing I have to do in the controller. It’s all in the app.js and it’s simple as adding the resolve code to any url states.


#9

The problem I see is the following.
If my user keeps is logged in state even if the app gets closed (that would be my case), once the user restart the app, he gets the default page (login page) but being already logged in, a sort of automatic redirect before even showing the login dialog should happen right?
The details of the implementation of this redirect are what I am unsure about.
Thanx a lot for answering.


#10

Yes, the automatic redirect should happen as long as the login has not expired.


#11

Is the config an okay place to put some startup code? For example, I want to make sure the user is logged in and phone has some sort of internet connection. Then depending on those outcomes, I might change the default state/page that the app is directed to. I’m just not sure if there are negatives to that, or if it’s bad practice.