社交身份验证(DRF + Djoser),“non_field_errors”:[“已提供无效状态。”] 在带有状态和代码的 POST 请求之后

问题描述 投票:0回答:1

我正在遵循这个 视频教程 并尝试使用

DRF
djoser
React
来实现 Google 社交身份验证。

导致错误的步骤:

  1. 发送

    GET
    请求:

    http://localhost:8000/auth/o/google-oauth2/?redirect_uri=http://localhost:8000
    

响应看起来像这样(我稍微修改了响应,因为我不确定这个网址是否可以安全共享)

{
    "authorization_url": "https://accounts.google.com/o/oauth2/auth?client_id=836198290956-fe0ilujf6e23l882oumgkufi8qm6fg3m.apps.googleusercontent.com&redirect_uri=http://localhost:8000&state=eNwMFCmEplYgbUTTP9nnrQ6MduAPxzDY&response_type=code&scope=https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/userinfo.profile+openid+openid+email+profile"
}
  1. 在浏览器中输入响应后,它会将我重定向到 google
    sign in
    页面。然后我选择我的帐户,然后按继续。我现在通过此网址重定向到
    localhost:8000
    http://localhost:8000/?state=eNwMFCmEplYgbUTTP9nnrQ6MduAPxzDY&code=4%2F0AcvDMrB6f3ZQuTD563Vxriu2n0VHmLEOHnDRqC6jD5BRm068jj2tyExxfZZJDFLAtcwYLg&scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&authuser=0&hd=circle.help&prompt=consent
    

我的问题出现在这里

  1. 我从此 url 获取状态和代码参数,并使用 Postman 发出 POST 请求(没有任何正文,只有 url,但我还将 Content-Type 标头设置为 application/x-www-form-urlencoded)为此网址:
localhost:8000/auth/o/google-oauth2/?state=eNwMFCmEplYgbUTTP9nnrQ6MduAPxzDY&code=4%2F0AcvDMrB6f3ZQuTD563Vxriu2n0VHmLEOHnDRqC6jD5BRm068jj2tyExxfZZJDFLAtcwYLg

但作为回应,我收到了这个(这是我的问题):

{
    "non_field_errors": [
        "Invalid state has been provided."
    ]
}

我尝试调试一下,发现原因就在这个模块:

# venv/lib/python3.11/site-packages/social_core/backends/oauth.py

class OAuthAuth(BaseAuth):
    ...
    # Other methods
    def validate_state(self):
        """Validate state value. Raises exception on error, returns state
        value if valid."""
        if not self.STATE_PARAMETER and not self.REDIRECT_STATE:
            return None
        state = self.get_session_state()
        request_state = self.get_request_state()
        if not request_state:
            raise AuthMissingParameter(self, "state")
        elif not state:
            raise AuthStateMissing(self, "state")
        elif not constant_time_compare(request_state, state):
            raise AuthStateForbidden(self)
        else:
            return state
    ...
    # Other methods

就我而言,在 OAuthAuth.validate_state() 方法中, state 变量与 request_state 变量不同,而 request_state (来自 OAuthAuth.validate_state())与状态(来自 url)相同
在上面两个 url 中,但是 state (来自 OAuthAuth.validate_state())完全不同。我不知道它从哪里来,为什么

self.get_request_state()

返回与 url 中不同的状态?也许我做错了什么,我应该在 Postman 中传递一些 cookie?

这是我安装的软件包的列表:

asgiref==3.8.1
certifi==2024.7.4
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==42.0.8
defusedxml==0.8.0rc2
Django==5.0.7
django-filter==24.2
django-templated-mail==1.1.1
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
djoser==2.2.3
idna==3.7
Markdown==3.6
oauthlib==3.2.2
psycopg==3.2.1
psycopg2-binary==2.9.9
pycparser==2.22
PyJWT==2.8.0
python3-openid==3.2.0
requests==2.32.3
requests-oauthlib==2.0.0
social-auth-app-django==5.4.2
social-auth-core==4.5.4
sqlparse==0.5.1
typing_extensions==4.12.2
urllib3==2.2.2

