I am following this video tutorial and trying to implement Google social authentication using DRF
, djoser
and React
.
Steps which leads to an error:
GET
request:http://localhost:8000/auth/o/google-oauth2/?redirect_uri=http://localhost:8000
The response looks like this (I modified the response slightly because I wasn't sure hat this url is safe to share)
{
"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"
}
sign in
page. Then I select my account, then I press continue. I am now redirected to localhost:8000
with this url Lhttp://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
My problem shows up here.
localhost:8000/auth/o/google-oauth2/?state=eNwMFCmEplYgbUTTP9nnrQ6MduAPxzDY&code=4%2F0AcvDMrB6f3ZQuTD563Vxriu2n0VHmLEOHnDRqC6jD5BRm068jj2tyExxfZZJDFLAtcwYLg
But in response I receive this (Here is my problem):
{
"non_field_errors": [
"Invalid state has been provided."
]
}
I tried to debug it, and found out that the cause lies in this module:
# 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
In my case, in OAuthAuth.validate_state()
method, the state
variable is different from request_state
variable, while request_state
(from OAuthAuth.validate_state()
) is the same as state (from urls) in both above urls, but the state
(from OAuthAuth.validate_state()
) is totally different. I can't figure out where it comes from, why do
self.get_request_state()
returns a different state that in url? Perhaps I am doing something wrong and I should pass some cookie in Postman?
UPD: I tried manually assign the correct state value from url to state = self.get_session_state() and everything worked fine, I just do not know why self.get_session_state() returns incorrect value?
Here are the list of my installed packages:
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
Here is my settings.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"
...
I found a bunch of similar questions, but the did not fully explain what is the cause of this error.
The problem you're experiencing is that the state is not persistent when you do redirects. The state is saved in the session
under the key f'_state_{self.name}_{state}'
. The self.name
value is not important. But the state
one is. The state
value is returned from Google as request arguments when it redirects after your credentials are verified. Usually it is stored in oauth_token
arg. This value is compared with the one in the session or cache.
So basically, you should to keep the session
when you are making the calls to the backend. If you are using a browser best way to use incognito mode. If you also want to use the postman, make that the session is copied to a request before make it.