Skip to content
This repository has been archived by the owner on Oct 27, 2022. It is now read-only.

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
mrogaski committed Aug 19, 2016
2 parents b23496f + d35a7fd commit 3848ceb
Show file tree
Hide file tree
Showing 21 changed files with 1,406 additions and 160 deletions.
34 changes: 0 additions & 34 deletions CHANGELOG.md

This file was deleted.

86 changes: 5 additions & 81 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
===================
django-discord-bind
===================

*A Django app for securely associating a user with a Discord account.*

.. image:: https://badge.fury.io/py/django-discord-bind.svg
:target: https://badge.fury.io/py/django-discord-bind
:alt: Git Repository
.. image:: https://travis-ci.org/mrogaski/django-discord-bind.svg?branch=master
:target: https://travis-ci.org/mrogaski/django-discord-bind

:alt: Build Status
.. image:: https://readthedocs.org/projects/django-discord-bind/badge/?version=latest
:target: http://django-discord-bind.readthedocs.io/en/latest/?badge=latest
:alt: Documentation Status

This is a simple Django application that allows users to associate one or
more Discord accounts to their Django accounts and automatically join a
Expand All @@ -21,85 +24,6 @@ Requirements
* Python 2.7, 3.4, 3.5
* Django 1.9, 1.10


Installation
------------

Install with pip::

pip install django-discord-bind

Add `discord_bind` to your `INSTALLED_APPS` setting:

.. code-block:: python
INSTALLED_APPS = [
...
'discord_bind',
]
Include the URL configuration in your project **urls.py**:

.. code-block:: python
urlpatterns = [
...
url(r'^discord/', include('discord_bind.urls')),
]
Run ``python manage.py migrate`` to create the discord_bind models.


Configuration
-------------

Required Settings
^^^^^^^^^^^^^^^^^

DISCORD_CLIENT_ID
The client identifier issued by the Discord authorization server. This
identifier is used in the authorization request of the OAuth 2.0
Authorization Code Grant workflow.

DISCORD_CLIENT_SECRET
A shared secret issued by the Discord authorization server. This
identifier is used in the access token request of the OAuth 2.0
Authorization Code Grant workflow.


Optional Settings
^^^^^^^^^^^^^^^^^

DISCORD_AUTHZ_PATH
The path of the authorization request service endpoint, which will be
appended to the DISCORD_BASE_URI setting.

Default: /oauth2/authorize

DISCORD_BASE_URI
The base URI for the Discord API.

Default: https://discordapp.com/api

DISCORD_INVITE_URI
The URI that the user will be redirected to after one or more successful
auto-invites.

Default: https://discordapp.com/channels/@me

DISCORD_RETURN_URI
The URI that the user will be redirected to if no auto-invites are
attempted or successful.

Default: /

DISCORD_TOKEN_PATH
The path of the access token request service endpoint, which will be
appended to the DISCORD_BASE_URI setting.

Default: /oauth2/token


License
-------

Expand Down
3 changes: 3 additions & 0 deletions discord_bind/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,7 @@
SOFTWARE.
"""
# following PEP 386
__version__ = '0.2.0'

default_app_config = 'discord_bind.apps.DiscordBindConfig'
4 changes: 0 additions & 4 deletions discord_bind/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,9 @@
SOFTWARE.
"""
import requests

from django.contrib import admin
from .models import DiscordUser, DiscordInvite

from discord_bind.app_settings import BASE_URI


