Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pulling in most of the PRs that are stale. #101

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 179 additions & 171 deletions Pipfile.lock

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,20 +309,22 @@ DEFAULTS = {

# What function is called to construct a serializer for drf tokens when
# exchanging a passwordless token for a real user auth token.
'PASSWORDLESS_AUTH_TOKEN_SERIALIZER': 'drfpasswordless.serializers.TokenResponseSerializer'
'PASSWORDLESS_AUTH_TOKEN_SERIALIZER': 'drfpasswordless.serializers.TokenResponseSerializer',

# A dictionary of demo user's primary key mapped to their static pin
'PASSWORDLESS_DEMO_USERS': {},

# configurable function for sending email
'PASSWORDLESS_EMAIL_CALLBACK': 'drfpasswordless.utils.send_email_with_callback_token'
'PASSWORDLESS_EMAIL_CALLBACK': 'drfpasswordless.utils.send_email_with_callback_token',

# configurable function for sending sms
'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token'
'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token',

# Token Generation Retry Count
'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3

# The length of the token to send in email or sms, maximum 6
'PASSWORDLESS_TOKEN_LENGTH': 6

}
```
Expand Down
3 changes: 2 additions & 1 deletion drfpasswordless/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.conf import settings
import string
from django.utils.crypto import get_random_string
from drfpasswordless.settings import api_settings

def generate_hex_token():
return uuid.uuid1().hex
Expand All @@ -13,7 +14,7 @@ def generate_numeric_token():
Generate a random 6 digit string of numbers.
We use this formatting to allow leading 0s.
"""
return get_random_string(length=6, allowed_chars=string.digits)
return get_random_string(length=api_settings.PASSWORDLESS_TOKEN_LENGTH, allowed_chars=string.digits)


class CallbackTokenManger(models.Manager):
Expand Down
6 changes: 2 additions & 4 deletions drfpasswordless/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.core.validators import RegexValidator
Expand Down Expand Up @@ -175,7 +175,7 @@ class AbstractBaseCallbackTokenSerializer(serializers.Serializer):

email = serializers.EmailField(required=False) # Needs to be required=false to require both.
mobile = serializers.CharField(required=False, validators=[phone_regex], max_length=17)
token = TokenField(min_length=6, max_length=6, validators=[token_age_validator])
token = TokenField(min_length=api_settings.PASSWORDLESS_TOKEN_LENGTH, max_length=api_settings.PASSWORDLESS_TOKEN_LENGTH, validators=[token_age_validator])

def validate_alias(self, attrs):
email = attrs.get('email', None)
Expand Down Expand Up @@ -299,5 +299,3 @@ class TokenResponseSerializer(serializers.Serializer):
"""
token = serializers.CharField(source='key')
key = serializers.CharField(write_only=True)


5 changes: 4 additions & 1 deletion drfpasswordless/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@
'PASSWORDLESS_SMS_CALLBACK': 'drfpasswordless.utils.send_sms_with_callback_token',

# Token Generation Retry Count
'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3
'PASSWORDLESS_TOKEN_GENERATION_ATTEMPTS': 3,

# The length of the token to send in email or sms, maximum 6
'PASSWORDLESS_TOKEN_LENGTH': 6
}

# List of settings that may be in string import notation.
Expand Down
Empty file.
111 changes: 74 additions & 37 deletions drfpasswordless/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,13 @@ def create_callback_token_for_user(user, alias_type, token_type):
alias_type_u = alias_type.upper()
to_alias_field = getattr(api_settings, f'PASSWORDLESS_USER_{alias_type_u}_FIELD_NAME')
if user.pk in api_settings.PASSWORDLESS_DEMO_USERS.keys():
token = CallbackToken.objects.filter(user=user).first()
if token:
return token
else:
return CallbackToken.objects.create(
user=user,
key=api_settings.PASSWORDLESS_DEMO_USERS[user.pk],
to_alias_type=alias_type_u,
to_alias=getattr(user, to_alias_field),
type=token_type
)
return CallbackToken.objects.create(
user=user,
key=api_settings.PASSWORDLESS_DEMO_USERS[user.pk],
to_alias_type=alias_type_u,
to_alias=getattr(user, to_alias_field),
type=token_type
)

token = CallbackToken.objects.create(user=user,
to_alias_type=alias_type_u,
Expand Down Expand Up @@ -165,49 +161,90 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs):
"""
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
# we assume success to prevent spamming SMS during testing.

# even if you have suppression on– you must provide a number if you have mobile selected.
if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER is None:
return False

return True

base_string = kwargs.get('mobile_message', api_settings.PASSWORDLESS_MOBILE_MESSAGE)
message = base_string % mobile_token.key

try:
if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER:
# We need a sending number to send properly

from twilio.rest import Client
twilio_client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])

to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
if to_number.__class__.__name__ == 'PhoneNumber':
to_number = to_number.__str__()
to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
if to_number.__class__.__name__ == 'PhoneNumber':
to_number = to_number.__str__()

twilio_client.messages.create(
body=base_string % mobile_token.key,
to=to_number,
from_=api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER
)
return True
else:
logger.debug("Failed to send token sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
if not api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER:
logger.debug("Failed to send token sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
return False
try:
provider = os.environ.get('PASSWORDLESS_SMS_PROVIDER', 'TWILIO')
SMS_PROVIDERS = {
'TWILIO': send_twilio_sms,
'GETLEAD': send_getlead_sms,
'PLIVO': send_plivo_sms,
}
try:
handler = SMS_PROVIDERS[provider]
except KeyError:
logger.debug("SMS-Provider not found.")
return False
else:
return handler(to_number, message, mobile_token.key)
except ImportError:
logger.debug("Couldn't import Twilio client. Is twilio installed?")
logger.debug("Couldn't import SMS-Provider client. Is SMS-Provider installed?")
return False
except KeyError:
logger.debug("Couldn't send SMS."
"Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?")
"Did you set your SMS-Provider account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?")
except Exception as e:
logger.debug("Failed to send token SMS to user: {}. "
"Possibly no mobile number on user object or the twilio package isn't set up yet. "
"Number entered was {}".format(user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)))
logger.debug("Failed to send token SMS to user. "
"Possibly no mobile number on user object or the SMS-Provider package isn't set up yet. "
"Number entered was {}".format(to_number))
logger.debug(e)
return False


def send_twilio_sms(to_number, message, code):
from twilio.rest import Client
twilio_client = Client(os.environ['TWILIO_ACCOUNT_SID'], os.environ['TWILIO_AUTH_TOKEN'])
twilio_client.messages.create(
body=message,
to=to_number,
from_=api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER
)
return True


def send_getlead_sms(to_number, message, code):
import requests
api_url = 'https://app.getlead.co.uk/api/push-otp'
data = dict(
username=os.environ['GETLEAD_UID'],
token=os.environ['GETLEAD_TOKEN'],
sender=api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER,
to=to_number,
otp=code,
purpose='login',
company=os.environ['GETLEAD_COMPANY_NAME'],
priority=4
)

response = requests.post(api_url, data=data).json()
if response.get("status") == "Fail":
return False
return True


def send_plivo_sms(to_number, message, code):
from plivo import RestClient
client = RestClient(os.environ["PLIVO_AUTH_ID"], os.environ["PLIVO_AUTH_TOKEN"])
client.messages.create(
src=api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER,
dst=to_number,
text=message,
)
return True


def create_authentication_token(user):
""" Default way to create an authentication token"""
return Token.objects.get_or_create(user=user)
2 changes: 1 addition & 1 deletion tests/test_verification.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from rest_framework import status
from rest_framework.authtoken.models import Token
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from rest_framework.test import APITestCase
from django.contrib.auth import get_user_model
from django.urls import reverse
Expand Down