diff --git a/.travis.yml b/.travis.yml
index 150e48978b6f17689d62fbe7b685bc977ed0bf4c..d26b3402bfaa6eb60f5b86c6adee7b7b0d91d5e6 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -63,7 +63,10 @@ script:
   - python manage.py check
 
   # Check for missing migrations
-  - python manage.py makemigrations --check --noinput
+  - python manage.py makemigrations --check --noinput --verbosity=0
+
+  # Collect static
+  - python manage.py collectstatic --noinput --verbosity=0
 
   # Run tests
   - coverage run --source='.' manage.py test opentech
diff --git a/opentech/apply/users/decorators.py b/opentech/apply/users/decorators.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ed11e35dc33bcd48ff59a7c2f76dbb47c3956d3
--- /dev/null
+++ b/opentech/apply/users/decorators.py
@@ -0,0 +1,14 @@
+from django.core.exceptions import PermissionDenied
+
+from .utils import can_use_oauth_check
+
+
+def require_oauth_whitelist(view_func):
+    """Simple decorator that limits the use of OAuth to the configure whitelisted domains"""
+    def decorated_view(request, *args, **kwargs):
+        if can_use_oauth_check(request.user):
+            return view_func(request, *args, **kwargs)
+
+        raise PermissionDenied
+
+    return decorated_view
diff --git a/opentech/apply/users/templates/users/account.html b/opentech/apply/users/templates/users/account.html
index 725d615316bb18d3700f3dc45ed61a22253c7ad1..38dc57c67f42fb10a9823b672a61418348709802 100644
--- a/opentech/apply/users/templates/users/account.html
+++ b/opentech/apply/users/templates/users/account.html
@@ -1,12 +1,17 @@
 {% extends 'base.html' %}
-{% load i18n %}
+{% load i18n users_tags %}
 
 {% block title %}Account{% endblock %}
 
 {% block content %}
 <h2>Welcome {{ user }}</h2>
 
-{% if show_change_password %}
+{% if show_change_password and user.has_usable_password and not backends.associated %}
     <a href="{% url 'users:password_change' %}">{% trans "Change password" %}</a>
 {% endif %}
+
+{% can_use_oauth as show_oauth_link %}
+{% if show_oauth_link %}
+    <a href="{% url 'users:oauth' %}">{% trans "Manage OAuth" %}</a>
+{% endif %}
 {% endblock %}
diff --git a/opentech/apply/users/templates/users/login.html b/opentech/apply/users/templates/users/login.html
index e2355d27b3b166c1b34a8edc4ea0b2c1ab02c3c3..8934f777685119e186be90537fbb5837776511c9 100644
--- a/opentech/apply/users/templates/users/login.html
+++ b/opentech/apply/users/templates/users/login.html
@@ -9,4 +9,6 @@
     {{ form.as_p }}
     <button type="submit">Login</button>
   </form>
+
+  <a href="{% url "social:begin" "google-oauth2" %}?next={% url "users:account" %}">Log in with your OTF email</a>
 {% endblock %}
