使用django-oauth-toolkit交换令牌时遇invalid_grant错误求助
Django OAuth Toolkit 持续出现invalid_grant错误排查
问题概述
使用django-oauth-toolkit实现OAuth2授权码流程时,始终返回invalid_grant错误。已确认redirect URL、client id、client secret与Django后台注册信息一致,且code verifier和code challenge的生成逻辑符合要求,但问题仍未解决。
测试脚本
import random import string import base64 import hashlib code_verifier = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randint(43, 128))) code_verifier = base64.urlsafe_b64encode(code_verifier.encode('utf-8')) code_challenge = hashlib.sha256(code_verifier).digest() code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8').replace('=', '') client_id = input("Enter client id: ") client_secret = input("Enter client secret: ") redirect_url = input("Enter callback url: ") print(f'vist http://127.0.0.1:8000/o/authorize/?response_type=code&code_challenge={code_challenge}&code_challenge_method=S256&client_id={client_id}&redirect_uri={redirect_url}') code = input("Enter code: ") print(f'enter: curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "client_id={client_id}" -d "client_secret={client_secret}" -d "code={code}" -d "code_verifier={code_verifier}" -d "redirect_uri={redirect_url}" -d "grant_type=authorization_code"')
运行输出
(venv) (base) peter@Peters-MacBook-Air Intra-Site % python api_test.py Enter client id: sJ3ijzbmdogfBZPfhkF6hOqifPuPSKKpOnN8hq1N Enter client secret: GnXNWqw7t1OwPRbOWTmDXKiuaqeJ7LRMhY9g2CWe00f3QrHLvx6aDKjGf5eF1t6QPkD1YO8BR43HNmzCjZYBW81FIjTng7QnVypzshMljEJRGTj5N7r8giwjKIiXyVng Enter callback url: https://192.168.1.44/authenticate/callback vist http://127.0.0.1:8000/o/authorize/?response_type=code&code_challenge=WiaJdysQ6aFnmkaO8yztt9kPBGUj-aqZSFVgmWYRBlU&code_challenge_method=S256&client_id=sJ3ijzbmdogfBZPfhkF6hOqifPuPSKKpOnN8hq1N&redirect_uri=https://192.168.1.44/authenticate/callback Enter code: hrLeADVEmQF8mDLKJXhAZJRQ2dElV7 enter: curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "client_id=sJ3ijzbmdogfBZPfhkF6hOqifPuPSKKpOnN8hq1N" -d "client_secret=GnXNWqw7t1OwPRbOWTmDXKiuaqeJ7LRMhY9g2CWe00f3QrHLvx6aDKjGf5eF1t6QPkD1YO8BR43HNmzCjZYBW81FIjTng7QnVypzshMljEJRGTj5N7r8giwjKIiXyVng" -d "code=hrLeADVEmQF8mDLKJXhAZJRQ2dElV7" -d "code_verifier=b'UDZGREwyR0ZVMjNCTzRBQlFaVlBUQk9TWkVRUDlCQzFSUTY1MkFNMUUzRTVCRVBNNlkwR0k4UEtGMUhaVVlTVkQ0QVowMzJUR0xLTDQ1U0VNOEtaWVcxT0tP'" -d "redirect_uri=https://192.168.1.44/authenticate/callback" -d "grant_type=authorization_code" (venv) (base) peter@Peters-MacBook-Air Intra-Site % curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "client_id=sJ3ijzbmdogfBZPfhkF6hOqifPuPSKKpOnN8hq1N" -d "client_secret=GnXNWqw7t1OwPRbOWTmDXKiuaqeJ7LRMhY9g2CWe00f3QrHLvx6aDKjGf5eF1t6QPkD1YO8BR43HNmzCjZYBW81FIjTng7QnVypzshMljEJRGTj5N7r8giwjKIiXyVng" -d "code=hrLeADVEmQF8mDLKJXhAZJRQ2dElV7" -d "code_verifier=b'UDZGREwyR0ZVMjNCTzRBQlFaVlBUQk9TWkVRUDlCQzFSUTY1MkFNMUUzRTVCRVBNNlkwR0k4UEtGMUhaVVlTVkQ0QVowMzJUR0xLTDQ1U0VNOEtaWVcxT0tP'" -d "redirect_uri=https://192.168.1.44/authenticate/callback" -d "grant_type=authorization_code" {"error": "invalid_grant"}%
配置信息
settings.py
OAUTH2_PROVIDER = { 'ALLOWED_REDIRECT_URI_SCHEMES': ["https", "intra"], "SCOPES": { "read": "Read scope", "write": "Write scope", "groups": "Access to your groups", }, } REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": [ "oauth2_provider.contrib.rest_framework.OAuth2Authentication", ], }
已安装应用
"oauth2_provider", "rest_framework",
错误原因与解决方法
核心问题:code_verifier格式错误
从运行输出的curl命令可以看到,code_verifier参数值带有b''前缀,这是因为脚本中code_verifier被处理为bytes对象,直接输出时保留了Python的bytes类型标识。服务器收到这个带前缀的字符串后,无法与生成code_challenge时使用的原始bytes值匹配,导致PKCE验证失败,返回invalid_grant。
修正脚本
修改脚本中code_verifier的生成逻辑,将bytes对象解码为字符串,并移除base64编码后的多余等号(符合PKCE规范):
import random import string import base64 import hashlib code_verifier = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randint(43, 128))) # 修正:将bytes解码为字符串,并移除末尾的等号 code_verifier = base64.urlsafe_b64encode(code_verifier.encode('utf-8')).decode('utf-8').replace('=', '') # 注意:计算code_challenge时,要使用code_verifier的字节形式 code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest() code_challenge = base64.urlsafe_b64encode(code_challenge).decode('utf-8').replace('=', '') client_id = input("Enter client id: ") client_secret = input("Enter client secret: ") redirect_url = input("Enter callback url: ") print(f'vist http://127.0.0.1:8000/o/authorize/?response_type=code&code_challenge={code_challenge}&code_challenge_method=S256&client_id={client_id}&redirect_uri={redirect_url}') code = input("Enter code: ") print(f'enter: curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: application/x-www-form-urlencoded" "http://127.0.0.1:8000/o/token/" -d "client_id={client_id}" -d "client_secret={client_secret}" -d "code={code}" -d "code_verifier={code_verifier}" -d "redirect_uri={redirect_url}" -d "grant_type=authorization_code"')
额外检查点
- 授权码唯一性:authorization code是一次性使用的,若重复提交同一code会直接返回
invalid_grant,确保每次请求使用新生成的code。 - redirect_uri完全匹配:请求token时的
redirect_uri必须与授权请求时的完全一致,包括大小写、是否带末尾斜杠等细节。 - 应用类型匹配:若Django后台注册的应用是
Public类型,请求token时无需传递client_secret(传递会导致验证失败);若为Confidential类型则正常传递。 - 时区与过期时间:确认服务器与本地时区一致,避免授权码因时间差提前过期(默认过期时间为5分钟)。
内容的提问来源于stack exchange,提问作者Peter
相关产品推荐
相关产品推荐

