Permalink
Browse files

django-authlib 0.1

  • Loading branch information...
matthiask committed Nov 21, 2016
0 parents commit 0e4a81c111f1d402c6a995121468b8e03e7397bf
Showing with 463 additions and 0 deletions.
  1. +11 −0 .gitignore
  2. +15 −0 .travis.yml
  3. +21 −0 LICENSE
  4. +6 −0 MANIFEST.in
  5. +8 −0 README.rst
  6. +2 −0 authlib/__init__.py
  7. +9 −0 authlib/base.py
  8. +170 −0 authlib/email.py
  9. +50 −0 authlib/facebook.py
  10. +52 −0 authlib/google.py
  11. +78 −0 authlib/twitter.py
  12. +5 −0 setup.cfg
  13. +36 −0 setup.py
@@ -0,0 +1,11 @@
*.py?
*~
*.sw?
.DS_Store
._*
*.egg-info
/MANIFEST
_build
build
dist
venv
@@ -0,0 +1,15 @@
language: python
sudo: false
python:
- "3.4"
- "2.7"
env:
- REQ="Django>=1.8,<1.9"
- REQ="Django>=1.9,<1.10"
- REQ="Django>=1.10,<1.11"
# command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
install:
- pip install -q $REQ towel==0.8.0 --use-mirrors
- python setup.py -q install
# command to run tests, e.g. python setup.py test
script: "cd tests && ./manage.py test testapp"
21 LICENSE
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2016 FEINHEIT AG

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.
@@ -0,0 +1,6 @@
include LICENSE
include MANIFEST.in
include README.rst
recursive-include authlib/static *
recursive-include authlib/locale *
recursive-include authlib/templates *
@@ -0,0 +1,8 @@
================================================
django-authlib - Authentication utils for Django
================================================

Usage
=====

No usage description yet, sorry.
@@ -0,0 +1,2 @@
VERSION = (0, 1, 0)
__version__ = '.'.join(map(str, VERSION))
@@ -0,0 +1,9 @@
class OAuthClient(object):
def __init__(self, request):
self._request = request

def get_authentication_url(self):
pass

def get_user_data(self):
pass
@@ -0,0 +1,170 @@
from django.contrib.auth import get_user_model
from django.core import signing
from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.template.loader import TemplateDoesNotExist, render_to_string
from django.urls import reverse
from django.utils.http import int_to_base36
from django.utils.translation import ugettext as _


# Assumes that this is a model with an unique `email` field.
User = get_user_model()


def render_to_mail(template, context, **kwargs):
"""
Renders a mail and returns the resulting ``EmailMultiAlternatives``
instance
* ``template``: The base name of the text and HTML (optional) version of
the mail.
* ``context``: The context used to render the mail. This context instance
should contain everything required.
* Additional keyword arguments are passed to the ``EmailMultiAlternatives``
instantiation. Use those to specify the ``to``, ``headers`` etc.
arguments.
Usage example::
# Render the template myproject/hello_mail.txt (first non-empty line
# contains the subject, third to last the body) and optionally the
# template myproject/hello_mail.html containing the alternative HTML
# representation.
message = render_to_mail('myproject/hello_mail', {}, to=[email])
message.send()
"""
lines = iter(render_to_string('%s.txt' % template, context).splitlines())

subject = ''
while True:
line = next(lines)
if line:
subject = line
break

body = '\n'.join(lines).strip('\n')
message = EmailMultiAlternatives(subject=subject, body=body, **kwargs)

try:
message.attach_alternative(
render_to_string('%s.html' % template, context),
'text/html')
except TemplateDoesNotExist:
pass

return message


def get_signer(salt='email_registration'):
"""
Returns the signer instance used to sign and unsign the registration
link tokens
"""
return signing.TimestampSigner(salt=salt)


def get_last_login_timestamp(user):
"""
Django 1.7 allows the `last_login` timestamp to be `None` for new users.
"""
return int(user.last_login.strftime('%s')) if user.last_login else 0


def get_confirmation_url(email, request, user=None):
"""
Returns the confirmation URL
"""
code = [email, '', '']
if user:
code[1] = str(user.id)
code[2] = int_to_base36(get_last_login_timestamp(user))

return request.build_absolute_uri(
reverse(
'email_registration_confirm',
kwargs={
'code': get_signer().sign(u':'.join(code)),
}))