diff --git a/opentech/apply/users/templates/users/oauth.html b/opentech/apply/users/templates/users/oauth.html
new file mode 100644
index 0000000000000000000000000000000000000000..c51d40a154c75fec0b114c33488eba4b9b142349
--- /dev/null
+++ b/opentech/apply/users/templates/users/oauth.html
@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+{% load users_tags %}
+
+{% block title %}OAuth{% endblock %}
+
+{% block content %}
+<div class="social">
+    {% if backends.associated %}
+        <h3>Providers</h3>
+        <ul>
+        {% for association in backends.associated %}
+            <li>
+                {% if user.has_usable_password %}
+                    <form id="{{ association.provider|backend_name }}-disconnect" class="disconnect-form col-md-2" action="{% url 'social:disconnect_individual' backend=association.provider association_id=association.id %}?next={% url "users:account" %}"
+                        method="post">
+                        <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
+                        <button class="btn btn-danger" name="{{ association.provider|backend_class }}">
+                            Disconnect {{ association.provider|backend_name }}
+                        </button>
+                    </form>
+                {% else %}
+                    {{ association.provider|backend_name }}
+                {% endif %}
+            </li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+    {% if backends.not_associated %}
+        <h3>Available providers</h3>
+        <ul>
+        {% for backend in backends.not_associated %}
+            <li>
+                <a id="{{ name }}-button" class="col-md-2 btn btn-default" name="{{ backend }}" href="{% url 'social:begin' backend=backend %}">
+                    {{ backend|backend_name }}
+                </a>
+            </li>
+        {% endfor %}
+        </ul>
+    {% endif %}
+</div>
+{% endblock %}
diff --git a/opentech/apply/users/templatetags/__init__.py b/opentech/apply/users/templatetags/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/users/templatetags/users_tags.py b/opentech/apply/users/templatetags/users_tags.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3e8b5432f9fe4e1221c013a0061350e111a21e1
--- /dev/null
+++ b/opentech/apply/users/templatetags/users_tags.py
@@ -0,0 +1,27 @@
+from django import template
+
+from ..utils import can_use_oauth_check
+
+register = template.Library()
+
+
+@register.filter
+def backend_name(name):
+    """Human readable mapping for the social auth backend"""
+    return {
+        'google-oauth': 'Google OAuth',
+        'google-oauth2': 'Google OAuth',
+        'google-openidconnect': 'Google OpenId',
+    }.get(name, name)
+
+
+@register.filter
+def backend_class(backend):
+    return backend.replace('-', ' ')
+
+
+@register.simple_tag(takes_context=True)
+def can_use_oauth(context):
+    user = context.get('user')
+
+    return can_use_oauth_check(user)
diff --git a/opentech/apply/users/tests/__init__.py b/opentech/apply/users/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/opentech/apply/users/tests/test_oauth_access.py b/opentech/apply/users/tests/test_oauth_access.py
new file mode 100644
index 0000000000000000000000000000000000000000..c425c7f0ec4dd89964346ad51c11dc23c7d30fb4
--- /dev/null
+++ b/opentech/apply/users/tests/test_oauth_access.py
@@ -0,0 +1,58 @@
+from django.contrib.auth import get_user_model
+from django.conf import settings
+from django.test import TestCase, override_settings
+from django.urls import reverse
+
+
+class TestOAuthAccess(TestCase):
+
+    def test_oauth_page_requires_login(self):
+        """
+        This checks that /account/oauth requires the user to be logged in
+        """
+        oauth_page = reverse('users:oauth')
+        response = self.client.get(oauth_page, follow=True)
+        self.assertRedirects(response, reverse(
+            'users:login') + '?next=' + reverse('users:oauth'), status_code=301, target_status_code=200)
+
+    @override_settings()
+    def test_oauth_not_set_up(self):
+        del settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS
+
+        self.login()
+        response = self.client.get(reverse('users:oauth'), follow=True)
+        self.assertEqual(response.status_code, 403)
+
+    def test_oauth_user_email_not_whitelisted(self):
+        self.login()
+        response = self.client.get(reverse('users:oauth'), follow=True)
+        self.assertEqual(response.status_code, 403)
+
+    @override_settings(SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS=['email.com'])
+    def test_oauth_whitelisted_user_can_see_link_to_oauth_settings_page(self):
+        self.login()
+
+        response = self.client.get(reverse('users:account'), follow=True)
+        self.assertContains(response, 'Manage OAuth')
+
+    @override_settings(SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS=['email.com'])
+    def test_oauth_whitelisted_user_can_access_oauth_settings_page(self):
+        """
+        Checks that the test user can access the OAuth page as their email is whitelisted
+        """
+        self.login()
+
+        response = self.client.get(reverse('users:oauth'), follow=True)
+        self.assertEqual(response.status_code, 200)
+        self.assertContains(response, 'Google OAuth')
+        self.assertNotContains(response, 'Disconnect Google OAuth')
+
+        self.assertTemplateUsed(response, 'users/oauth.html')
+
+    def login(self):
+        user = get_user_model().objects.create_user(username='test', email='test@email.com', password='password')
+        self.assertTrue(
+            self.client.login(username='test', password='password')
+        )
+
+        return user
diff --git a/opentech/apply/users/urls.py b/opentech/apply/users/urls.py
index 4942e2dada305c2d44cd8b4df19f80b9d882ab6b..1fc074d79c5e3e79e3cba737a10a1915552ce18c 100644
--- a/opentech/apply/users/urls.py
+++ b/opentech/apply/users/urls.py
@@ -2,7 +2,7 @@ from django.conf.urls import url
 from django.contrib.auth import views as auth_views
 from django.urls import reverse_lazy
 
-from opentech.apply.users.views import account
+from opentech.apply.users.views import account, oauth
 
 urlpatterns = [
     url(r'^$', account, name='account'),
@@ -53,4 +53,5 @@ urlpatterns = [
         auth_views.PasswordResetCompleteView.as_view(template_name='users/password_reset/complete.html'),
         name='password_reset_complete'
     ),
+    url(r'^oauth$', oauth, name='oauth'),
 ]
diff --git a/opentech/apply/users/utils.py b/opentech/apply/users/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..0e290b3203a99a774513b2f2dc5bd9134b4b3cfd
--- /dev/null
+++ b/opentech/apply/users/utils.py
@@ -0,0 +1,16 @@
+from django.conf import settings
+
+
+def can_use_oauth_check(user):
+    """
+    Checks that the user belongs to the whitelisted domains.
+    Anonymous or non-whitelisted email domains cannot log in
+    or associate OAuth accounts
+    """
+    try:
+        domain = user.email.split('@')[-1]
+        return domain in settings.SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS
+    except AttributeError:
+        # Anonymous user or setting not defined
+        pass
+    return False
diff --git a/opentech/apply/users/views.py b/opentech/apply/users/views.py
index c531b183ae221c1441c401f0f17f36ff2fe32980..c9bad14dfb283aa4c1c8caa5f2e769cf20708025 100644
--- a/opentech/apply/users/views.py
+++ b/opentech/apply/users/views.py
@@ -1,9 +1,12 @@
 from django.contrib.auth.decorators import login_required
 from django.shortcuts import render
