Django - 在处理终端节点请求之前验证 AWS Cognito 令牌是不是有效

Posted

技术标签:

【中文标题】Django - 在处理终端节点请求之前验证 AWS Cognito 令牌是不是有效【英文标题】:Django - Verify AWS Cognito token is valid before processing endpoint requestDjango - 在处理终端节点请求之前验证 AWS Cognito 令牌是否有效 【发布时间】:2021-10-30 14:28:36 【问题描述】:

所以我在下面有这段代码来检查 AWS Cognito 令牌。我显然不想将这 6 行代码添加到每个端点。另外我不知道这是否是验证我正在做的所有事情的正确方法是期望令牌的格式为'',解析它并仅解码 JWT 令牌部分。如何验证每个请求附带的 AWS 放大令牌以确保用户正确登录。我想将此身份验证添加到 APIView 端点和 DRF api_view 装饰端点。

views.py

import django.db.utils
from rest_framework import authentication, permissions, status
from rest_framework.views import APIView
from .serializers import *
from .models import *
from rest_framework.response import Response
from django.http import JsonResponse
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from .core.api import jwt
from django.core.exceptions import ObjectDoesNotExist
class LoginView(APIView):
    def post(self, request):
        # 'Bearer z324weroko2iorjqoi=+3r3+3ij.2o2ij4='
        token = request.META['HTTP_AUTHORIZATION'].split(' ')[1]
        print(token)
    
        # TODO this should be separated out to a login module
        try:
            res = jwt.decode_cognito_jwt(token)
            return Response(status=status.Http_200_OK)
        except:
            return Response("Invalid JWT", status=status.HTTP_401_UNAUTHORIZED)

@api_view(['GET'])
@swagger_auto_schema(
    operation_description="Get Goals joined by User"
)
def get_goals_by_user(request, user_id):
    try:
        # Get goal ids of user with id
        goals_query = JoinGoal.objects.filter(
            joiner_id=user_id).values_list('goal_id', flat=True)
        goals_list = list(goals_query)
        # Get Goals using list of goals PK with descriptions and uuid
        data = list(Goal.objects.filter(
            pk__in=goals_list).values('description', 'uuid'))
        response_data = dict(goals=data)
        return JsonResponse(response_data, status=status.HTTP_200_OK)
    except JoinGoal.DoesNotExist:
        return Response(dict(error=does_not_exist_msg(JoinGoal.__name__, 'joiner_id', user_id)), status=status.HTTP_400_BAD_REQUEST)

【问题讨论】:

【参考方案1】:

如果使用djangorestframework,@bdbd 的答案将是您的最佳选择。否则,您可能需要探索以下选项:

    实现您自己的装饰器来执行身份验证。这与 @login_required 装饰器或 @user_passes_test 装饰器具有相同的想法。在为基于类的视图编写此类装饰器时,您可能对django.utils.decorators.method_decorator 感兴趣。
from functools import partial, wraps

from django.utils.decorators import method_decorator


def cognito_authenticator(view_func=None):
    if view_func is None:
        return partial(cognito_authenticator)

    @wraps(view_func)
    def wrapped_view(request, *args, **kwargs):
        # Check the cognito token from the request.
        token = request.META['HTTP_AUTHORIZATION'].split(' ')[1]

        try:
            res = jwt.decode_cognito_jwt(token)
            # Authenticate res if valid. Raise exception if not.
        except Exception:
            # Fail if invalid
            return HttpResponseForbidden("You are forbidden here!")
        else:
            # Proceed with the view if valid
            return view_func(request, *args, **kwargs)

    return wrapped_view


# We can decorate it here before the class definition but can also be done before the class method itself. See https://docs.djangoproject.com/en/3.2/topics/class-based-views/intro/#decorating-the-class
@method_decorator(
    name="post",
    decorator=[
        cognito_authenticator,
    ],
)
class SomeView(View):
    @method_decorator(cognito_authenticator)  # As explained above, this is another way of putting the decorator
    def get(self, request):
        return HttpResponse("Allowed entry!")

    def post(self, request):
        return HttpResponse("Allowed entry!")


# Or if using function-based views
@api_view(['POST'])
@cognito_authenticator
def some_view(request):
    return HttpResponse(f"Allowed entry!")
    写一个custom middleware。请注意order 很重要。与填充request.user 字段的默认AuthenticationMiddleware 相同。在您的情况下,实现 __call__ 方法,您将在其中检查 Cognito 令牌。当令牌无效时,不要通过返回例如继续查看视图。 HttpResponseForbidden 就像在这个 reference 中一样。
class CognitoAuthenticatorMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        token = request.META['HTTP_AUTHORIZATION'].split(' ')[1]

        try:
            res = jwt.decode_cognito_jwt(token)
            # Authenticate res if valid. Raise exception if not.
        except Exception:
            # Fail if invalid
            return HttpResponseForbidden("You are forbidden here!")

        # Proceed if valid
        response = self.get_response(request)

        return response
