2020年4月3日 星期五

Customize your own Django authentication decouple with the models.py

For many systems, authentication mechanisms may already exist, especially for systems that are authenticated by an OAuth server.
In this article, I want to customize Django authentication without creating any user model...

Step 1.
Customize your own authentication middleware.

auth.middleware.py

from django.conf import settings
from auth.backend import CamelotBackend
from auth.user import AnonymousUser


class CamelotMiddleware:

    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.
        self.process_request(request)
        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.
        return response

    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE%s setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'auth.middleware.CamelotMiddleware'.") % (
                "_CLASSES" if settings.MIDDLEWARE is None else "")
        token = request.session.get('oauth_token')
        if token:
            access_token = token.get('access_token')
            request.user = CamelotBackend.authenticate(request=request,
                                                       token=access_token)
        else:
            request.user = AnonymousUser()
        return None

Step 2.
Customize your own authentication backend.
For user authentication, you need to write your own backend class and implementing the methods for authenticate and get_user

auth.backend.py

class CustomizedAuthBackend:

    @staticmethod
    def authenticate(request, token=None):
        if token:
            return CamelotBackend.get_user(request=request, token=token)
        else:
            return AnonymousUser()

    @staticmethod
    def get_user(request, token=None):
        if token:
            camelot_oauth = OAuthHelper()
            try:
                user_info = camelot_oauth.get_user_info_by_oauth_token(
                    oauth_token=token)
                return OAuthUser(
                    account_id=user_info.get('account_id'),
                    username=user_info.get('first_name'),
                    email=user_info.get('email'))
            except (BusinessException, Exception):
                #  clear session
                try:
                    del request.session['oauth_token']
                except (
                        KeyError,
                        Exception,
                ):
                    return AnonymousUser()
        else:
            return AnonymousUser()

    @staticmethod
    def logout(request):
        """
        Remove the authenticated user's ID from the request and flush their session
        data.
        """
        # Dispatch the signal before the user is logged out so the receivers have a
        # chance to find out *who* logged out.
        user = getattr(request, 'user', None)
        if not getattr(user, 'is_authenticated', True):
            user = None
        request.session.flush()
        if hasattr(request, 'user'):
            from auth.user import AnonymousUser
            request.user = AnonymousUser()

Step 3.
Replace the original authentication model.

auth.user.py

class OAuthUser(object):
    _username = str()
    _account_id = str()
    _email = str()

    def __init__(self, account_id, username, email):
        self._account_id = account_id
        self._username = username
        self._email = email

    def __eq__(self, other):
        return isinstance(other, self.__class__)

    @property
    def is_anonymous(self):
        return False

    @property
    def is_authenticated(self):
        return True

    def get_username(self):
        return self._username


class AnonymousUser:
    _username = str()

    def __str__(self):
        return 'AnonymousUser'

    def __hash__(self):
        return 1    # instances always return the same hash value

    @property
    def is_anonymous(self):
        return True

    @property
    def is_authenticated(self):
        return False

    def get_username(self):
        return self._username

Step 4.
Write your own decorator to check if the user is logged in.

auth.decorators.py
from functools import wraps
from django.conf import settings
from django.shortcuts import redirect, reverse


def member_required(func):
    @wraps(func)
    def decorator(request, *args, **kwargs):
        assert hasattr(request, 'user'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE%s setting to insert "
            "'auth.middleware.CamelotMiddleware'") % (
                "_CLASSES" if settings.MIDDLEWARE is None else "")
        if request.user and request.user.is_authenticated:
            return func(request, *args, **kwargs)
        else:
            return redirect(reverse('storefront:login'))
    return decorator

