python - 通过 Google 获取访问令牌 OAuth 时引发 FlowExchangeError
问题描述
我想向网站添加“通过 GMail 登录”功能。我创建login.html
并project.py
处理响应。
我添加一个按钮login.html
:
function renderButton() {
gapi.signin2.render('my-signin2', {
'scope': 'profile email',
'width': 240,
'height': 50,
'longtitle': true,
'theme': 'dark',
'onsuccess': signInCallback,
'onfailure': signInCallback
});
};
我有一个回调函数。在浏览器控制台中,我可以看到响应包含access_token
, id_token
(有什么区别?)和我的用户配置文件详细信息(姓名、电子邮件等),因此请求本身必须成功,但是,error
函数被调用,因为response
返回我的gconnect
处理程序是401
:
function signInCallback(authResult) {
var access_token = authResult['wc']['access_token'];
if (access_token) {
// Hide the sign-in button now that the user is authorized
$('#my-signin2').attr('style', 'display: none');
// Send the one-time-use code to the server, if the server responds, write a 'login successful' message to the web page and then redirect back to the main restaurants page
$.ajax({
type: 'POST',
url: '/gconnect?state={{STATE}}',
processData: false,
data: access_token,
contentType: 'application/octet-stream; charset=utf-8',
success: function(result)
{
....
},
error: function(result)
{
if (result)
{
// THIS CASE IS EXECUTED, although authResult['error'] is undefined
console.log('Logged in successfully as: ' + authResult['error']);
} else if (authResult['wc']['error'])
{
....
} else
{
....
}//else
}//error function
});//ajax
};//if access token
};//callback
处理对 Google 的 ajax 请求的代码在尝试获取时抛出 FlowExchangeError credentials = oauth_flow.step2_exchange(code)
:
@app.route('/gconnect', methods=['POST'])
def gconnect():
if request.args.get('state') != login_session['state']:
response = make_response(json.dumps('Invalid state parameter.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
# Obtain authorization code
code = request.data
try:
# Upgrade the authorization code into a credentials object
oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='')
oauth_flow.redirect_uri = 'postmessage'
##### THROWS EXCEPTION HERE #####
credentials = oauth_flow.step2_exchange(code)
except FlowExchangeError:
response = make_response(
json.dumps('Failed to upgrade the authorization code.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
# Check that the access token is valid.
access_token = credentials.access_token
url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s'
% access_token)
h = httplib2.Http()
result = json.loads(h.request(url, 'GET')[1])
# If there was an error in the access token info, abort.
if result.get('error') is not None:
response = make_response(json.dumps(result.get('error')), 500)
response.headers['Content-Type'] = 'application/json'
return response
# Verify that the access token is used for the intended user.
gplus_id = credentials.id_token['sub']
if result['user_id'] != gplus_id:
response = make_response(
json.dumps("Token's user ID doesn't match given user ID."), 401)
response.headers['Content-Type'] = 'application/json'
return response
# Verify that the access token is valid for this app.
if result['issued_to'] != CLIENT_ID:
response = make_response(
json.dumps("Token's client ID does not match app's."), 401)
print "Token's client ID does not match app's."
response.headers['Content-Type'] = 'application/json'
return response
stored_access_token = login_session.get('access_token')
stored_gplus_id = login_session.get('gplus_id')
if stored_access_token is not None and gplus_id == stored_gplus_id:
response = make_response(json.dumps('Current user is already connected.'),
200)
response.headers['Content-Type'] = 'application/json'
return response
# Store the access token in the session for later use.
login_session['access_token'] = credentials.access_token
login_session['gplus_id'] = gplus_id
# Get user info
userinfo_url = "https://www.googleapis.com/oauth2/v1/userinfo"
params = {'access_token': credentials.access_token, 'alt': 'json'}
answer = requests.get(userinfo_url, params=params)
data = answer.json()
login_session['username'] = data['name']
login_session['picture'] = data['picture']
login_session['email'] = data['email']
output = ''
output += '<h1>Welcome, '
output += login_session['username']
return output
我检查了从 Google API 获得的 client_secrets.json,看起来还可以,我需要更新它吗?
{"web":{"client_id":"blah blah blah.apps.googleusercontent.com","project_id":"blah","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"blah client secret","redirect_uris":["http://localhost:1234"],"javascript_origins":["http://localhost:1234"]}}
为什么credentials = oauth_flow.step2_exchange(code)失败?
这是我第一次实现这个,我正在学习Web和OAuth,所有的概念都很难一次掌握。我也在使用 Udacity OAuth 课程,但他们的代码很旧并且不起作用。我可能在这里缺少什么?
解决方案
您需要关注服务器端应用程序的 Google Signin,它详细描述了授权代码流程的工作原理,以及前端、后端和用户之间的交互。
在服务器端,您oauth_flow.step2_exchange(code)
在发送访问令牌时使用需要授权码。如上面的链接所述,在此处发送访问令牌不是授权代码流或一次性代码流的一部分:
您的服务器交换此一次性使用代码以从 Google 获取其自己的访问和刷新令牌,以便服务器能够进行自己的 API 调用,这可以在用户离线时完成。与纯服务器端流程和向您的服务器发送访问令牌相比,这种一次性代码流具有安全优势。
如果要使用此流程,则需要auth2.grantOfflineAccess()
在前端使用:
auth2.grantOfflineAccess().then(signInCallback);
这样当用户单击按钮时,它将返回授权码 + 访问令牌:
Google 登录按钮提供访问令牌和授权代码。该代码是一次性代码,您的服务器可以与 Google 的服务器交换访问令牌。
如果您希望您的服务器代表您的用户访问 Google 服务,您只需要授权码
在本教程中,它提供了以下示例,该示例应该适合您(您可以进行一些修改):
<html itemscope itemtype="http://schema.org/Article">
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="https://apis.google.com/js/client:platform.js?onload=start" async defer></script>
<script>
function start() {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init({
client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com',
// Scopes to request in addition to 'profile' and 'email'
//scope: 'additional_scope'
});
});
}
</script>
</head>
<body>
<button id="signinButton">Sign in with Google</button>
<script>
$('#signinButton').click(function() {
auth2.grantOfflineAccess().then(signInCallback);
});
</script>
<script>
function signInCallback(authResult) {
if (authResult['code']) {
// Hide the sign-in button now that the user is authorized, for example:
$('#signinButton').attr('style', 'display: none');
// Send the code to the server
$.ajax({
type: 'POST',
url: 'http://example.com/storeauthcode',
// Always include an `X-Requested-With` header in every AJAX request,
// to protect against CSRF attacks.
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
console.log(result);
// Handle or verify the server response.
},
processData: false,
data: authResult['code']
});
} else {
// There was an error.
}
}
</script>
</body>
</html>
请注意,上面的答案假设您要使用授权代码流/一次性代码流,因为它是您在服务器端实现的。
也可以像您一样发送访问令牌(例如,保持客户端不变)并删除“获取授权码”部分:
# Obtain authorization code
code = request.data
try:
# Upgrade the authorization code into a credentials object
oauth_flow = flow_from_clientsecrets('client_secrets.json', scope='')
oauth_flow.redirect_uri = 'postmessage'
##### THROWS EXCEPTION HERE #####
credentials = oauth_flow.step2_exchange(code)
except FlowExchangeError:
response = make_response(
json.dumps('Failed to upgrade the authorization code.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
反而 :
access_token = request.data
但这样做将不再是授权代码流/一次性代码流
access_token
您已经问过和之间有什么区别id_token
:
- 访问令牌是使您可以访问资源的令牌,在这种情况下是 Google 服务
- id_token 是一个 JWT 令牌,用于将您标识为 Google 用户 - 例如经过身份验证的用户,它是一个通常在服务器端检查的令牌(检查 JWT 的签名和字段)以验证用户身份
id_token 将在服务器端用于识别连接的用户。签出第 7 步中的 Python 示例:
# Get profile info from ID token
userid = credentials.id_token['sub']
email = credentials.id_token['email']
请注意,还有其他流程,网站将其发送id_token
到服务器,服务器对其进行检查,并对用户进行身份验证(服务器不关心此流程中的访问令牌/刷新令牌)。在授权码的情况下,只有临时代码在前端和后端之间共享。
还有一件事是关于refresh_token
哪些是用于生成其他access_token
. 访问令牌的生命周期有限(1 小时)。使用grantOfflineAccess
生成的代码将在用户第一次进行身份验证时为您提供 access_token + refresh_token。如果您想存储它refresh_token
以在后台访问 Google 服务,它属于您,这取决于您的需要
推荐阅读
- javascript - JQuery Ajax 文件 Ypload 导致完整页面重新加载
- mule - 在 mule 流中按顺序从 Jms 消费消息
- hibernate - 休眠搜索总是返回一个空列表
- android - 无法在 Android 模拟器上运行 Flutter App
- django - Django 模板显示不同的模型字段
- arrays - Julia:变异类型稳定数组的最佳实践
- cassandra - Cassandra 备份和恢复到另一个集群
- javascript - 删除 nodeJS 应用程序中的所有组和子项
- python - 在给定字符串条件的情况下删除熊猫中的数据
- powershell - 将用户输入添加到 Select-String 命令