@admin.register(DiscordUser)
class DiscordUserAdmin(admin.ModelAdmin):
Expand Down
6 changes: 6 additions & 0 deletions discord_bind/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@
"""
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _


class DiscordBindConfig(AppConfig):
""" Application config """
name = 'discord_bind'
verbose_name = _('Discord Binding')

def ready(self):
from . import conf
39 changes: 21 additions & 18 deletions discord_bind/app_settings.py → discord_bind/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,29 @@
from __future__ import unicode_literals

from django.conf import settings
from appconf import AppConf


# API service endpoints
BASE_URI = getattr(settings, 'DISCORD_BASE_URI',
'https://discordapp.com/api')
AUTHZ_URI = getattr(settings, 'DISCORD_AUTHZ_URI',
BASE_URI + '/oauth2/authorize')
TOKEN_URI = getattr(settings, 'DISCORD_TOKEN_URI',
BASE_URI + '/oauth2/token')
class DiscordBindConf(AppConf):
""" Application settings """
# API service endpoints
BASE_URI = 'https://discordapp.com/api'
AUTHZ_PATH = '/oauth2/authorize'
TOKEN_PATH = '/oauth2/token'

# OAuth2 application credentials
CLIENT_ID = getattr(settings, 'DISCORD_CLIENT_ID', '')
CLIENT_SECRET = getattr(settings, 'DISCORD_CLIENT_SECRET', '')
# OAuth2 application credentials
CLIENT_ID = None
CLIENT_SECRET = None

# OAuth2 scope
AUTHZ_SCOPE = (
['email', 'guilds.join'] if getattr(settings, 'DISCORD_EMAIL_SCOPE', True)
else ['identity', 'guilds.join'])
# OAuth2 scope
EMAIL_SCOPE = True

# Return URI
INVITE_URI = getattr(settings, 'DISCORD_INVITE_URI',
'https://discordapp.com/channels/@me')
RETURN_URI = getattr(settings, 'DISCORD_RETURN_URI', '/')
# URI settings
REDIRECT_URI = None
INVITE_URI = 'https://discordapp.com/channels/@me'
RETURN_URI = '/'

class Meta:
proxy = True
prefix = 'discord'
required = ['CLIENT_ID', 'CLIENT_SECRET']
4 changes: 2 additions & 2 deletions discord_bind/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from django.db import models
from django.contrib.auth.models import User, Group
from django.utils.encoding import python_2_unicode_compatible
from discord_bind.app_settings import BASE_URI
from discord_bind.conf import settings

import logging
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -81,7 +81,7 @@ def __str__(self):

def update_context(self):
result = False
r = requests.get(BASE_URI + '/invites/' + self.code)
r = requests.get(settings.DISCORD_BASE_URI + '/invites/' + self.code)
if r.status_code == requests.codes.ok:
logger.info('fetched data for Discord invite %s' % self.code)
invite = r.json()
Expand Down
153 changes: 153 additions & 0 deletions discord_bind/tests/test_callback_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""
The MIT License (MIT)
Copyright (c) 2016, Mark Rogaski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from __future__ import unicode_literals

try:
from unittest.mock import patch, Mock, MagicMock
except ImportError:
from mock import patch, Mock, MagicMock
try:
from urllib.parse import urlparse
except ImportError:
from urlparse import urlparse
import os

from django.test import TestCase, RequestFactory, override_settings
from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.auth.models import User, AnonymousUser, Group
from django.core.urlresolvers import reverse

from discord_bind.views import callback
from discord_bind.models import DiscordInvite
from discord_bind.conf import settings

os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'


class TestAccessTokenRequest(TestCase):
""" Test the authorization request view """
def setUp(self):
self.factory = RequestFactory()
self.user = User.objects.create_user(username="Ralff",
email="[email protected]",
password="test")
g = Group.objects.create(name='Discord Users')
g.user_set.add(self.user)
for code in ['code0', 'code1', 'code2', 'code3']:
DiscordInvite.objects.create(code=code, active=False)

def tearDown(self):
self.user.delete()

@override_settings(DISCORD_CLIENT_ID='212763200357720576')
def test_get_callback(self):

@patch('discord_bind.views.OAuth2Session.get')
@patch('discord_bind.views.OAuth2Session.fetch_token')
def get_callback(user, query, mock_fetch, mock_get):
# build request
url = reverse('discord_bind_callback')
if query != '':
url = url + '?' + query
request = self.factory.get(url)

# add user and session
request.user = user
middleware = SessionMiddleware()
middleware.process_request(request)
request.session['discord_bind_oauth_state'] = 'xyz'
request.session['discord_bind_invite_uri'] = (
settings.DISCORD_INVITE_URI)
request.session['discord_bind_return_uri'] = (
settings.DISCORD_RETURN_URI)
request.session.save()

# build mock harness
mock_fetch.return_value = {
"access_token": "tvYhMddlVlxNGPtsAN34w9P6pivuLG",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "pUbZsF6BBZ8cD1CZqwxW25hCPUkQF5",
"scope": "email"
}
user_data = {
"avatar": "000d1294c515f3331cf32b31bc132f92",
"discriminator": "4021",
"email": "[email protected]",
"id": "132196734423007232",
"mfa_enabled": True,
"username": "stigg",
"verified": True
}
mock_response = Mock()
mock_response.json.return_value = user_data
mock_get.return_value = mock_response

# fire
return callback(request)

# Anonymous users should bounce to the login page
response = get_callback(AnonymousUser(), '')
self.assertEqual(response.status_code, 302)
self.assertTrue('login' in response['location'])

# Discord user binding
response = get_callback(self.user,
'code=SplxlOBeZQQYbYS6WxSbIA&state=xyz')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], settings.DISCORD_RETURN_URI)

# Missing code
response = get_callback(self.user,
'state=xyz')
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], settings.DISCORD_RETURN_URI)

# CSRF
response = get_callback(self.user,
'code=SplxlOBeZQQYbYS6WxSbIA&state=abc')
self.assertEqual(response.status_code, 403)
response = get_callback(self.user,
'error=server_error&state=abc')
self.assertEqual(response.status_code, 403)

# Missing state
response = get_callback(self.user,
'code=SplxlOBeZQQYbYS6WxSbIA')
self.assertEqual(response.status_code, 403)
response = get_callback(self.user,
'error=server_error')
self.assertEqual(response.status_code, 403)

# Valid error responses
for error in ['invalid_request', 'unauthorized_client',
'access_denied', 'unsupported_response_type',
'invalid_scope', 'server_error',
'temporarily_unavailable']:
response = get_callback(self.user,
'error=%s&state=xyz' % error)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], settings.DISCORD_RETURN_URI)
Loading

0 comments on commit 3848ceb

Please sign in to comment.