import base64
import re
import time
import uuid
from datetime import datetime
import jwt
from django.contrib.auth.models import User
from django.middleware import cache
from django.urls import resolve
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
import django_river_ml.utils as utils
from django_river_ml import settings
[docs]
def is_authenticated(request):
    """
    Function to check if a request is authenticated.
    Returns a boolean to indicate if the user is authenticated, and a response with
    the challenge if not.
    request (requests.Request)    : the Request object to inspect
    """
    # Derive the view name from the request PATH_INFO
    func, _, _ = resolve(request.META["PATH_INFO"])
    view_name = "%s.%s" % (func.__module__, func.__name__)
    # If authentication is disabled, return the original view
    if settings.DISABLE_AUTHENTICATION or view_name not in settings.AUTHENTICATED_VIEWS:
        return True, None, None
    # Case 2: Already has a jwt valid token
    is_valid, user = validate_jwt(request)
    if is_valid:
        return True, None, user
    # Case 3: False and response will return request for auth
    user = get_user(request)
    if not user:
        headers = {"Www-Authenticate": get_challenge(request)}
        return False, Response(status=401, headers=headers), user
    # Denied for any other reason
    return False, Response(status=403), user 
[docs]
def generate_jwt(username):
    """Given a username generate a jwt
    token to return to the user with a default expiration of 10 minutes.
    username (str)  : the user's username to add under "sub"
    """
    # The jti expires after TOKEN_EXPIRES_SECONDS
    issued_at = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
    filecache = cache.caches["django_river_ml"]
    jti = str(uuid.uuid4())
    filecache.set(jti, "good", timeout=settings.TOKEN_EXPIRES_SECONDS)
    now = int(time.time())
    expires_at = now + settings.TOKEN_EXPIRES_SECONDS
    # import jwt and generate token
    # https://tools.ietf.org/html/rfc7519#section-4.1.5
    payload = {
        "sub": username,
        "exp": expires_at,
        "nbf": now,
        "iat": now,
        "jti": jti,
    }
    token = jwt.encode(payload, settings.JWT_SERVER_SECRET, algorithm="HS256")
    if isinstance(token, bytes):
        token = token.decode("utf-8")
    return {
        "token": token,
        "expires_in": settings.TOKEN_EXPIRES_SECONDS,
        "issued_at": issued_at,
    } 
[docs]
def validate_jwt(request):
    """
    Given a jwt token, decode and validate
    request (requests.Request)    : the Request object to inspect
    """
    header = request.META.get("HTTP_AUTHORIZATION", "")
    if re.search("bearer", header, re.IGNORECASE):
        encoded = re.sub("bearer", "", header, flags=re.IGNORECASE).strip()
        # Any reason not valid will issue an error here
        try:
            decoded = jwt.decode(
                encoded, settings.JWT_SERVER_SECRET, algorithms=["HS256"]
            )
        except Exception as exc:
            print("jwt could no be decoded, %s" % exc)
            return False, None
        # Ensure that the jti is still valid
        filecache = cache.caches["django_river_ml"]
        if not filecache.get(decoded.get("jti")) == "good":
            print("Filecache with jti not found.")
            return False, None
        # The user must exist
        try:
            user = User.objects.get(username=decoded.get("sub"))
            return True, user
        except User.DoesNotExist:
            print("Username %s not found" % decoded.get("sub"))
            return False, None
    return False, None 
[docs]
def get_user(request):
    """Given a request, read the Authorization header to get the base64 encoded
    username and token (password) which is a basic auth. If we return the user
    object, the user is successfully authenticated. Otherwise, return None.
    and the calling function should return Forbidden status.
    request (requests.Request)    : the Request object to inspect
    """
    header = request.META.get("HTTP_AUTHORIZATION", "")
    if re.search("basic", header, re.IGNORECASE):
        encoded = re.sub("basic", "", header, flags=re.IGNORECASE).strip()
        decoded = base64.b64decode(encoded).decode("utf-8")
        username, token = decoded.split(":", 1)
        try:
            token = Token.objects.get(key=token)
            if token.user.username == username:
                return token.user
        except Exception:
            pass 
[docs]
def get_token(request):
    """The same as validate_token, but return the token object to check the
    associated user.
    request (requests.Request)    : the Request object to inspect
    """
    # Coming from HTTP, look for authorization as bearer token
    token = request.META.get("HTTP_AUTHORIZATION")
    if token:
        try:
            return Token.objects.get(key=token.replace("BEARER", "").strip())
        except Token.DoesNotExist:
            pass
    # Next attempt - try to get token via user session
    elif request.user.is_authenticated and not request.user.is_anonymous:
        try:
            return Token.objects.get(user=request.user)
        except Token.DoesNotExist:
            pass 
[docs]
def get_challenge(request):
    """Given an unauthenticated request, return a challenge in
    the Www-Authenticate header
    request (requests.Request): the Request object to inspect
    """
    DOMAIN_NAME = utils.get_server(request)
    auth_server = "%s/%s/auth/token" % (DOMAIN_NAME, settings.URL_PREFIX)
    return 'realm="%s",service="%s"' % (
        auth_server,
        DOMAIN_NAME,
    )