这是我的设置.py

SECRET_KEY = "django_secret_key"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []

# Application definition

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # Third party apps
    "rest_framework",
    "djoser",
    "social_django",
    "rest_framework_simplejwt",
    "rest_framework_simplejwt.token_blacklist",  # more smooth with migration

    # My apps
    "accounts"
]

MIDDLEWARE = [
    "social_django.middleware.SocialAuthExceptionMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

ROOT_URLCONF = "djSocAuth.urls"

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        # "DIRS": [BASE_DIR / 'templates'],  # os.path.join(BASE_DIR, "build")
        "DIRS": [os.path.join(BASE_DIR, "build")],  # os.path.join(BASE_DIR, "build")
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
                "social_django.context_processors.backends",
                "social_django.context_processors.login_redirect"
            ],
        },
    },
]

REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": {
        "rest_framework.permissions.IsAuthenticated"
    },
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

AUTHENTICATION_BACKENDS = (
    "social_core.backends.google.GoogleOAuth2",  # Enable using google OAuth 2
    "django.contrib.auth.backends.ModelBackend",  # Enable logining via email and password
)

SIMPLE_JWT = {
    'AUTH_HEADER_TYPES': ('JWT',),
    "ACCESS_TOKEN_LIFETIME": timedelta(days=10000),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=10000),
    "AUTH_TOKEN_CLASSES": (
        "rest_framework_simplejwt.tokens.AccessToken",
    )
}
from djoser.social.token.jwt import TokenStrategy

DJOSER = {
    "LOGIN_FIELD": "email",
    "USER_CREATE_PASSWORD_RETYPE": True,  # confirm password field
    "USERNAME_CHANGED_EMAIL_CONFIRMATION": True,  # whenever username is changed - confirmation email is sent
    "PASSWORD_CHANGED_EMAIL_CONFIRMATION": True,
    "SET_USERNAME_RETYPE": True,
    "SET_PASSWORD_RETYPE": True,
    "PASSWORD_RESET_CONFIRM_URL": "password/reset/confirm/{uid}/{token}",
    "USERNAME_RESET_CONFIRM_URL": "email/reset/confirm/{uid}/{token}",
    "ACTIVATION_URL": "activate/{uid}/{token}",  # this should be on frontend, --> auth/users/activation/
    "SEND_ACTIVATION_EMAIL": True,
    "SOCIAL_AUTH_TOKEN_STRATEGY": "djoser.social.token.jwt.TokenStrategy",
    "SOCIAL_AUTH_ALLOWED_REDIRECT_URIS": [
        "http://localhost:8000"
    ],
    "SERIALIZERS": {
        "user_create": "accounts.serializers.UserCreateSerializer",
        "user": "accounts.serializers.UserCreateSerializer",
        "current_user": "accounts.serializers.UserCreateSerializer",
        "user_delete": "djoser.serializers.UserDeleteSerializer",

    }
}

# Google config
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = "my_key"
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = "my_secret"
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
    "https://www.googleapis.com/auth/userinfo.email",  # retrieve email
    "https://www.googleapis.com/auth/userinfo.profile",
    "openid"
]  # when people go to sign up and sing in, they are going to retrieve some data from their accounts
SOCIAL_AUTH_GOOGLE_OAUTH2_EXTRA_DATA = ["first_name", "last_name"]

AUTH_USER_MODEL = "accounts.UserAccount"
...

我发现了一堆类似的问题,但没有完全解释这个错误的原因是什么。

django oauth djoser
1个回答
0
投票

您遇到的问题是,当您进行重定向时,状态不会持久。状态保存在

session
键下的
f'_state_{self.name}_{state}'
中。
self.name
值并不重要。但
state
是。在验证您的凭据后进行重定向时,
state
值会作为请求参数从 Google 返回。该值与会话或缓存中的值进行比较。

所以基本上,当您调用后端时,您应该保留

session
。如果您使用浏览器,最好使用隐身模式。如果您还想使用邮递员,请在创建请求之前将会话复制到请求。

© www.soinside.com 2019 - 2024. All rights reserved.