Skip to content

Commit

Permalink
Merge pull request #416 from EDsCODE/git_social_auth
Browse files Browse the repository at this point in the history
Github/Gitlab social auth
  • Loading branch information
timgl committed Apr 6, 2020
2 parents 0aaed5e + f77e9b3 commit 827b575
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 9 deletions.
41 changes: 39 additions & 2 deletions posthog/settings.py
Expand Up @@ -15,7 +15,6 @@
import dj_database_url
import sentry_sdk
from django.core.exceptions import ImproperlyConfigured

from sentry_sdk.integrations.django import DjangoIntegration

VERSION = '1.0.10.2'
Expand Down Expand Up @@ -79,7 +78,8 @@ def get_env(key):
'posthog.apps.PostHogConfig',
'rest_framework',
'loginas',
'corsheaders'
'corsheaders',
'social_django'
]

MIDDLEWARE = [
Expand Down Expand Up @@ -117,6 +117,43 @@ def get_env(key):
WSGI_APPLICATION = 'posthog.wsgi.application'


# Social Auth

SOCIAL_AUTH_POSTGRES_JSONFIELD = True
SOCIAL_AUTH_USER_MODEL = 'posthog.User'

AUTHENTICATION_BACKENDS = (
'social_core.backends.github.GithubOAuth2',
'social_core.backends.gitlab.GitLabOAuth2',
'django.contrib.auth.backends.ModelBackend',
)

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',
'posthog.urls.social_create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
)

SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['signup_token',]

SOCIAL_AUTH_GITHUB_SCOPE = ['user:email']
SOCIAL_AUTH_GITHUB_KEY = os.environ.get('SOCIAL_AUTH_GITHUB_KEY', "")
SOCIAL_AUTH_GITHUB_SECRET = os.environ.get('SOCIAL_AUTH_GITHUB_SECRET', "")

SOCIAL_AUTH_GITLAB_SCOPE = ['read_user']
SOCIAL_AUTH_GITLAB_KEY = os.environ.get('SOCIAL_AUTH_GITLAB_KEY', "")
SOCIAL_AUTH_GITLAB_SECRET = os.environ.get('SOCIAL_AUTH_GITLAB_SECRET', "")

# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

Expand Down
11 changes: 11 additions & 0 deletions posthog/templates/auth_error.html
@@ -0,0 +1,11 @@
{% extends 'layout.html' %}

{% block content %}
<div class="form-signin">
<h1 class="h3 mb-3 font-weight-normal">Ooops! Auth Error</h1>
{% if message %}
<p>{{ message }}</p>
{% endif %}
<a class="btn btn-lg btn-primary btn-block" href="/login">Return to Login</a>
</div>
{% endblock %}
10 changes: 10 additions & 0 deletions posthog/templates/login.html
Expand Up @@ -17,5 +17,15 @@ <h1 class="h3 mb-3 font-weight-normal">Log in to <a href='https://posthog.com'>P
<label for="inputPassword">Password</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
{% if github_auth or gitlab_auth %}
<div class="btn-toolbar mt-3 d-flex justify-content-between">
{% if github_auth %}
<a href="{% url 'social:begin' 'github' %}" class="btn btn-outline-dark">Sign In with Github</a>
{% endif %}
{% if gitlab_auth %}
<a href="{% url 'social:begin' 'gitlab' %}" class="btn btn-outline-dark">Sign In with Gitlab</a>
{% endif %}
</div>
{% endif %}
</form>
{% endblock %}
10 changes: 10 additions & 0 deletions posthog/templates/signup_to_team.html
Expand Up @@ -23,5 +23,15 @@ <h1 class="h3 mb-3 font-weight-normal">Join {{team.name}} on PostHog</h1>
</div>
<p>Already have an account? <a href='/login'>Log in here.</a></p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign up</button>
{% if github_auth or gitlab_auth %}
<div class="btn-toolbar mt-3 d-flex justify-content-between">
{% if github_auth %}
<a href="{% url 'social:begin' 'github' %}?signup_token={{ signup_token }}" class="btn btn-outline-dark">Sign In with Github</a>
{% endif %}
{% if gitlab_auth %}
<a href="{% url 'social:begin' 'gitlab' %}?signup_token={{ signup_token }}" class="btn btn-outline-dark">Sign In with Gitlab</a>
{% endif %}
</div>
{% endif %}
</form>
{% endblock %}
39 changes: 37 additions & 2 deletions posthog/test/test_signup.py
@@ -1,5 +1,9 @@
from django.test import TestCase, Client
from posthog.models import User, DashboardItem, Action, Person, Event
from posthog.models import User, DashboardItem, Action, Person, Event, Team
from social_django.strategy import DjangoStrategy
from social_django.models import DjangoStorage
from social_core.utils import module_member
from posthog.urls import social_create_user

class TestSignup(TestCase):
def setUp(self):
Expand Down Expand Up @@ -29,4 +33,35 @@ def test_signup_new_team(self):
self.assertEqual(items[0].filters['actions'][0]['id'], action.pk)

self.assertEqual(items[1].filters['actions'][0]['id'], action.pk)
self.assertEqual(items[1].type, 'ActionsTable')
self.assertEqual(items[1].type, 'ActionsTable')

class TestSocialSignup(TestCase):
def setUp(self):
super().setUp()
Backend = module_member('social_core.backends.github.GithubOAuth2')
self.strategy = DjangoStrategy(DjangoStorage)
self.backend = Backend(self.strategy, redirect_uri='/complete/github')
self.login_redirect_url = '/'