MIDDLEWARE = [
    ...
    'path.to.CognitoAuthenticatorMiddleware',
    ...
]

更新

这是使用 Option-1 运行的示例。为简单起见,settings.py 只是默认设置。

views.py

from functools import partial, wraps

from django.http import HttpResponse, HttpResponseForbidden
from django.utils.decorators import method_decorator
from django.views import View  # If using django views
from rest_framework.views import APIView  # If using djangorestframework views


def cognito_authenticator(view_func=None):
    if view_func is None:
        return partial(cognito_authenticator)

    @wraps(view_func)
    def wrapped_view(request, *args, **kwargs):
        # To simplify the authentication, we would check if there is a query parameter "name=me". If none, it is forbidden.
        if request.GET.get('name') == "me":
            return view_func(request, *args, **kwargs)
        return HttpResponseForbidden("You are forbidden here!")

    return wrapped_view


@method_decorator(  # Try this style-1
    name="get",
    decorator=[
        cognito_authenticator,
    ],
)
class SomeView(View):  # If using djangorestframework view, this can also inherit from APIView or others e.g. class SomeView(APIView):
    @method_decorator(cognito_authenticator)  # Or try this style-2
    def get(self, request):
        return HttpResponse(f"Allowed entry!")

urls.py

from django.urls import path

from my_app import views

urlpatterns = [
    path("some-view/", views.SomeView.as_view()),
]

示例运行:

$ curl http://127.0.0.1:8000/my_app/some-view/?name=notme
You are forbidden here!
$ curl http://127.0.0.1:8000/my_app/some-view/?name=me
Allowed entry!

【讨论】:

是你要修饰的类方法的名称,如“post”、“get”、“put”等。看看怎么用here。正如您在该参考资料中看到的那样,您也可以直接将其放在方法上而不是类上。 我更新了答案,你能检查一下它现在是否有效吗? 附带说明 - 您的示例代码并没有真正验证令牌。它只是解码它。它是否有效是另一回事。由于 Cognito 是 OIDC 提供者,请查看 Auth0 如何处理 JWT 令牌验证:auth0.com/docs/quickstart/backend/python/…(它也是 OIDC)。您将拥有 Cognito 的主机 UI 域,而不是 AUTH0_DOMAIN @user8714896 我用最低配置的示例运行更新了我的答案。您可能有兴趣在此基础上构建并查看您会出错的部分。另外,感谢 AleksanderWons 的反馈,我在示例代码中阐明必须执行进一步的身份验证。 @user8714896 我更新了我的答案,包括您将如何在基于函数的视图上执行此操作。总之,将@method_decorator(cognito_authenticator) 替换为@cognito_authenticator。我试过了,它对我有用。【参考方案2】:

由于您似乎在使用DRF,您可以创建自己的身份验证类并在那里应用 JWT 的处理:

from django.contrib.auth.models import AnonymousUser
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions


class MyCustomJWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.META['HTTP_AUTHORIZATION'].split(' ')[1]
        try:
            jwt.decode_cognito_jwt(token)
        except Exception:
            raise exceptions.AuthenticationFailed('Invalid JWT')

        return AnonymousUser(), None


class MyCustomAPIView(APIView):
    authentication_classes = (MyCustomJWTAuthentication, )

或者如果你想将其应用于所有APIViews:

REST_FRAMEWORK = 
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'path.to.MyCustomJWTAuthentication',
    ),

请注意,一旦 JWT 解码失败,将不会检查其他身份验证类。如果您不想这样做,请将except 子句的处理更改为不引发AuthenticationFailed

【讨论】:

你会说我验证令牌的方式是“正确的方式”吗?至少使用 Cognito? IMO 这取决于令牌的使用方式。身份验证的目的是识别用户,但如果你只需要一个充当简单 api 密钥的令牌,那应该没问题。 如果我确实需要识别用户,例如获取他们的电子邮件或用户名,我可以从令牌中获取吗? 是否可以应用于所有端点,甚至是 DRF 端点,并且有一个不添加它的黑名单? if I do need to identify user such as get their email or username can I get that from the token? -- 是的,但这取决于 jwt 令牌的设置方式。如果您对其进行解码并找到电子邮件或某种 id 来识别用户,那么您绝对可以使用它

以上是关于Django - 在处理终端节点请求之前验证 AWS Cognito 令牌是不是有效的主要内容,如果未能解决你的问题,请参考以下文章

63.django中间件

Heroku节点+反应应用程序:未处理的锚标记身份验证请求

Django+中间件+登录验证

Django+中间件+登陆验证

django - 如何在验证之前处理/清理字段

django中视图处理请求方式(FBVCBV)