def send_registration_mail(email, *, request, user=None):
"""
Sends the registration mail
* ``email``: The email address where the registration link should be
sent to.
* ``request``: A HTTP request instance, used to construct the complete
URL (including protocol and domain) for the registration link.
* ``user``: Optional user instance. If the user exists already and you
just want to send a link where the user can choose his/her password.
The mail is rendered using the following two templates:
* ``registration/email_registration_email.txt``: The first line of this
template will be the subject, the third to the last line the body of the
email.
* ``registration/email_registration_email.html``: The body of the HTML
version of the mail. This template is **NOT** available by default and
is not required either.
"""

render_to_mail(
'registration/email_registration_email',
{
'url': get_confirmation_url(email, request, user=user),
},
to=[email],
).send()


def decode(code, *, max_age):
"""
Decodes the code from the registration link and returns a tuple consisting
of the verified email address and the associated user instance or ``None``
if no user was passed to ``send_registration_mail``
Pass the maximum age in seconds of the link as ``max_age``.
This method raises ``ValidationError`` exceptions containing an translated
message what went wrong suitable for presenting directly to the user.
"""
try:
data = get_signer().unsign(code, max_age=max_age)
except signing.SignatureExpired:
raise ValidationError(_(
'The link is expired. Please request another registration link.'
), code='email_registration_expired')

except signing.BadSignature:
raise ValidationError(_(
'Unable to verify the signature. Please request a new'
' registration link.'
), code='email_registration_signature')

parts = data.split(':')
if len(parts) != 3:
raise ValidationError(_(
'Something went wrong while decoding the'
' registration request. Please try again.'
), code='email_registration_broken')

email, uid, timestamp = parts
if uid and timestamp:
try:
user = User.objects.get(pk=uid)
except (User.DoesNotExist, TypeError, ValueError):
raise ValidationError(_(
'Something went wrong while decoding the'
' registration request. Please try again.'
), code='email_registration_invalid_uid')

if timestamp != int_to_base36(get_last_login_timestamp(user)):
raise ValidationError(_(
'The link has already been used.'
), code='email_registration_used')

else:
user = None

return email, user
@@ -0,0 +1,50 @@
from django.conf import settings
from django.urls import reverse

from requests_oauthlib import OAuth2Session

from .base import OAuthClient


class FacebookOAuth2Client(OAuthClient):
authorization_base_url = 'https://www.facebook.com/dialog/oauth'
token_url = 'https://graph.facebook.com/oauth/access_token'
scope = ['email']
client_id = settings.FACEBOOK_CLIENT_ID
client_secret = settings.FACEBOOK_CLIENT_SECRET

def __init__(self, request):
self._request = request
self._session = OAuth2Session(
self.client_id,
scope=self.scope,
redirect_uri=request.build_absolute_uri(reverse(
'accounts_oauth2',
kwargs={'provider': 'facebook'},
)),
)

def get_authentication_url(self):
authorization_url, self._state = self._session.authorization_url(
self.authorization_base_url,
access_type='online', # Only right now.
# approval_prompt='force', # Maybe not, later.
)

return authorization_url

def get_user_data(self):
self._session.fetch_token(
self.token_url,
client_secret=self.client_secret,
authorization_response=self._request.get_full_path(),
)
data = self._session.get(
'https://graph.facebook.com/v2.8/me',
params={'fields': 'email,name'},
).json()

return {
'email': data['email'],
'full_name': data['name'],
}
@@ -0,0 +1,52 @@
from django.conf import settings
from django.urls import reverse

from requests_oauthlib import OAuth2Session

from .base import OAuthClient


class GoogleOAuth2Client(OAuthClient):
authorization_base_url = 'https://accounts.google.com/o/oauth2/v2/auth'
token_url = 'https://www.googleapis.com/oauth2/v4/token'
scope = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
client_id = settings.GOOGLE_CLIENT_ID
client_secret = settings.GOOGLE_CLIENT_SECRET

def __init__(self, request):
self._request = request
self._session = OAuth2Session(
self.client_id,
scope=self.scope,
redirect_uri=request.build_absolute_uri(reverse(
'accounts_oauth2',
kwargs={'provider': 'google'},
)),
)

def get_authentication_url(self):
authorization_url, self._state = self._session.authorization_url(
self.authorization_base_url,
access_type='offline', # Only right now.
# approval_prompt='force', # Maybe not, later.
)

return authorization_url

def get_user_data(self):
self._session.fetch_token(
self.token_url,
client_secret=self.client_secret,
authorization_response=self._request.get_full_path(),
)
data = self._session.get(
'https://www.googleapis.com/oauth2/v1/userinfo'
).json()

return {
'email': data['email'],
'full_name': data['name'],
}
Oops, something went wrong.

0 comments on commit 0e4a81c

Please sign in to comment.