def test_custom_create_user_pipeline(self):
Team.objects.create(signup_token='faketoken')
details = {'username': 'fake_username', 'email': 'fake@email.com', 'fullname': 'bob bob', 'first_name': 'bob', 'last_name': 'bob'}

# try to create user without a signup token
result = social_create_user(self.strategy, details, self.backend)
count = User.objects.count()
self.assertEqual(count, 0)

# try to create user without a wrong/unassociated token
self.strategy.session_set('signup_token', 'wrongtoken')
result = social_create_user(self.strategy, details, self.backend)
count = User.objects.count()
self.assertEqual(count, 0)

# create user
self.strategy.session_set('signup_token', 'faketoken')
result = social_create_user(self.strategy, details, self.backend)
user = User.objects.get()
self.assertEqual(result['is_new'], True)
self.assertEqual(user.email, "fake@email.com")

48 changes: 43 additions & 5 deletions posthog/urls.py
Expand Up @@ -6,6 +6,7 @@
from django.contrib.auth import authenticate, login, views as auth_views, decorators
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from django.template.loader import render_to_string

from .api import router, capture, user
from .models import Team, User
Expand Down Expand Up @@ -34,7 +35,7 @@ def login_view(request):
password = request.POST['password']
user = authenticate(request, email=email, password=password)
if user is not None:
login(request, user)
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
posthoganalytics.capture(user.distinct_id, 'user logged in')
return redirect('/')
else:
Expand All @@ -59,13 +60,13 @@ def signup_to_team_view(request, token):
try:
user = User.objects.create_user(email=email, password=password, first_name=request.POST.get('name'))
except:
return render_template('signup_to_team.html', request=request, context={'email': email, 'error': True, 'team': team})
login(request, user)
return render_template('signup_to_team.html', request=request, context={'email': email, 'error': True, 'team': team, 'signup_token': token})
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
team.users.add(user)
team.save()
posthoganalytics.capture(user.distinct_id, 'user signed up', properties={'is_first_user': False})
return redirect('/')
return render_template('signup_to_team.html', request, context={'team': team})
return render_template('signup_to_team.html', request, context={'team': team, 'signup_token': token})

def setup_admin(request):
if User.objects.exists():
Expand All @@ -84,7 +85,7 @@ def setup_admin(request):
except:
return render_template('setup_admin.html', request=request, context={'error': True, 'email': request.POST['email'], 'company_name': request.POST.get('company_name'), 'name': request.POST.get('name')})
Team.objects.create_with_data(users=[user], name=company_name)
login(request, user)
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
posthoganalytics.capture(user.distinct_id, 'user signed up', properties={'is_first_user': is_first_user})
posthoganalytics.identify(user.distinct_id, properties={
'email': user.email,
Expand All @@ -93,6 +94,42 @@ def setup_admin(request):
})
return redirect('/')

def social_create_user(strategy, details, backend, user=None, *args, **kwargs):
if user:
return {'is_new': False}

signup_token = strategy.session_get('signup_token')
if signup_token is None:
processed = render_to_string('auth_error.html', {'message': "There is no team associated with this account! Please use an invite link from a team to create an account!"})
return HttpResponse(processed, status=401)

fields = dict((name, kwargs.get(name, details.get(name)))
for name in backend.setting('USER_FIELDS', ['email']))

if not fields:
return

try:
team = Team.objects.get(signup_token=signup_token)
except Team.DoesNotExist:
processed = render_to_string('auth_error.html', {'message': "We can't find the team associated with this signup token. Please ensure the invite link is provided from an existing team!"})
return HttpResponse(processed, status=401)

try:
user = strategy.create_user(**fields)
except:
processed = render_to_string('auth_error.html', {'message': "Account unable to be created. This account may already exist. Please try again or use different credentials!"})
return HttpResponse(processed, status=401)

team.users.add(user)
team.save()
posthoganalytics.capture(user.distinct_id, 'user signed up', properties={'is_first_user': False})

return {
'is_new': True,
'user': user
}

def logout(request):
return auth_views.logout_then_login(request)

Expand All @@ -119,6 +156,7 @@ def logout(request):
path('batch/', capture.get_event),
path('logout', logout, name='login'),
path('login', login_view, name='login'),
path('', include('social_django.urls', namespace='social')),
path('signup/<str:token>', signup_to_team_view, name='signup'),
path('setup_admin', setup_admin, name='setup_admin'),
# react frontend
Expand Down
12 changes: 12 additions & 0 deletions posthog/utils.py
Expand Up @@ -75,6 +75,18 @@ def render_template(template_name: str, request, context=None) -> HttpResponse:
context.update({
'sentry_dsn': os.environ['SENTRY_DSN']
})

attach_social_auth(context)
html = template.render(context, request=request)
return HttpResponse(html)

def attach_social_auth(context):
if os.environ.get('SOCIAL_AUTH_GITHUB_KEY') and os.environ.get('SOCIAL_AUTH_GITHUB_SECRET'):
context.update({
'github_auth': True
})
if os.environ.get('SOCIAL_AUTH_GITLAB_KEY') and os.environ.get('SOCIAL_AUTH_GITLAB_SECRET'):
context.update({
'gitlab_auth': True
})

1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -47,3 +47,4 @@ urllib3==1.25.8
uuid==1.30
wcwidth==0.1.8
whitenoise==5.0.1
social-auth-app-django==3.1.0

0 comments on commit 827b575

Please sign in to comment.