通过 Google 获取访问令牌 OAuth 时抛出 FlowExchangeError
FlowExchangeError thrown when getting access token OAuth via Google
我想向网站添加 'sign-in via 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
函数是因为我的 gconnect
处理程序返回的 response
是 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 请求的代码在尝试获取 credentials = oauth_flow.step2_exchange(code)
时抛出 FlowExchangeError :
@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
我已经检查了 client_secrets.json 我从 Google API 得到的,看起来还可以,我需要续订吗?
{"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 for server side apps,其中详细描述了授权代码流程的工作原理,以及前端、后端和用户之间的交互。
在服务器端,您使用 oauth_flow.step2_exchange(code)
需要授权码,而您发送的是访问令牌。如上文 link 所述,在此处发送访问令牌不是授权代码流或一次性代码流的一部分:
Your server exchanges this one-time-use code to acquire its own access
and refresh tokens from Google for the server to be able to make its
own API calls, which can be done while the user is offline. This
one-time code flow has security advantages over both a pure
server-side flow and over sending access tokens to your server.
如果你想使用这个流程,你需要在前端使用auth2.grantOfflineAccess()
:
auth2.grantOfflineAccess().then(signInCallback);
这样,当用户点击按钮时,它将 return 一个授权码 + 访问令牌:
The Google Sign-In button provides both an access token and an
authorization code. The code is a one-time code that your server can
exchange with Google's servers for an access token.
如果您想让您的服务器代表您的用户访问Google服务,您只需要授权码
来自 this tutorial 它给出了以下应该适合您的示例(您需要进行一些修改):
<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" 部分:
# 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 将在服务器端识别连接的用户时很有用。查看 step 7 中的 Python 示例:
# Get profile info from ID token
userid = credentials.id_token['sub']
email = credentials.id_token['email']
注意有 other flow 网站发送 id_token
到服务器,服务器检查它,并对用户进行身份验证(服务器不关心访问 token/refresh此流程中的令牌)。在授权码的情况下,只有临时代码在前后端之间共享。
还有一件事是关于 refresh_token
的,它们是用于生成其他 access_token
的令牌。访问令牌的生命周期有限(1 小时)。使用 grantOfflineAccess
生成一个代码,它将为您提供 access_token + refresh_token 第一次 用户身份验证。如果你想存储这个refresh_token
用于后台访问Google服务,它属于你,这取决于你的需要
我想向网站添加 'sign-in via 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
函数是因为我的 gconnect
处理程序返回的 response
是 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 请求的代码在尝试获取 credentials = oauth_flow.step2_exchange(code)
时抛出 FlowExchangeError :
@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
我已经检查了 client_secrets.json 我从 Google API 得到的,看起来还可以,我需要续订吗?
{"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 for server side apps,其中详细描述了授权代码流程的工作原理,以及前端、后端和用户之间的交互。
在服务器端,您使用 oauth_flow.step2_exchange(code)
需要授权码,而您发送的是访问令牌。如上文 link 所述,在此处发送访问令牌不是授权代码流或一次性代码流的一部分:
Your server exchanges this one-time-use code to acquire its own access and refresh tokens from Google for the server to be able to make its own API calls, which can be done while the user is offline. This one-time code flow has security advantages over both a pure server-side flow and over sending access tokens to your server.
如果你想使用这个流程,你需要在前端使用auth2.grantOfflineAccess()
:
auth2.grantOfflineAccess().then(signInCallback);
这样,当用户点击按钮时,它将 return 一个授权码 + 访问令牌:
The Google Sign-In button provides both an access token and an authorization code. The code is a one-time code that your server can exchange with Google's servers for an access token.
如果您想让您的服务器代表您的用户访问Google服务,您只需要授权码
来自 this tutorial 它给出了以下应该适合您的示例(您需要进行一些修改):
<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" 部分:
# 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 将在服务器端识别连接的用户时很有用。查看 step 7 中的 Python 示例:
# Get profile info from ID token
userid = credentials.id_token['sub']
email = credentials.id_token['email']
注意有 other flow 网站发送 id_token
到服务器,服务器检查它,并对用户进行身份验证(服务器不关心访问 token/refresh此流程中的令牌)。在授权码的情况下,只有临时代码在前后端之间共享。
还有一件事是关于 refresh_token
的,它们是用于生成其他 access_token
的令牌。访问令牌的生命周期有限(1 小时)。使用 grantOfflineAccess
生成一个代码,它将为您提供 access_token + refresh_token 第一次 用户身份验证。如果你想存储这个refresh_token
用于后台访问Google服务,它属于你,这取决于你的需要