+from django.template.response import TemplateResponse
 from django.urls import reverse_lazy
 
 from wagtail.wagtailadmin.views.account import password_management_enabled
 
+from .decorators import require_oauth_whitelist
+
 
 @login_required(login_url=reverse_lazy('users:login'))
 def account(request):
@@ -12,3 +15,13 @@ def account(request):
     return render(request, 'users/account.html', {
         'show_change_password': password_management_enabled() and request.user.has_usable_password(),
     })
+
+
+@login_required(login_url=reverse_lazy('users:login'))
+@require_oauth_whitelist
+def oauth(request):
+    """
+    Generic, empty view for the OAuth associations.
+    """
+
+    return TemplateResponse(request, 'users/oauth.html', {})
diff --git a/opentech/settings/base.py b/opentech/settings/base.py
index 22546b424a49d35b8533c7bca1123adcd054378f..9d805608db649bdf1fe68bbbf3fb1c351c3a5aea 100644
--- a/opentech/settings/base.py
+++ b/opentech/settings/base.py
@@ -31,6 +31,8 @@ INSTALLED_APPS = [
     'opentech.public.standardpages',
     'opentech.public.utils',
 
+    'social_django',
+
     'wagtail.contrib.modeladmin',
     'wagtail.contrib.postgres_search',
     'wagtail.contrib.settings',
@@ -72,6 +74,8 @@ MIDDLEWARE = [
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 
+    'social_django.middleware.SocialAuthExceptionMiddleware',
+
     'wagtail.wagtailcore.middleware.SiteMiddleware',
     'wagtail.wagtailredirects.middleware.RedirectMiddleware',
     'opentech.public.esi.middleware.ESIMiddleware',
@@ -94,6 +98,8 @@ TEMPLATES = [
                 'django.contrib.messages.context_processors.messages',
                 'wagtail.contrib.settings.context_processors.settings',
                 'opentech.public.utils.context_processors.global_vars',
+                'social_django.context_processors.backends',
+                'social_django.context_processors.login_redirect',
             ],
         },
     },
@@ -188,6 +194,11 @@ AUTH_USER_MODEL = 'users.User'
 LOGIN_URL = 'users:login'
 LOGIN_REDIRECT_URL = '/'
 
+AUTHENTICATION_BACKENDS = (
+    'social_core.backends.google.GoogleOAuth2',
+    'django.contrib.auth.backends.ModelBackend',
+)
+
 # Logging
 
 LOGGING = {
@@ -266,3 +277,31 @@ ESI_ENABLED = False
 # Custom settings
 
 ENABLE_STYLEGUIDE = False
+
+# Social Auth
+SOCIAL_AUTH_URL_NAMESPACE = 'social'
+
+# Set the Google OAuth2 credentials in ENV variables or local.py
+# To create a new set of credentials, go to https://console.developers.google.com/apis/credentials
+# Make sure the Google+ API is enabled for your API project
+SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS = ['opentechfund.org']
+SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = ''
+SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = ''
+
+SOCIAL_AUTH_LOGIN_ERROR_URL = 'users:account'
+SOCIAL_AUTH_NEW_ASSOCIATION_REDIRECT_URL = 'users:account'
+
+# For pipelines, see http://python-social-auth.readthedocs.io/en/latest/pipeline.html?highlight=pipelines#authentication-pipeline
+# Create / associate accounts (including by email)
+SOCIAL_AUTH_PIPELINE = (
+    'social_core.pipeline.social_auth.social_details',
+    'social_core.pipeline.social_auth.social_uid',
+    'social_core.pipeline.social_auth.auth_allowed',
+    'social_core.pipeline.social_auth.social_user',
+    'social_core.pipeline.user.get_username',
+    'social_core.pipeline.social_auth.associate_by_email',
+    'social_core.pipeline.user.create_user',
+    'social_core.pipeline.social_auth.associate_user',
+    'social_core.pipeline.social_auth.load_extra_data',
+    'social_core.pipeline.user.user_details',
+)
diff --git a/opentech/urls.py b/opentech/urls.py
index f15ca87d8a2f1e85d1753b4bb7f3d778f2348ec7..9048a2eedac7677a8273b30a5ec743eb795a84b8 100644
--- a/opentech/urls.py
+++ b/opentech/urls.py
@@ -22,6 +22,7 @@ urlpatterns = [
     url('^sitemap\.xml$', sitemap),
     url('^', include(public_urls)),
     url('^', include(apply_urls)),
+    url('^', include('social_django.urls', namespace='social')),
 ]
 
 
diff --git a/requirements.txt b/requirements.txt
index b4faca247b766cd071cf0f20e18e3861dcb15956..83d06ba2b9b890ff1af1af148881eadcb7987443 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,6 +12,8 @@ factory_boy==2.9.2
 wagtail_factories==0.3.0
 flake8
 
+social_auth_app_django==2.1.0
+
 # Production dependencies
 dj-database-url==0.4.1
 whitenoise==2.0.4