jazzband / django-oauth-toolkit

OAuth2 goodies for the Djangonauts!
https://django-oauth-toolkit.readthedocs.io
Other
3.14k stars 794 forks source link

oauth2_provider_accesstoken_source_refresh_token_id_key constraint violation #722

Open ghost opened 5 years ago

ghost commented 5 years ago

Stack trace:

UniqueViolation: duplicate key value violates unique constraint "oauth2_provider_accesstoken_source_refresh_token_id_key"
DETAIL:  Key (source_refresh_token_id)=(155) already exists.

  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

IntegrityError: duplicate key value violates unique constraint "oauth2_provider_accesstoken_source_refresh_token_id_key"
DETAIL:  Key (source_refresh_token_id)=(155) already exists.

  File "django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "django/views/generic/base.py", line 97, in dispatch
    return handler(request, *args, **kwargs)
  File "appname/apps/auth/views.py", line 45, in post
    return super().post(request, *args, **kwargs)
  File "django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "django/views/decorators/debug.py", line 76, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "oauth2_provider/views/base.py", line 206, in post
    url, headers, body, status = self.create_token_response(request)
  File "oauth2_provider/views/mixins.py", line 123, in create_token_response
    return core.create_token_response(request)
  File "oauth2_provider/oauth2_backends.py", line 138, in create_token_response
    headers, extra_credentials)
  File "oauthlib/oauth2/rfc6749/endpoints/base.py", line 87, in wrapper
    return f(endpoint, uri, *args, **kwargs)
  File "oauthlib/oauth2/rfc6749/endpoints/token.py", line 117, in create_token_response
    request, self.default_token_type)
  File "oauthlib/oauth2/rfc6749/grant_types/refresh_token.py", line 72, in create_token_response
    self.request_validator.save_token(token, request)
  File "oauthlib/oauth2/rfc6749/request_validator.py", line 334, in save_token
    return self.save_bearer_token(token, request, *args, **kwargs)
  File "python3.6/contextlib.py", line 52, in inner
    return func(*args, **kwds)
  File "oauth2_provider/oauth2_validators.py", line 531, in save_bearer_token
    source_refresh_token=refresh_token_instance,
  File "oauth2_provider/oauth2_validators.py", line 563, in _create_access_token
    access_token.save()
  File "django/db/models/base.py", line 741, in save
    force_update=force_update, update_fields=update_fields)
  File "django/db/models/base.py", line 779, in save_base
    force_update, using, update_fields,
  File "django/db/models/base.py", line 870, in _save_table
    result = self._do_insert(cls._base_manager, using, fields, update_pk, raw)
  File "django/db/models/base.py", line 908, in _do_insert
    using=using, raw=raw)
  File "django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "django/db/models/query.py", line 1186, in _insert
    return query.get_compiler(using=using).execute_sql(return_id)
  File "django/db/models/sql/compiler.py", line 1335, in execute_sql
    cursor.execute(sql, params)
  File "raven/contrib/django/client.py", line 127, in execute
    return real_execute(self, sql, params)
  File "django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "django/db/backends/utils.py", line 76, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "django/db/utils.py", line 89, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

Packages:

stefanw commented 5 years ago

I also see this error in production. Here is the code branch that triggers the IntegrityError (in v1.2).

I don't know if this is triggered by a race condition (the atomic decorator on the method would not prevent this) or if this is a logic bug in save_bearer_token.

When this triggers in save_bearer_token the state of variables is as follows:

So it looks like a race condition happens at this line between reading previous_access_token and creating a new access token.

So maybe this block should be wrapped in an inner transaction with IntegrityError handling?

try:
    with transaction.atomic():
        access_token = self._create_access_token(
            expires,
            request,
            token,
            source_refresh_token=refresh_token_instance,
        )

        refresh_token = RefreshToken(
            user=request.user,
            token=refresh_token_code,
            application=request.client,
            access_token=access_token
        )
        refresh_token.save()
except IntegrityError:
    # Try getting the apparenly already created new access_token from the database
M-Mortaz commented 5 years ago

Hi, i'm new in django and i don't know exactly what is going on with this block code . but i have this error in my production too(small project for school) . my question is: 1-is this error dangerous? 2-what happen to my client when this error happen?(they can't login again or ...) i can't anywhere to ask my question unless here and if this place isn't appropriate please forgive me.thank you

stefanw commented 5 years ago

638 is related and some code has landed in master that looks like it fixes this.

adamchainz commented 5 years ago

I was just debugging this myself. #649 is indeed the fix that landed in master that passes my parallel request test:

$ parallel -N0 http POST http://localhost:8000/o/token/ grant_type=refresh_token client_id=development refresh_token=vpkGCDH80hEU5usZek0seQFNApkPr5 --ignore-stdin --form ::: {1..5}
adamchainz commented 5 years ago

If the maintainers could release to PyPI that would be awesome!

oliamb commented 4 years ago

Is it possibly solved by https://github.com/jazzband/django-oauth-toolkit/pull/810? It is part of 1.3.1 release

adamchainz commented 4 years ago

649 was the fix, and has been released, so this can be closed.