In this part of the tutorial, we’re going to manage user authentication using the django-allauth package, which let’s you configure quite a few Social Accounts, including Facebook, Twitter or Google.
The idea is that when a user visits the home page, it can log in (or register) using any of the available social account services. Moreover, the database creates a new User instance every time a user is registered through this process.
In this part we’re going to focus on Google authentication, and as always, we’re going to start writing a test 🙂
The outline of this part is:
- Define what we expect and write a Test
- Install django-allauth
- Settings File
- Urls
- Database migrations
- Sites configuration
- Google App registration
- Allauth Django configuration
- Testing the user flow
Define what we expect and write a Test
As we haven’t touched the base.html template that came with HTML5 Boilerplate, when we go to the TaskBuster home page we see a top navigation bar with a Sign in form.
We are going to modify it so that only contains a button that says “Sign in with Google”, visible for users that are not logged in, and a “Logout” button for the others.
Let’s write a new functional test in functional_tests/test_allauth.py (you have to create this file):
# -*- coding: utf-8 -*- from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException from django.core.urlresolvers import reverse from django.contrib.staticfiles.testing import StaticLiveServerTestCase from django.utils.translation import activate class TestGoogleLogin(StaticLiveServerTestCase): def setUp(self): self.browser = webdriver.Firefox() self.browser.implicitly_wait(3) self.browser.wait = WebDriverWait(self.browser, 10) activate('en') def tearDown(self): self.browser.quit() def get_element_by_id(self, element_id): return self.browser.wait.until(EC.presence_of_element_located( (By.ID, element_id))) def get_button_by_id(self, element_id): return self.browser.wait.until(EC.element_to_be_clickable( (By.ID, element_id))) def get_full_url(self, namespace): return self.live_server_url + reverse(namespace) def test_google_login(self): self.browser.get(self.get_full_url("home")) google_login = self.get_element_by_id("google_login") with self.assertRaises(TimeoutException): self.get_element_by_id("logout") self.assertEqual( google_login.get_attribute("href"), self.live_server_url + "/accounts/google/login") google_login.click() with self.assertRaises(TimeoutException): self.get_element_by_id("google_login") google_logout = self.get_element_by_id("logout") google_logout.click() google_login = self.get_element_by_id("google_login")
The previous test:
- Initializes a browser in setUp. WebDriverWait is used to make the browser wait a certain amount of time before rising an exception when an element is not found.
- tearDown just quits the browser
- get_element_by_id and get_button_by_id are helper functions that use WebDriverWait to find elements by ID. Note that for a button we wait until the element is clickable.
- get_full_url is another helper function that we used in other tests. It returns the full url given a reverse name.
- test_google_login is the main test here. It goes to the home page and:
- checks that the login button is present
- checks that the logout button is not present
- checks that the login button points to the correct url (/accounts/google/login)
- checks that after clicking on the login button, the user gets logged in and it sees the logout button instead.
- a click on the logout button should make the user see the login button again.
Now that we know what we want, let’s install the allauth package 🙂
Install django-allauth
Let’s install the package django-allauth, which allows users to register and log in using different social accounts like Google or Twitter.
$ pip install django-allauth
which will install several libraries:
Successfully installed django-allauth python3-openid requests-oauthlib requests defusedxml oauthlib Cleaning up...
You should add them all into the requirements/base.txt file, and install them into the testing environment.
Note: if the version of django-allauth < 0.20.0, you should install the development version in order to be compatible with Django 1.8, with:
$ pip install -U git+https://github.com/pennersr/django-allauth.git
Settings File
Next, open the settings/base.py file and make sure you have the django.template.context_procesors.request. If you’re using Django 1.7, you’ll have to use allauth context processors (read more):
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, "templates")], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', 'django.template.context_processors.i18n', # Required by allauth template tags "django.core.context_processors.request", ], }, }, ]
Next, we need to include the Authentication Backend used by allauth:
AUTHENTICATION_BACKENDS = ( # Default backend -- used to login by username in Django admin "django.contrib.auth.backends.ModelBackend", # `allauth` specific authentication methods, such as login by e-mail "allauth.account.auth_backends.AuthenticationBackend", )
And the required apps (including the app for Google):
INSTALLED_APPS += ( # The Django sites framework is required 'django.contrib.sites', 'allauth', 'allauth.account', 'allauth.socialaccount', # Login via Google 'allauth.socialaccount.providers.google', ) SITE_ID = 1
Note that we are adding these Installed Apps into the existing INSTALLED_APPS setting using +=.
The SITE_ID parameter is used by the Django sites framework.
Finally, we are going to set the following parameters to customize the authorization process:
ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_EMAIL_VERIFICATION = "none" SOCIALACCOUNT_QUERY_EMAIL = True LOGIN_REDIRECT_URL = "/"
This will make allauth to ask for the email (if possible) in the authorization process. It will ask it to Google, without any verification process, and after logging in, it will redirect the user to the home page.
You can find more info about the available settings here.
Urls
Next open the taskbuster/urls.py file and add the corresponding urls before the i18n internationalization urls:
urlpatterns = [ url(r'^(?P<filename>(robots.txt)|(humans.txt))$', home_files, name='home-files'), url(r'^accounts/', include('allauth.urls')), ] urlpatterns += i18n_patterns( ... )
Database migrations
Ok, now we just need to update the database:
$ python manage.py check $ python manage.py migrate
Sites configuration
Next, we need to configure the Sites Framework. Run the development server with
$ python manage.py runserver
and go to the admin interface at http://127.0.0.1:8000/admin/sites/site and create a Site for the localhost, 127.0.0.1:8000, or your website domain for production. If you have an example.com site defined, modify it so it has an id equal to the SITE_ID=1 setting variable. Otherwise, if you create a new site, you have to change the settings variable, SITE_ID, with the ID of the site you just created (probably 2).
Note: you need to create a superuser account to access the Django admin. If you don’t have it yet, you can create a new one with:
$ python manage.py createsuperuser
We are ready to create our Google Application!
Goolge App Registration
First, we are going to create a Google App to obtain a key/secret pair. Go to the Google Developers Console and click on Create Project. Choose a name for your project and an ID.
Next, click on your newly created project and on the left menu select APIs & auth –> Credentials and click on the Consent screen tab. You should provide at least a Name and an Email.
Go back to the Credentials tab and click on Create new Client ID. Select Web Application and use:
Authorized Javascript Origins: http://127.0.0.1:8000/ Authorized Redirect Uris: http://127.0.0.1:8000/accounts/google/login/callback/
Note that the redirect URI proposed automatically is different from the one written here 🙂
This app will work in our development and testing environment. You should create another Client/secret pair for production, and change http://127.0.0.1:8000/ by your website domain.
Great! We have our Google App ready, so let’s configure the Django part!
Allauth Django Configuration
Create a Social Application for Google at http://127.0.0.1:8000/admin/socialaccount/socialapp, with the following properties:
- Provider: Google
- Name: Google (or something similar)
- Client ID: your application Client ID (obtained in the Developers Console at APIs & auth –> Credentials).
- Secret Key: your application Client Secret
- Key: not needed (leave blank).
- Select the corresponding site.
Finally, just save the instance.
Testing the user flow
Ok, now we are ready to run our tests!
$ python manage.py test functional_tests.test_allauth
First, it fails because the home page doesn’t have an element with an id equal to google_login.
Let’s edit the taskbuster/templates/base.html file and find the navigation bar. There you should replace the form tag by the following code:
<div class="navbar-collapse collapse"> <div class="navbar-form navbar-right"> {% if user.is_authenticated %} <a id="logout" href="/accounts/logout" class="btn btn-success">Logout</a> {% else %} <a id="google_login" href="/accounts/google/login" class="btn btn-success"> Sign in with Google </a> {% endif %} </div> </div><!--/.navbar-collapse -->
We have included some logic here, to display the Sign in button only when the user is not logged in, and the logout button otherwise.
If you run the tests again, you’ll see that we get an Internal Error after clicking on the Sign in button. This is because the testing database doesn’t have a Google App defined (the testing database is empty!). Let’s change this!
As in the development database we only have a Site, a Google App and the admin user, we are going to use it to dump its data to create a fixture.
First, create the folder fixutres:
$ mkdir taskbuster/fixtures
and next, create the fixture with
$ python manage.py dumpdata --indent 2 --natural -e contenttypes -e auth.Permission > taskbuster/fixtures/allauth_fixture.json
Open you .gitignore file and add a line to omit this folder, as it contains sensitive information
# .gitignore ... taskbuster/fixtures
Next, we need to load this fixture in our tests. In your settings/testing.py file indicate the fixture folder
FIXTURE_DIRS = ( os.path.join(BASE_DIR, 'fixtures'), )
and at the beginning of your test class, include the fixture as
class TestGoogleLogin(StaticLiveServerTestCase): fixtures = ['allauth_fixture'] def setUp(self): ...
Ok, try again to run your tests.
$ python manage.py test functional_tests.test_allauth
This time you’ll see the Google Authorization page, where the user should indicate its username and password to log in, and the test fails as it can’t find the logout button.
Let’s make the test to insert our credentials. Create the file taskbuster/fixtures/google_user.json and write
{"Email": "example@gmail.com", "Passwd": "example_psw"}
where you should insert a valid Google Credentials. As this file is located under the fixtures folder, it won’t be in the git repository.
Note that the keys Email and Passwd are the html element ids of the Google login form.
Next, add the following method inside the test, that will be called after clicking on the Sign in button:
class TestGoogleLogin(StaticLiveServerTestCase): ... def user_login(self): import json with open("taskbuster/fixtures/google_user.json") as f: credentials = json.loads(f.read()) self.get_element_by_id("Email").send_keys(credentials["Email"]) self.get_button_by_id("next").click() self.get_element_by_id("Passwd").send_keys(credentials["Passwd"]) for btn in ["signIn", "submit_approve_access"]: self.get_button_by_id(btn).click() return def test_google_login(self): ... google_login.click() self.user_login() ...
Ok, if you run this test again you’ll see the following message in the browser:
indicating that the redirect URI at http://localhost:8081 is not valid. This is because we registered a different url, http://127.0.0.1:8000, and not the one that is used by tests.
Go back into the Developers Console and register the url http://localhost:8081 and the corresponding callback url in your app.
Next, open the taskbuster/fixtures/allauth_fixture.json, and replace all occurrences of 127.0.0.1:8000 for localhost:8081 (I counted 3).
Run the test again.
Yes! the authorization process was successful 🙂
But… when the Logout button is clicked, you are redirected to another url, with a confirmation form in it.
In order to omit this step, edit the taskbuster/urls.py file and add the following line before including allauth.urls:
url(r'^accounts/logout/$', 'django.contrib.auth.views.logout', {'next_page': '/'}),
This will make the user to go back to the home page after clicking the Logout button.
Ok, run your tests again, this time it should work! 🙂
Well Done!!
Can you try to configure another Social Account? Like Twitter, LinkedIn or Facebook?
What’s next?
Now, you can:
- Take a look at Part B – User Authentication with Twitter
- or continue this tutorial by leaning how to create your first App and start defining Django Models!
Please, +1 and share if useful! Thanks! 🙂