Step 5.
Configure the settings.py
INSTALLED_APPS = [
    'myapp.MyAppConfig',
    #  remove the django.contrib.auth
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    #  your customized middleware
    'auth.middleware.CamelotMiddleware', 
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

#  remember to replaced it by your own user model
AUTH_USER_MODEL = 'auth.user.OAuthUser'

#  rember to replaced it by your own authentication backend
AUTHENTICATION_BACKENDS = ['auth.backend.CamelotBackend']

SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"

Step 6.
Check your member in views.py.
Implement the OAuth client by "requests-oauthlib"
from requests_oauthlib import OAuth2Session
from auth.decorators import member_required

@member_required
def refresh_token(request):
    my_auth = OAuth2Session(client_id, token=request.session['oauth_token'])
    token = my_auth.refresh_token(token_url=token_url)
    request.session['oauth_token'] = token

    return JsonResponse(token)

Source code of the views.py
from Camelot import settings
from django.http import JsonResponse
from django.shortcuts import redirect, reverse
from requests_oauthlib import OAuth2Session
from auth.backend import CamelotBackend
from apps.utility.helpers.profile_helper import OAuthHelper
from auth.decorators import member_required

client_id = settings.env.str('OAUTH_CLIENT_ID')
client_secret = settings.env.str('OAUTH_CLIENT_SECRET_KEY')
authorization_login_url = settings.env.str('OAUTH_AUTHORIZATION_URL')
token_url = settings.env.str('OAUTH_TOKEN_URL')
authorization_logout_url = settings.env.str('OAUTH_TOKEN_REVOKE')
redirect_path = 'callback'


def login(request):
    redirect_url = f'{request.scheme}://{request.get_host()}/{redirect_path}'
    myzyxel = OAuth2Session(client_id, redirect_uri=redirect_url)
    authorization_url, state = myzyxel.authorization_url(
        authorization_login_url)

    request.session['oauth_state'] = state

    return redirect(authorization_url)


def callback(request):
    """ Step 3: Retrieving an access token.

    The user has been redirected back from the provider to your registered
    callback URL. With this redirection comes an authorization code included
    in the redirect URL. We will use that to obtain an access token.
    """

    redirect_url = f'{request.scheme}://{request.get_host()}/{redirect_path}'
    myzyxel = OAuth2Session(client_id,
                            state=request.session['oauth_state'],
                            redirect_uri=redirect_url)
    token = myzyxel.fetch_token(token_url,
                                client_secret=client_secret,
                                authorization_response=request.get_raw_uri(),
                                include_client_id=True)

    # At this point you can fetch protected resources but lets save
    # the token and show how this is done from a persisted token
    # in /profile.
    request.session['oauth_token'] = token

    return redirect(reverse('storefront:profile'))


@member_required
def profile(request):
    """Fetching a protected resource using an OAuth 2 token.
    """
    access_token = request.session.get('oauth_token').get('access_token')
    camelot_oauth = OAuthHelper()
    user_info = camelot_oauth.get_user_info_by_oauth_token(
        oauth_token=access_token)

    return JsonResponse(user_info)


@member_required
def refresh_token(request):
    myzyxel = OAuth2Session(client_id, token=request.session['oauth_token'])
    token = myzyxel.refresh_token(token_url=token_url)
    request.session['oauth_token'] = token

    return JsonResponse(token)


def logout(request):
    redirect_url = f'{request.scheme}://{request.get_host()}/'
    current_token = request.session.get('oauth_token')
    logout_url = f"{authorization_logout_url}"
    if current_token:
        access_token = current_token.get('access_token')
        logout_url = f"{authorization_logout_url}?access_token={access_token}&logout_redirect_uri={redirect_url}"
        try:
            del request.session['oauth_token']
        except (
                KeyError,
                Exception,
        ):
            pass
    CamelotBackend.logout(request=request)
    return redirect(logout_url)

All installed packages
asgiref==3.2.7
Django==3.0.5
pytz==2019.3
sqlparse==0.3.1
django-environ==0.4.5
django-sslserver==0.22
requests==2.23.0
requests-oauthlib==1.3.0
oauthlib==3.1.0

沒有留言: