on
Italy
- Get link
- X
- Other Apps
Authentication for single page apps can be a tricky matter. In many
cases, SPA architecture involves having an isolated front-end
application with a framework like AngularJS, and a separate backend that
serves as a data API to feed the front-end. In these cases, traditional
session-based authentication that is done in most round-trip
applications falls short. Session-based authentication has a lot of
issues for this kind of architecture, but probably the biggest is that
it introduces state to the API, and one of the tenets of REST is that things remains stateless.
Another consideration is that if you ever want to use that same data
API as a backend for a mobile application, session-based authentication
won’t work.Authorization
header when we make HTTP requests. If the user has an invalid JWT or no
JWT at all, their request to access the protected resoures will be
denied, and they will get an error.your-domain.auth0.com and is used when configuring the Auth0 tools that we’ll see below.# Angular related modules
npm install angular angular-material angular-ui-router angular-aria angular-animate
# Auth0 related modules
npm install angular-jwt angular-storage auth0-angular
# To serve the app (if not already installed)
npm install -g http-server
Next, let’s set up our app.js and index.html
files to bootstrap the application. At this time we can let Angular
know which modules we need access to from the dependencies we installed.// app.js
(function() {
'use strict';
angular
.module('authApp', ['auth0', 'angular-storage', 'angular-jwt', 'ngMaterial', 'ui.router'])
.config(function($provide, authProvider, $urlRouterProvider, $stateProvider, $httpProvider, jwtInterceptorProvider) {
authProvider.init({
domain: 'YOUR_AUTH0_DOMAIN',
clientID: 'YOUR_AUTH0_CLIENT_ID'
});
});
})();
Here we’ve configured authProvider from auth0-angular with our credentials from the dashboard. Of course, you’ll want to replace the values in the sample with your own credentials.
Angular Auth
Notice here that we’re bringing in the Auth0Lock
widget script from Auth0’s CDN. This is the script that we’ll need to
show the login box that Auth0 provides. By setting the viewport like we
have, we make sure that the widget shows up properly on mobile devices.toolbar component. We’ll wrap this up into its own directive to keep things clean. // components/toolbar/toolbar.dir.js
(function() {
'use strict';
angular
.module('authApp')
.directive('toolbar', toolbar);
function toolbar() {
return {
templateUrl: 'components/toolbar/toolbar.tpl.html',
controller: toolbarController,
controllerAs: 'toolbar'
}
}
function toolbarController(auth, store, $location) {
var vm = this;
vm.login = login;
vm.logout = logout;
vm.auth = auth;
function login() {
// The auth service has a signin method that
// makes use of Auth0Lock. If authentication
// is successful, the user's profile and token
// are saved in local storage with the store service
auth.signin({}, function(profile, token) {
store.set('profile', profile);
store.set('token', token);
$location.path('/');
}, function(error) {
console.log(error);
})
}
function logout() {
// The signout method on the auth service
// sets isAuthenticated to false but we
// also need to remove the profile and
// token from local storage
auth.signout();
store.remove('profile');
store.remove('token');
$location.path('/');
}
}
})();
In the login method, auth.signin is
responsible for opening Auth0’s Lock widget. This is awesome–with a
single method, we have a fully-functioning login box! The callback gives
us access to the user’s profile and JWT on a successful login or
signup, and we need to store these in local storage for use later.auth.signout to set the user’s authentication state to false.
Note here that we’re using terms like “signin”, “signout”, and
“authentication” like we would typically think about them with
statefull, session-based authentication. However, the user isn’t really
“authenticated” in the traditional sense, and the fact that all we need
to do to log the user out is remove their token from local storage
illustrates this nicely.
Angular Auth
Login
Profile
Logout
Notice here that we are conditionally showing and hiding the three buttons based on the user’s isAuthenticated state.index.html file to see it at work. At the same time, we’ll drop in some other scripts that we’ll need later.
...
...
http-server and navigate to http://localhost:8080//profile
route that will take the user to their profile page and simply display
some of the data that is saved in local storage. We want this route to
be protected so that if the user isn’t authenticated, they aren’t able
to navigate to it. We’ll also set up a /home route so that the user is redirected to somewhere meaningful if they are logged out.home and profile components.
Welcome to the Angular Auth app!
Login from the toolbar above to access your profile.
We’ll also set up the profile view and controller with methods to make HTTP calls to the NodeJS server that we’ll set up later.
Get Message
Get Secret Message
{{ user.profile.nickname }}
{{ user.profile.email }}
{{ user.message }}
// components/profile/profile.ctr.js
(function() {
'use strict';
angular
.module('authApp')
.controller('profileController', profileController);
function profileController($http) {
var vm = this;
vm.getMessage = getMessage;
vm.getSecretMessage = getSecretMessage;
vm.message;
vm.profile = JSON.parse(localStorage.getItem('profile'));
// Makes a call to a public API route that
// does not require authentication. We can
// avoid sending the JWT as an Authorization
// header with skipAuthorization: true
function getMessage() {
$http.get('http://localhost:3001/api/public', {
skipAuthorization: true
}).then(function(response) {
vm.message = response.data.message;
});
}
// Makes a call to a private endpoint that does
// require authentication. The JWT is automatically
// sent with HTTP calls using jwtInterceptorProvider in app.js
function getSecretMessage() {
$http.get('http://localhost:3001/api/private').then(function(response) {
vm.message = response.data.message;
});
}
}
})();
The next thing we need to do is set up our routing. At the same time,
we’ll set some configuration that will automatically attach the JWT as
an Authorization header when making HTTP calls.// app.js
...
.config(function(...) {
...
$urlRouterProvider.otherwise("/home");
$stateProvider
.state('home', {
url: '/home',
templateUrl: 'components/home/home.tpl.html'
})
.state('profile', {
url: '/profile',
templateUrl: 'components/profile/profile.tpl.html',
controller: 'profileController as user'
});
jwtInterceptorProvider.tokenGetter = function(store) {
return store.get('token');
}
$httpProvider.interceptors.push('jwtInterceptor');
...
Here we’ve provided some routing configuration for $stateProvider and have defaulted to the home state when the profile state isn’t matched. The jwtInterceptorProvider is the HTTP interceptor that takes care of attaching the user’s JWT as an Authorization
header on each request. For any HTTP request that is made, Angular will
intercept it before it goes out and attach whatever is returned from
the tokenGetter function which, in this case, is the user’s JWT from local storage.server and then install some dependencies.mkdir server && cd server
npm init
npm install express express-jwt cors
// server/server.js
var express = require('express');
var app = express();
var jwt = require('express-jwt');
var cors = require('cors');
app.use(cors());
var authCheck = jwt({
secret: new Buffer('YOUR_AUTH0_CLIENT_SECRET', 'base64'),
audience: 'YOUR_AUTH0_CLIENT_ID'
});
app.get('/api/public', function(req, res) {
res.json({ message: "Hello from a public endpoint! You don't need to be authenticated to see this." });
});
app.get('/api/private', authCheck, function(req, res) {
res.json({ message: "Hello from a private endpoint! You DO need to be authenticated to see this." });
});
app.listen(3001);
console.log('Listening on http://localhost:3001');
secret, along with our Auth0 client ID as the audience.
We then just need to apply the middleware to whichever routes we want
to protect by passing it in as the second argument, just like we’ve done
for the private route here.node server.jsAuthorization header, and we get the result back from the server. We can also see how the JWT gets attached using the Bearer scheme if we inspect the request in dev tools.401 Unauthorized error will have been returned. What we need is some way to redirect the user if they become unauthenticated while at the /profile route. We can do that with another HTTP interceptor that looks for any 401 errors returned in responses, and redirects the user to the /home route if it finds any.// app.js
...
.config(function(...) {
...
function redirect($q, $injector, auth, store, $location) {
return {
responseError: function(rejection) {
if (rejection.status === 401) {
auth.signout();
store.remove('profile');
store.remove('token');
$location.path('/home')
}
return $q.reject(rejection);
}
}
}
$provide.factory('redirect', redirect);
$httpProvider.interceptors.push('redirect');
...
The redirect function is used to check for a rejection.status of 401 on any responses that come back from HTTP requests. If one is found, we use auth.signout to set isAuthenticated to false, remove the user’s profile and JWT, and take them to the home state.404 instead of a 401,
or possibly some other response code. If this is the case, we could
just set up an array of codes that we want to redirect on, and then set
up some logic that matches any of the items in the array and redirects
the user if they are found.isAuthenticated boolean value that gets set on login isn’t persisted, and thus our Login button comes back, even though we are actually authenticated.$locationChangeStart.// app.js
...
.run(function($rootScope, $state, auth, store, jwtHelper, $location) {
$rootScope.$on('$locationChangeStart', function() {
// Get the JWT that is saved in local storage
// and if it is there, check whether it is expired.
// If it isn't, set the user's auth state
var token = store.get('token');
if (token) {
if (!jwtHelper.isTokenExpired(token)) {
if (!auth.isAuthenticated) {
auth.authenticate(store.get('profile'), token);
}
}
}
else {
// Otherwise, redirect to the home route
$location.path('/home');
}
});
});
...
$locationChangeStart gets
evaluated every time the page is refreshed, or when a new URL is
reached. Inside the callback we are looking for a saved JWT, and if
there is one, we check whether it is expired. If the JWT isn’t expired,
we set the user’s auth state with their profile and token. If the JWT is
expired, we redirect to the home route./profile
route and removing the JWT from local storage. If we were to then send a
request to the protected API endpoint, we would be redirected to the
home route because no JWT would be sent with the request, resulting in a
401. However, the angular-storage
library actually caches items so that they don’t need to be retrieved
from local storage each time, which helps with performance. If we remove
the JWT and then refresh the page, we see that we get redirected when
trying to make the request.logout method, which calls store.remove, will take the JWT out of the cache, and very few people would try to unauthenticate themselves by manually removing their JWT.
Comments
Post a Comment