Learn how to make authentication in your Angular applications simpler and more consistent
Auth0! Parse.com! Firebase!
They are just a few of the so many cool services and libraries that are available today to easily authenticate users in your Angular application.
Unfortunately such a rich set of options also come with an equally diverse set of API's:
<!-- auth0 -->
<h1>Welcome {{ profile.first_name }}</h1>
<!-- parse.com -->
<h1>Welcome {{ currentUser.get('username') }}</h1>
<!-- firebase -->
<h1>Welcome user {{ authData.uid }}</h1>
<!-- angular-client-side-auth -->
<h1>Welcome {{ user.username }}</h1>
What if your project suddenly requires a different authentication strategy just before being released? Surely you don't want to change all your scripts and view templates the day before a release?
And what about maintenance? How can you conveniently support and maintain multiple projects if you have all kinds of API's to support?
In this article I share some of the best practices and tactics I came up with over the years to make authentication in Angular applications simpler and more consistent.
By the end of the article you will know exactly how you can create your own meaningful API to consistently:
- access session details in a view
- control access in your entire application
- show/hide content when a user is not logged in
- restrict route or state access for non authorized users
- pass credentials to a RESTful service when using ngResource
in different applications, even if they use different authentication libraries.
So let's get started!
The goal
Before we dive into the code, let's think of what we are actually trying to accomplish:
- On one hand we want to be able to deal with authentication such as logging in and out.
- On the other hand we want to be able to store session related data such as the user's first name, last name, or any information you receive from the authentication service you are dealing with.
Both are related but have different concerns.
The former is concerned with the actual logic while the latter is concerned with storing and persisting data so we can refresh the page and things keep working.
Our goal is to create a consistent API for both authentication and session so we can easily recognize intentions in scripts and views without knowing the exact underlying authentication logic.
A demo backend authentication service
For the sake of demonstration, let us assume that we have a backend authentication service that exposes the following endpoints:
POST /login
Log in user. Upon successful login, it returns:
HTTP 200
{
"user": {
"name": "John Doe"
"email": "john@doe.com"
},
"accessToken": "some-access-token"
}
GET /logout
Log out user. Upon successful login, it returns:
HTTP 200
The Angular side
To make sure we can conveniently access both the authentication logic and the session data in our entire application, we will create 2 separate Angular services.
Using services has multiple benefits:
- you can easily inject services anywhere in your application
- you can easily unit test services
- services are singletons so there is only one instance of them, which is perfect for what we need here
I like to name them auth
and session
to keep their names small and meaningful but you can name them anything you like.
The auth service
The auth
service lets us abstract the actual authentication logic of the authentication library we need to support in the project.
Here is an example of what an auth
service can look like for our demo backend authentication service (don't worry about session
yet, we'll get there shortly):
(function (angular) {
function AuthService($http, session){
/**
* Check whether the user is logged in
* @returns boolean
*/
this.isLoggedIn = function isLoggedIn(){
return session.getUser() !== null;
};
/**
* Log in
*
* @param credentials
* @returns {*|Promise}
*/
this.logIn = function(credentials){
return $http
.post('/api/login', credentials)
.then(function(response){
var data = response.data;
session.setUser(data.user);
session.setAccessToken(data.accessToken);
});
};
/**
* Log out
*
* @returns {*|Promise}
*/
this.logOut = function(){
return $http
.get('/api/logout')
.then(function(response){
// Destroy session in the browser
session.destroy();
});
};
}
// Inject dependencies
AuthService.$inject = ['$http', 'session'];
// Export
angular
.module('app')
.service('auth', AuthService);
})(angular);
In this case we are using $http
to call our backend service but that's not relevant.
The thing to remember here is that you create your own API:
auth.logIn()
auth.logOut()
auth.isLoggedIn()
that hides away the actual implementation logic.
If you decide to use another authentication library, you only have to rewrite the logic in these functions.
Here is an example when using Firebase authentication:
(function (angular) {
function AuthService($q, $firebaseAuth, session){
var ref = new Firebase('https://<your-url>.firebaseio.com/');
var authObj = $firebaseAuth(ref);
this.isLoggedIn = function isLoggedIn(){
return session.getAuthData() !== null;
};
this.logIn = function(){
return authObj
.$authWithOAuthPopup('github', {
scope: 'user'
})
.then(
function(authData){
session.setAuthData(authData);
return authData;
},
function(error){
$q.reject(error);
}
);
};
this.logOut = function(){
authObj.unauth();
session.destroy();
};
}
// Inject dependencies
AuthService.$inject = ['$q', '$firebaseAuth', 'session'];
// Export
angular.module('app')
.service('auth', AuthService);
})(angular);
It is perfectly fine to create additional methods depending on the authentication service you are dealing with.
For example, you could be dealing with roles and add a method auth.isAdmin()
that checks if a certain role is stored in the session
.
This is not a form of inconsistency between applications. The main goal for consistency here is that you always use auth to implement logic.
Note that both examples expose a similar API, while using completely different logic under the hood.
The session service
The session
service takes care of storing data that we receive from the authentication service after successfully logging in.
Here we provide a simple API to store the user
and accessToken
we receive from the authentication service.
I am a fan of creating getters and setters. If you use an IDE like webstorm it will provide you with autocompletion in your application. A second benefit is that you can create extra methods likesession.getFirstName()
,session.getFullName()
or whatever you feel you need in your application.
For our demo backend authentication service, we can use localStorage to persist the session during page reloads:
(function (angular) {
function sessionService($log, localStorage){
// Instantiate data when service
// is loaded
this._user = JSON.parse(localStorage.getItem('session.user'));
this._accessToken = JSON.parse(localStorage.getItem('session.accessToken'));
this.getUser = function(){
return this._user;
};
this.setUser = function(user){
this._user = user;
localStorage.setItem('session.user', JSON.stringify(user));
return this;
};
this.getAccessToken = function(){
return this._accessToken;
};
this.setAccessToken = function(token){
this._accessToken = token;
localStorage.setItem('session.accessToken', token);
return this;
};
/**
* Destroy session
*/
this.destroy = function destroy(){
this.setUser(null);
this.setAccessToken(null);
};
}
// Inject dependencies
sessionService.$inject = ['$log', 'localStorage'];
// Export
angular
.module('app')
.service('session', sessionService);
})(angular);
As you can see, the getters merely return the requested data while the setters actually store the data in localStorage
as well to make sure the data is persisted in case the browser window is closed.
The localStorage
service is a simple wrapper that provides access to the browser's native localStorage. It looks like this:
(function (angular) {
function localStorageServiceFactory($window){
if($window.localStorage){
return $window.localStorage;
}
throw new Error('Local storage support is needed');
}
// Inject dependencies
localStorageServiceFactory.$inject = ['$window'];
// Export
angular
.module('app')
.factory('localStorage', localStorageServiceFactory);
})(angular);
Depending on the data you receive from your authentication service, you may have different getters and setters in your session. That's okay and it's not a form of inconsistency across applications. The main goal for consistency here is that you always use the session to access session specific data.
Here is an example when using Firebase authentication:
(function (angular) {
function sessionService($log, localStorage){
this._authData = JSON.parse(localStorage.getItem('session.authData'));
this.getAuthData = function(){
return this._authData;
};
this.setAuthData = function(authData){
this._authData = authData;
localStorage.setItem('session.authData', JSON.stringify(authData));
return this;
};
this.getGitHubAccessToken = function(){
if(this._authData && this._authData.github && this._authData.github.accessToken){
return this._authData.github.accessToken;
}
return null;
};
/**
* Destroy session
*/
this.destroy = function destroy(){
this.setAuthData(null);
};
}
// Inject dependencies
sessionService.$inject = ['$log', 'localStorage'];
// Export
angular.module('app')
.service('session', sessionService);
})(angular);
Assign both services to $rootScope
I always assign both services to $rootScope
:
(function (angular) {
function assignServicesToRootScope($rootScope, auth, session){
$rootScope.auth = auth;
$rootScope.session = session;
}
// Inject dependencies
assignServicesToRootScope.$inject = ['$rootScope', 'auth', 'session'];
// Export
angular
.module('app')
.run(assignServicesToRootScope);
})(angular);
You may think: "Hey, it's not a good practice to assign data to $rootscope
!".
You're right, but here we are not assigning data. Both auth
and session
are application wide services so it makes perfect sense to assign them to $rootScope
in order to make them available in all our view templates.
It will make your life a whole lot easier and your code a whole lot simpler, as you'll see next.
Show/hide content when user is not logged in
Because we assigned both services to $rootScope
, we can now easily hide/show content in all our application view templates:
<!-- hide title if logged in -->
<h1 class="ng-cloak" ng-hide="auth.isLoggedIn()">
Hi stranger!
</h1>
<!-- show title if logged in -->
<h1 class="ng-cloak" ng-show="auth.isLoggedIn()">
<!-- show session data -->
Hi {{ session.getFullName() }}
</h1>
Notice how auth
is intuitively used for logic (ng-if
, ng-show
, etc) while session
is used for displaying data, which is exactly how we wanted things to behave when we defined our goal.
To reiterate: the key benefit is that you can now use your own custom API in your markup so it becomes consistent across all your applications, so next time you see something like:
<h1 ng-if="auth.isLoggedIn()">
Access denied!
</h1>
in one of your application view templates, you'll immediately know what it will do without having to worry about the exact authentication logic that was implemented behind the scenes.
If you're working with designers that edit view templates, they will also appreciate a consistent API when working together with you on different projects. They may even be able to reuse parts of view templates across projects because the markup will be a lot more consistent.
Important: keep in mind that even though you hide content in the view, the content can still be viewed in the page source. So if certain pieces of content are too sensitive to show up in the page source, you will have to make sure your server doesn't send them in the first place.
Check access in scripts
Because Angular allows you to inject services anywhere in your application, detecting authentication state becomes dead simple:
(function (angular) {
function SomeController(auth){
// Do something if logged in
if(auth.isLoggedIn()){
// ...
}
}
// Inject dependencies
SomeController.$inject = ['auth'];
// Export
angular
.module('app')
.controller('SomeController', SomeController);
})(angular);
Control access during application bootstrap
Some applications depend on third party access tokens and only allow access to users that arrive with a valid token.
To prevent the entire application interface from loading, you can control access during the Angular bootstrap process using the run()
method of your application module:
(function (angular) {
function checkAccessDuringApplicationBootstrap($window, auth){
if(auth.isLoggedIn()){
return;
}
// Redirect to third party login page
$window.location = '...';
// Make sure bootstrap process is stopped
throw new Error('Access denied');
}
// Inject dependencies
checkAccessDuringApplicationBootstrap.$inject = ['$window', 'auth'];
// Export
angular
.module('app')
.run(checkAccessDuringApplicationBootstrap);
})(angular);
Control access during route or state changes
You may also want to control access whenever the route or state changes.
The event listener below contains code for both ngRoute and ui-router:
(function (angular) {
function checkAccessOnStateChange($rootScope, auth){
// Listen for location changes
// This happens before route or state changes
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl){
if(!auth.isLoggedIn()){
// Redirect to login
// Prevent location change
event.preventDefault();
}
});
// Listen for route changes when using ngRoute
$rootScope.$on('$routeChangeStart', function(event, nextRoute, currentRoute){
// Here we simply check if logged in but you can
// implement more complex logic that inspects the
// route to see if access is allowed or not
if(!auth.isLoggedIn()){
// Redirect to login
// Prevent state change
event.preventDefault();
}
});
// Listen for state changes when using ui-router
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
// Here we simply check if logged in but you can
// implement more complex logic that inspects the
// state to see if access is allowed or not
if(!auth.isLoggedIn()){
// Redirect to login
// Prevent state change
event.preventDefault();
}
});
}
// Inject dependencies
checkAccessOnStateChange.$inject = ['$rootScope', 'auth'];
// Export
angular
.module('app')
.run(checkAccessOnStateChange);
})(angular);
As noted in the comments, you can create your own custom access control logic where you inspect routes or states before allowing access or not.
Pass credentials to a RESTful service when using ngResource
If you are using Angular's ngResource module to communicate with a RESTful backend and you need to pass credentials, you can easily add session details to the HTTP requests that ngResource makes.
In this example we pass the accessToken
in the headers in every HTTP request that ngResource makes:
(function (angular) {
function BooksResourceFactory($resource, session){
var url = '/books/:_id';
// Pass session token as Authorization header
var headers = {
'Authorization': 'Bearer ' + session.getAccessToken()
};
// Assemble actions with custom headers attached
var actions = {
'get' : {method: 'GET', headers: headers},
'save' : {method: 'POST', headers: headers},
'create': {method: 'POST', headers: headers},
'query' : {method: 'GET', isArray: true, headers: headers},
'remove': {method: 'DELETE', headers: headers},
'delete': {method: 'DELETE', headers: headers},
'update': {method: 'PUT', headers: headers}
};
var Books = $resource(url, {_id: '@_id'}, actions);
return Books;
}
BooksResourceFactory.$inject = ['$resource', 'session'];
angular.module('app')
.factory('BooksResource', BooksResourceFactory);
})(angular);
If your application has a header property value that can change at runtime, you can assign a function instead of a static value so the value is generated dynamically at runtime.
For example, to let AngularJS dynamically evaluate the Authorization
header value every time a resource action is called, you would replace:
// Pass session token as Authorization header
// Value is evaluated only once when resource is instantiated
var headers = {
'Authorization': 'Bearer ' + session.getAccessToken()
};
with:
// Pass session token as Authorization header
// and re-evaluate the value every time an action is called
var headers = {
'Authorization': function(){
return 'Bearer ' + session.getAccessToken();
}
};
Conclusion
The code snippets in this article just scratch the surface of what you can do with auth
and session
.
By abstracting your:
- authentication logic in an
auth
service - session data in a
session
service
you can create your own consistent API that you can use in all your applications.
An API that makes sense to you and your team and hides away the underlying authentication logic of the application.
I could have saved myself a lot of time, money and refactoring over the years if only some kind of blueprint was available at the time.
So hopefully this article can be for you what I needed back then.
Have a great one!