In a previous post we went through how to authenticated using a DRF endpoint.
This post will expand on how to achieve persistent login with a one page app using Angular. Note as there are 2 frameworks in play there will be some need to workaround integrations problems. It doesn’t get messy, which is a testiment to the quality of the respective frameworks!
For a demo check out this a silly microblog tool I made where you can create an account and log into it: github
Note my folder strucure is like so:
project/ urls.py settings.py static/ js/ app.js ...[all third party js files]... apps/ core/ views.py urls.py accounts/ urls.py api/ views.py serializers.py authentication.py
Create the Angular module
In app.js we create a module that will call the DRF endpoints in order to authenticate:
angular.module('authApp', ['ngResource']). config(['$httpProvider', function($httpProvider){ // django and angular both support csrf tokens. This tells // angular which cookie to add to what header. $httpProvider.defaults.xsrfCookieName = 'csrftoken'; $httpProvider.defaults.xsrfHeaderName = 'X-CSRFToken'; }]). factory('api', function($resource){ function add_auth_header(data, headersGetter){ // as per HTTP authentication spec [1], credentials must be // encoded in base64. Lets use window.btoa [2] var headers = headersGetter(); headers['Authorization'] = ('Basic ' + btoa(data.username + ':' + data.password)); } // defining the endpoints. Note we escape url trailing dashes: Angular // strips unescaped trailing slashes. Problem as Django redirects urls // not ending in slashes to url that ends in slash for SEO reasons, unless // we tell Django not to [3]. This is a problem as the POST data cannot // be sent with the redirect. So we want Angular to not strip the slashes! return { auth: $resource('/api/auth\\/', {}, { login: {method: 'POST', transformRequest: add_auth_header}, logout: {method: 'DELETE'} }), users: $resource('/api/users\\/', {}, { create: {method: 'POST'} }) }; }). controller('authController', function($scope, api) { // Angular does not detect auto-fill or auto-complete. If the browser // autofills "username", Angular will be unaware of this and think // the $scope.username is blank. To workaround this we use the // autofill-event polyfill [4][5] $('#id_auth_form input').checkAndTriggerAutoFillEvent(); $scope.getCredentials = function(){ return {username: $scope.username, password: $scope.password}; }; $scope.login = function(){ api.auth.login($scope.getCredentials()). $promise. then(function(data){ // on good username and password $scope.user = data.username; }). catch(function(data){ // on incorrect username and password alert(data.data.detail); }); }; $scope.logout = function(){ api.auth.logout(function(){ $scope.user = undefined; }); }; $scope.register = function($event){ // prevent login form from firing $event.preventDefault(); // create user and immediatly login on success api.users.create($scope.getCredentials()). $promise. then($scope.login). catch(function(data){ alert(data.data.username); }); }; }); // [1] https://tools.ietf.org/html/rfc2617 // [2] https://developer.mozilla.org/en-US/docs/Web/API/Window.btoa // [3] https://docs.djangoproject.com/en/dev/ref/settings/#append-slash // [4] https://github.com/tbosch/autofill-event // [5] http://remysharp.com/2010/10/08/what-is-a-polyfill/
Create the Angular view
Django and Angular both use curly braces to denote variables. Below we want Django to take care of defining the user (‘{{user.username}}’) based on the value passed in the Context to the template, while Angular should take care of everything else. We use verbatim template tag to prevent Django from stealing Angular’s thunder. In one_page_app.html we do:
<html ng-cloak ng-app> <head> <link rel="stylesheet" href="/static/js/angular/angular-csp.css"/> <link rel="stylesheet" href="/static/js/bootstrap/dist/css/bootstrap.min.css"/> </head> <body> <div class="navbar navbar-fixed-bottom" ng-controller="authController" ng-init="user='{{user.username}}'"> {% verbatim %} <div ng-show="user" class="navbar-form navbar-right"> <input type="submit" class="btn btn-default" ng-click="logout()" value="logout {{user}}"/> </div> <div ng-hide="user"> <form id="id_auth_form" class="navbar-form navbar-right" ng-submit="login()"> <div class="form-group"> <input ng-model="username" required name="username" type="text" placeholder="username" class="form-control"> </div> <div class="form-group"> <input ng-model="password" required name="password" type="password" placeholder="password" class="form-control"> </div> <div class="btn-group"> <input type="submit" class="btn btn-default" value="login"> <input type="submit" class="btn btn-default" value="register" ng-click="register($event)"> </div> </form> </div> </div> </body> <script src="/static/js/jquery/jquery.js"></script> <script src="/static/js/bootstrap/dist/js/bootstrap.min.js"></script> <script src="/static/js/angular/angular.min.js"></script> <script src="/static/js/angular-resource/angular-resource.min.js"></script> <script src="/static/js/autofill-event/src/autofill-event.js"></script> <script src="/static/js/app.js"></script> </html> {% endverbatim %}
Requirements
Bower is fantastic tool to manage front-end requirements. Lets define bower.json like so:
{ "dependencies": { "angular": "1.3.0-build.2419+sha.fe0e434", "angular-resource": "1.3.0-build.2419+sha.fe0e434", "autofill-event": "1.0.0", "jquery": "1.8.2", "bootstrap": "3.0.0", } }
DRF Endpoint
We also need to update the apps.accounts.api.AuthView created in previous post: we need to call django.contrib.auth.login in order to attach the session id the request object, and cause the creation a Set-Cookie header on the respons, which causes the browser to create the defined cookie and so we will achieve persistent login upon reloading the page:
from django.contrib.auth import login, logout class AuthView(APIView): authentication_classes = (QuietBasicAuthentication,) def post(self, request, *args, **kwargs): login(request, request.user) return Response(UserSerializer(request.user).data) def delete(self, request, *args, **kwargs): logout(request) return Response({})
django.contrib.auth.logout stops the session, and sends instructions to browser to make the cookie expire.
Serving the view
We also of course need to serve the one_page_app.html, so in core.views.py we do
from django.views.generic.base import TemplateView class OnePageAppView(TemplateView): template_name = 'one_page_app.html'
And in core.urls we route a url to the Django view:
from django.conf.urls import patterns, include, url from . import views urlpatterns = patterns('', url(r'^$', views.OnePageAppView.as_view(), name='home'), )
With this all plugged in we have a one page app that:
– Shows login form if user isn’t authenticated with their Django session cookie.
– Shows logout if user is authenticated by either session cookie or username and password.
– Allows logging in in a RESTful way.
– Allows creating new user and automatically logs after creation.
– Alerts login and registration errors.
– Took only about 150 lines of code to achieve.
Obviously you must call this Djangular.
Someone beat us to it 🙂
https://github.com/appliedsec/djangular
ok how about djugular. Authentication — straight at the neck 🙂
any one can help me becouse m new to this…. but i need to develop
My partner and I stumbled over here coming from a different web address
and thought I might as well check things out.
I like what I see so now i’m following you. Look forward to going over your web page yet
again.
This is really interesting, You’re a very skilled blogger.
I’ve joined your feed and look forward to seeking more of your great post.
Also, I’ve shared your site in my social networks!
I get pleasure from, cause I found exactly what I was looking for.
You have ended my 4 day long hunt! God Bless you man. Have a nice day.
Bye
Fantastic site. A lot of helpful info here. I’m sending it to several pals ans also sharing in delicious.
And obviously, thanks in your effort!
Very nice… Although you can skip the “forcing escaped slashes” part by adding this to your app config in Angular:
.config([‘$resourceProvider’, function($resourceProvider) { $resourceProvider.defaults.stripTrailingSlashes = false; }]);
Reblogged this on nerd diaries and commented:
A good tutorial about authentication with angularjs and django rest framework
Great tutorial.
Can you tell me how to send some additional data through login api because my request.data is empty in ‘post’ method on the DRF side?
the .DATA property is empty because the username and password are being provided as a header. What additional data are you sending to the login endpoint?
I’d like to send ‘remember me’ boolean value to check if user wants to end session at browser close.