custom.py 3.7 KB
Newer Older
David Seaward's avatar
David Seaward committed
1 2
import logging

3
from django.conf import settings
David Seaward's avatar
David Seaward committed
4
from django.contrib.auth.password_validation import MinimumLengthValidator as BaseValidator
5
from django.utils.translation import ugettext_lazy as _
David Seaward's avatar
David Seaward committed
6
from django_auth_ldap.backend import LDAPBackend as BaseBackend
7 8
from woocommerce import API as WOOCOMMERCE_API

9
from .models import User, UsernameValidator
David Seaward's avatar
David Seaward committed
10 11 12 13

log = logging.getLogger(__name__)


14
class AuthenticationBackend(BaseBackend):
David Seaward's avatar
David Seaward committed
15 16

    def __init__(self, *args, **kwargs):
17
        super(AuthenticationBackend, self).__init__(*args, **kwargs)
David Seaward's avatar
David Seaward committed
18

19 20 21 22 23 24 25 26
    def is_valid_jwt(self, username=None, password=None):

        is_valid = False

        try:

            jwt_wcapi = WOOCOMMERCE_API(
                url=settings.WOO_URL,
27 28
                consumer_key=settings.WOO_CONSUMER_KEY,
                consumer_secret=settings.WOO_CONSUMER_SECRET,
29
                wp_api=True,
30 31
                version="jwt-auth/v1",  # required for JWT Authentication
                query_string_auth=True,  # required for OAuth over HTTPS
32 33 34
            )

            jwt_response = jwt_wcapi.post("token", {"username": username, "password": password})
35

36 37 38 39 40 41 42 43
            jwt_status = jwt_response.status_code
            jwt_token = jwt_response.json().get("token", None)
            jwt_code = jwt_response.json().get("code", None)

            known_codes = ["[jwt_auth] incorrect_password", "[jwt_auth] invalid_username"]

            if jwt_status == 200 and jwt_token is not None:
                is_valid = True
David Seaward's avatar
David Seaward committed
44
            elif jwt_status == 403 and jwt_code in known_codes:
45 46 47 48
                # recognised authentication failure
                is_valid = False
            else:
                # raise exception for an unrecognised failure
49
                raise Exception("Unrecognised JWT response: %s" % (jwt_response.json(),))
50 51 52 53 54 55

        except Exception as e:
            logging.exception("JWT authentication failed with an unrecognised error: %s" % (e,))
        finally:
            return is_valid

56
    def authenticate(self, request=None, username=None, password=None, **kwargs):
57
        user_model = User
58
        normalized_username = user_model.normalize_username(username)
59

60 61 62 63 64 65 66
        # first, validate username (even if it exists, username must be valid)

        if not settings.DEBUG_SKIP_VALIDATE_ON_AUTHENTICATION:
            validator = UsernameValidator()
            validator(username)

        # second, attempt LDAP authentication (with early exit on success)
67 68 69 70 71

        user = super(AuthenticationBackend, self).authenticate(request, normalized_username, password, **kwargs)

        if user is not None:
            return user
72

73
        # third, attempt WooCommerce/JWT authentication
74 75 76
        # (if successful, create and return LDAP user, otherwise return None)

        if self.is_valid_jwt(normalized_username, password):
77

78 79 80 81 82 83 84 85 86
            # try to get a preexisting user object
            # otherwise create a new one

            try:
                user = user_model.objects.get(username=username)
            except user_model.DoesNotExist:
                user = user_model(username=username, email=None)

            # update/set user details
David Seaward's avatar
David Seaward committed
87
            user.email = user.get_identity()
88 89 90
            user.set_password(password)
            user.save()

91 92 93 94
            return super(AuthenticationBackend, self).authenticate(request, normalized_username, password, **kwargs)
        else:
            return None

95

96 97
class PassphraseValidator(BaseValidator):
    # TODO: bundle in all the other validators from django.contrib.auth.password_validation
David Seaward's avatar
David Seaward committed
98

99
    def __init__(self, min_length=15, *args, **kwargs):
100
        super(PassphraseValidator, self).__init__(min_length, *args, **kwargs)
David Seaward's avatar
David Seaward committed
101 102

    def get_help_text(self):
103
        return _("A good passphrase is made of at least three long words.")