You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django集成drf-social-oauth2调用convert-token报AccessDeniedError

环境与问题背景
  • 运行环境:Python 3.9 + Django 4.0,项目已安装drf-social-oauth2依赖
  • 原有实现:基于已废弃的gapi OAuth2实现谷歌登录,功能运行正常,前端可获取access_token传递给后端完成身份校验
  • 切换原因:收到gapi服务即将停用的控制台警告,更换为新版Google Identity Service(GSI Client)
  • 方案差异:新版GSI Client默认不再返回原有格式的access_token,直接返回credential字段承载的JWT凭证,需要解码后才能获取用户详情
  • 报错现象:将GSI返回的凭证提交到/auth/convert-token/端点时,接口返回如下错误:
AccessDeniedError at /auth/convert-token (access_denied) Your credentials aren't allowed <oauthlib.Request SANITIZED>

已确认所有配置参数填写无误,已通过VSCode Thunder Client完成接口测试,问题排查受阻。

问题根因

drf-social-oauth2默认内置的谷歌OAuth2认证逻辑,仅支持接收谷歌返回的access_token,携带该token请求谷歌官方用户信息接口完成身份校验。新版GSI Client默认返回的credential是谷歌签发的ID Token(JWT格式),直接把这个值传给/auth/convert-token/端点时,后端会把它当成普通access_token请求谷歌用户信息接口,谷歌接口无法识别该类凭证,直接返回权限拒绝,最终抛出上述错误。

可行解决方案

二选一即可,优先推荐方案1,改动量最小。

方案1:前端调整GSI配置,返回标准access_token

GSI Client并非只能返回JWT格式的ID Token,可通过初始化配置调整为OAuth2授权模式,直接获取和旧版gapi格式一致的access_token,拿到后按原有逻辑传给/auth/convert-token/端点即可,无需修改后端代码。
配置要点:

  • 初始化时不要使用一键登录默认返回credential的配置,改用token_client方式初始化
  • 授权scope配置包含openid email profile
  • 最终取响应中的access_token字段传递给后端,不要传递credential字段

方案2:自定义谷歌认证后端,兼容ID Token校验

如果不想调整前端逻辑,可自定义drf-social-oauth2的谷歌认证后端,增加JWT格式ID Token的校验逻辑,跳过无效的access_token请求步骤:

  1. 安装谷歌官方JWT校验依赖:
    pip install google-auth
  2. 在项目认证逻辑目录下新建oauth_backends.py,写入自定义后端代码:
from google.oauth2 import id_token
from google.auth.transport import requests
from drf_social_oauth2.backends import GoogleOAuth2
from django.conf import settings

class GoogleIdTokenOAuth2(GoogleOAuth2):
    def get_user_details(self, response):
        # 兼容原有access_token返回的用户数据格式
        if 'sub' not in response:
            return super().get_user_details(response)
        # 适配ID Token解析出的用户字段
        return {
            'username': response.get('email'),
            'email': response.get('email'),
            'first_name': response.get('given_name', ''),
            'last_name': response.get('family_name', ''),
            'fullname': response.get('name', ''),
        }

    def get_user_id(self, details, response):
        if 'sub' in response:
            return response['sub']
        return super().get_user_id(details, response)

    def request_user_data(self, token, *args, **kwargs):
        # 识别JWT格式的ID Token
        if len(token.split('.')) == 3:
            try:
                # 校验JWT签名、过期时间、受众
                id_info = id_token.verify_oauth2_token(
                    token,
                    requests.Request(),
                    settings.GOOGLE_OAUTH2_CLIENT_ID
                )
                # 校验token签发方为谷歌
                if id_info['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
                    raise ValueError('Invalid token issuer')
                return id_info
            except Exception:
                pass
        # 非ID Token走原有access_token认证逻辑
        return super().request_user_data(token, *args, **kwargs)
  1. 修改项目settings.py中的认证后端配置,替换默认的谷歌认证后端:
AUTHENTICATION_BACKENDS = (
    # 替换为自定义的谷歌认证后端,注意替换为实际的项目路径
    'your_project.xxx.oauth_backends.GoogleIdTokenOAuth2',
    # 保留原有其他认证后端
    'django.contrib.auth.backends.ModelBackend',
)
  1. 前端传参时,token字段直接传GSI返回的credential值即可,其余参数保持原有规则不变。
排查注意事项
  • 确认谷歌云控制台的OAuth2客户端配置,已将当前前端域名加入「已授权的JavaScript来源」,后端回调地址加入「已授权的重定向URI」
  • 安全提示:使用方案2时必须通过谷歌官方库校验JWT的签名、受众、签发方、过期时间,禁止直接无校验解码JWT提取用户信息,否则会存在任意用户身份伪造的高危安全漏洞
  • 接口请求时grant_type字段固定为convert_token,client_id、client_secret需和django-oauth-toolkit中创建的应用参数一致,backend字段固定为google

内容的提问来源于stack exchange,提问作者Ernest Alidza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 17:15:44