Flask 和 React - 在 Spotify 授权后处理令牌
Flask and React - Handling tokens after Spotify Authorization
我已经在我的应用程序中为用户登录实现了 JWT(在 Spotify Auth 之前),如下所示:
烧瓶
@auth_blueprint.route('/auth/login', methods=['POST'])
def login_user():
# get post data
post_data = request.get_json()
response_object = {
'status': 'fail',
'message': 'Invalid payload.'
}
if not post_data:
return jsonify(response_object), 400
email = post_data.get('email')
password = post_data.get('password')
try:
# fetch the user data
user = User.query.filter_by(email=email).first()
if user and bcrypt.check_password_hash(user.password, password):
auth_token = user.encode_auth_token(user.id)
if auth_token:
response_object['status'] = 'success'
response_object['message'] = 'Successfully logged in.'
response_object['auth_token'] = auth_token.decode()
return jsonify(response_object), 200
else:
response_object['message'] = 'User does not exist.'
return jsonify(response_object), 404
except Exception:
response_object['message'] = 'Try again.'
return jsonify(response_object), 500
这些是我的 SQLAlchemy 的方法 User(db.Model)
def encode_auth_token(self, user_id):
"""Generates the auth token"""
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(
days=current_app.config.get('TOKEN_EXPIRATION_DAYS'),
seconds=current_app.config.get('TOKEN_EXPIRATION_SECONDS')
),
'iat': datetime.datetime.utcnow(),
'sub': user_id
}
return jwt.encode(
payload,
current_app.config.get('SECRET_KEY'),
algorithm='HS256'
)
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
"""
Decodes the auth token - :param auth_token: - :return: integer|string
"""
try:
payload = jwt.decode(
auth_token, current_app.config.get('SECRET_KEY'))
return payload['sub']
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.'
except jwt.InvalidTokenError:
return 'Invalid token. Please log in again.'
反应
App.jsx
loginUser(token) {
window.localStorage.setItem('authToken', token);
this.setState({ isAuthenticated: true });
this.getUsers();
this.createMessage('Welcome', 'success');
};
(...)
<Route exact path='/login' render={() => (
<Form
isAuthenticated={this.state.isAuthenticated}
loginUser={this.loginUser}
/>
)} />
和
Form.jsx
handleUserFormSubmit(event) {
event.preventDefault();
const data = {
email: this.state.formData.email,
password: this.state.formData.password
};
const url = `${process.env.REACT_APP_WEB_SERVICE_URL}/auth/${formType.toLowerCase()}`;
axios.post(url, data)
.then((res) => {
this.props.loginUser(res.data.auth_token);
})
第三方授权+二次APP认证
现在我想在 Spotify 回调后添加第二层身份验证和处理令牌,如下所示:
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def spotify_callback():
# Auth Step 4: Requests refresh and access tokens
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
REDIRECT_URI = os.environ.get('SPOTIPY_REDIRECT_URI')
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": REDIRECT_URI,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
# At this point, there is to generate a custom token for the frontend
# Either a self-contained signed JWT or a random token?
# In case the token is not a JWT, it should be stored in the session (in case of a stateful API)
# or in the database (in case of a stateless API)
# In case of a JWT, the authenticity can be tested by the backend with the signature so it doesn't need to be stored at all?
res = make_response(redirect('http://localhost/about', code=302))
return res
注意:这是获取新 Spotify 令牌的可能端点:
@spotify_auth_bp.route("/refresh_token", methods=['GET', 'POST'])
def refresh_token():
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
code_payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}
encode = 'application/x-www-form-urlencoded'
auth = base64.b64encode("{}:{}".format(CLIENT_ID, CLIENT_SECRET).encode())
headers = {"Content-Type" : encode, "Authorization" : "Basic {}".format(auth)}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload, headers=headers)
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
return access_token
在 Spotify 回调后处理我的令牌的最佳方式是什么?
考虑到,一旦用户使用该应用程序登录,他也会不停地使用 Spotify 登录,必须每 60 分钟刷新一次 Spotify 的访问令牌:
授权码是一个服务器到服务器的流程,只是为了保护秘密应用凭证,然后在前端拥有令牌是安全的吗?
我是否应该将访问令牌和刷新令牌都存储在前端,并使用无状态 JWT?
我是否应该只保留临时访问令牌并在数据库中保留刷新令牌,有状态 JWT?
我应该选择只在服务器端持久化的会话吗?
在这里处理我的敏感数据最安全的方法是什么?而且,考虑到上面的代码,怎么会这样?
这里有很多问题!让我们一一道来:
Is Authorization Code a server-to-server flow only to protect secret app credentials, and then it is safe to have tokens at frontend?
在Authorization Code
赠款中,您必须用Authorization Code
兑换代币。这是通过 /token
(grant_type
: authorization_code
) 的请求完成的,它需要您的 client_id
和 client_secret
它秘密存储在您的服务器中(又名 not-public 在您的 React Web 应用程序中)。在这种情况下,它确实是 server-to-server.
Should I keep both Access token and refresh tokens stored at frontend, and have a Stateless JWT?
在你的情况下,我会说不。如果令牌将用于在 server-side 上向 Spotify 发出一些 API 请求,请保留 access_token
和 refresh_token
server-side.
但是,它不再是无状态的了?确实。
你能做什么"stateless"?
如果您真的 want/need 无状态令牌,恕我直言,您可以使用以下选项将 access_token
存储在 Cookie 中(这是强制性的):
- 安全:cookie 仅通过 HTTPS 发送
- HttpOnly:无法从 Javascript
访问
- SameSite:最好严格! (这里要看你是否需要CORS)
专业版:
- 它是无状态的
反对意见:
- 它可能是一个巨大的饼干。
- 任何访问您计算机的人都可以获得 access_token,就像 session cookie 一样。到期时间在这里很重要。另见:https://whosebug.com/a/41076836/2437450
- 还有别的吗????有待挑战。
案例 refresh_token
.
我建议存储刷新令牌 server-side 因为它通常是 long-life 令牌。
access_token
过期了怎么办?
当请求带有过期的 access_token
时,您可以简单地用 server-side 存储的 refresh_token
刷新 access_token
,完成工作,然后 return 通过 Set-Cookie
header.
存储的新 access_token
的响应
关于 JWT 的补充说明
如果您始终拥有 JWT 并将它们存储在 Http-Only cookie 中,您可能会说您无法通过任何方式知道您是否来自 React 应用 logged-in。
好吧,我已经用 JWT 试验了一个非常好的技巧。
一个 JWT 由 3 部分组成; header、有效载荷和签名。您真正想要在 cookie 中保护的是签名。事实上,如果您没有正确的签名,那么 JWT 就毫无用处。所以你可以做的是拆分 JWT 并只制作签名 Http-Only.
在你的情况下它应该是这样的:
@app.route('/callback')
def callback():
# (...)
access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJSYXBoYWVsIE1lZGFlciJ9.V5exVQ92sZRwRxKeOFxqb4DzWaMTnKu-VmhW-r1pg8E'
a11n_h, a11n_d, a11n_s = access_token.split('.')
response = redirect('http://localhost/about', 302)
response.set_cookie('a11n.h', a11n_h, secure=True)
response.set_cookie('a11n.d', a11n_d, secure=True)
response.set_cookie('a11n.s', a11n_s, secure=True, httponly=True)
return response
您将有 3 个 cookie:
a11n.h
:header(选项:安全)
a11n.d
:有效负载(选项:安全)
a11n.s
:签名(选项:安全,Http-Only)
结果是:
a11n.d
cookie 可从您的 React 应用访问(您甚至可以从中获取用户信息)
a11n.s
无法从 Javascript 访问 cookie
- 在向 Spotify
发送请求之前,您必须在 server-side 上从 cookies 重新组装 access_token
重新组装access_token
:
@app.route('/resource')
def resource():
a11n_h = request.cookies.get('a11n.h')
a11n_d = request.cookies.get('a11n.d')
a11n_s = request.cookies.get('a11n.s')
access_token = a11n_h + '.' + a11n_d + '.' + a11n_s
jwt.decode(access_token, verify=True)
希望对您有所帮助!
免责声明:
代码示例需要改进(错误处理、检查等)。它们只是说明流程的示例。
我已经在我的应用程序中为用户登录实现了 JWT(在 Spotify Auth 之前),如下所示:
烧瓶
@auth_blueprint.route('/auth/login', methods=['POST'])
def login_user():
# get post data
post_data = request.get_json()
response_object = {
'status': 'fail',
'message': 'Invalid payload.'
}
if not post_data:
return jsonify(response_object), 400
email = post_data.get('email')
password = post_data.get('password')
try:
# fetch the user data
user = User.query.filter_by(email=email).first()
if user and bcrypt.check_password_hash(user.password, password):
auth_token = user.encode_auth_token(user.id)
if auth_token:
response_object['status'] = 'success'
response_object['message'] = 'Successfully logged in.'
response_object['auth_token'] = auth_token.decode()
return jsonify(response_object), 200
else:
response_object['message'] = 'User does not exist.'
return jsonify(response_object), 404
except Exception:
response_object['message'] = 'Try again.'
return jsonify(response_object), 500
这些是我的 SQLAlchemy 的方法 User(db.Model)
def encode_auth_token(self, user_id):
"""Generates the auth token"""
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(
days=current_app.config.get('TOKEN_EXPIRATION_DAYS'),
seconds=current_app.config.get('TOKEN_EXPIRATION_SECONDS')
),
'iat': datetime.datetime.utcnow(),
'sub': user_id
}
return jwt.encode(
payload,
current_app.config.get('SECRET_KEY'),
algorithm='HS256'
)
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
"""
Decodes the auth token - :param auth_token: - :return: integer|string
"""
try:
payload = jwt.decode(
auth_token, current_app.config.get('SECRET_KEY'))
return payload['sub']
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.'
except jwt.InvalidTokenError:
return 'Invalid token. Please log in again.'
反应
App.jsx
loginUser(token) {
window.localStorage.setItem('authToken', token);
this.setState({ isAuthenticated: true });
this.getUsers();
this.createMessage('Welcome', 'success');
};
(...)
<Route exact path='/login' render={() => (
<Form
isAuthenticated={this.state.isAuthenticated}
loginUser={this.loginUser}
/>
)} />
和
Form.jsx
handleUserFormSubmit(event) {
event.preventDefault();
const data = {
email: this.state.formData.email,
password: this.state.formData.password
};
const url = `${process.env.REACT_APP_WEB_SERVICE_URL}/auth/${formType.toLowerCase()}`;
axios.post(url, data)
.then((res) => {
this.props.loginUser(res.data.auth_token);
})
第三方授权+二次APP认证
现在我想在 Spotify 回调后添加第二层身份验证和处理令牌,如下所示:
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def spotify_callback():
# Auth Step 4: Requests refresh and access tokens
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
REDIRECT_URI = os.environ.get('SPOTIPY_REDIRECT_URI')
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": REDIRECT_URI,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
# At this point, there is to generate a custom token for the frontend
# Either a self-contained signed JWT or a random token?
# In case the token is not a JWT, it should be stored in the session (in case of a stateful API)
# or in the database (in case of a stateless API)
# In case of a JWT, the authenticity can be tested by the backend with the signature so it doesn't need to be stored at all?
res = make_response(redirect('http://localhost/about', code=302))
return res
注意:这是获取新 Spotify 令牌的可能端点:
@spotify_auth_bp.route("/refresh_token", methods=['GET', 'POST'])
def refresh_token():
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
code_payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}
encode = 'application/x-www-form-urlencoded'
auth = base64.b64encode("{}:{}".format(CLIENT_ID, CLIENT_SECRET).encode())
headers = {"Content-Type" : encode, "Authorization" : "Basic {}".format(auth)}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload, headers=headers)
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
return access_token
在 Spotify 回调后处理我的令牌的最佳方式是什么?
考虑到,一旦用户使用该应用程序登录,他也会不停地使用 Spotify 登录,必须每 60 分钟刷新一次 Spotify 的访问令牌:
授权码是一个服务器到服务器的流程,只是为了保护秘密应用凭证,然后在前端拥有令牌是安全的吗?
我是否应该将访问令牌和刷新令牌都存储在前端,并使用无状态 JWT?
我是否应该只保留临时访问令牌并在数据库中保留刷新令牌,有状态 JWT?
我应该选择只在服务器端持久化的会话吗?
在这里处理我的敏感数据最安全的方法是什么?而且,考虑到上面的代码,怎么会这样?
这里有很多问题!让我们一一道来:
Is Authorization Code a server-to-server flow only to protect secret app credentials, and then it is safe to have tokens at frontend?
在Authorization Code
赠款中,您必须用Authorization Code
兑换代币。这是通过 /token
(grant_type
: authorization_code
) 的请求完成的,它需要您的 client_id
和 client_secret
它秘密存储在您的服务器中(又名 not-public 在您的 React Web 应用程序中)。在这种情况下,它确实是 server-to-server.
Should I keep both Access token and refresh tokens stored at frontend, and have a Stateless JWT?
在你的情况下,我会说不。如果令牌将用于在 server-side 上向 Spotify 发出一些 API 请求,请保留 access_token
和 refresh_token
server-side.
但是,它不再是无状态的了?确实。
你能做什么"stateless"?
如果您真的 want/need 无状态令牌,恕我直言,您可以使用以下选项将 access_token
存储在 Cookie 中(这是强制性的):
- 安全:cookie 仅通过 HTTPS 发送
- HttpOnly:无法从 Javascript 访问
- SameSite:最好严格! (这里要看你是否需要CORS)
专业版:
- 它是无状态的
反对意见:
- 它可能是一个巨大的饼干。
- 任何访问您计算机的人都可以获得 access_token,就像 session cookie 一样。到期时间在这里很重要。另见:https://whosebug.com/a/41076836/2437450
- 还有别的吗????有待挑战。
案例 refresh_token
.
我建议存储刷新令牌 server-side 因为它通常是 long-life 令牌。
access_token
过期了怎么办?
当请求带有过期的 access_token
时,您可以简单地用 server-side 存储的 refresh_token
刷新 access_token
,完成工作,然后 return 通过 Set-Cookie
header.
access_token
的响应
关于 JWT 的补充说明
如果您始终拥有 JWT 并将它们存储在 Http-Only cookie 中,您可能会说您无法通过任何方式知道您是否来自 React 应用 logged-in。 好吧,我已经用 JWT 试验了一个非常好的技巧。
一个 JWT 由 3 部分组成; header、有效载荷和签名。您真正想要在 cookie 中保护的是签名。事实上,如果您没有正确的签名,那么 JWT 就毫无用处。所以你可以做的是拆分 JWT 并只制作签名 Http-Only.
在你的情况下它应该是这样的:
@app.route('/callback')
def callback():
# (...)
access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJSYXBoYWVsIE1lZGFlciJ9.V5exVQ92sZRwRxKeOFxqb4DzWaMTnKu-VmhW-r1pg8E'
a11n_h, a11n_d, a11n_s = access_token.split('.')
response = redirect('http://localhost/about', 302)
response.set_cookie('a11n.h', a11n_h, secure=True)
response.set_cookie('a11n.d', a11n_d, secure=True)
response.set_cookie('a11n.s', a11n_s, secure=True, httponly=True)
return response
您将有 3 个 cookie:
a11n.h
:header(选项:安全)a11n.d
:有效负载(选项:安全)a11n.s
:签名(选项:安全,Http-Only)
结果是:
a11n.d
cookie 可从您的 React 应用访问(您甚至可以从中获取用户信息)a11n.s
无法从 Javascript 访问 cookie
- 在向 Spotify 发送请求之前,您必须在 server-side 上从 cookies 重新组装
access_token
重新组装access_token
:
@app.route('/resource')
def resource():
a11n_h = request.cookies.get('a11n.h')
a11n_d = request.cookies.get('a11n.d')
a11n_s = request.cookies.get('a11n.s')
access_token = a11n_h + '.' + a11n_d + '.' + a11n_s
jwt.decode(access_token, verify=True)
希望对您有所帮助!
免责声明:
代码示例需要改进(错误处理、检查等)。它们只是说明流程的示例。