【问题标题】:Flask and React - Handling tokens after Spotify AuthorizationFlask 和 React - 在 Spotify 授权后处理令牌
【发布时间】:2020-07-06 15:17:44
【问题描述】:

我已经在我的应用中实现了用户登录的 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);
    })

第三方授权+二次应用认证

现在我想在 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?

  • 我是否应该只保留临时访问令牌并在数据库中保留刷新令牌,并拥有 Stateful JWT?

  • 我应该选择只在服务器端持久化的 Session 吗?

在这里处理我的敏感数据最安全的方法是什么?而且,考虑到上面的代码,怎么会这样?

【问题讨论】:

    标签: reactjs authentication flask jwt authorization


    【解决方案1】:

    这里有很多问题!让我们一一来看:

    授权代码是服务器到服务器的流程,仅用于保护秘密应用凭据,然后在前端拥有令牌是安全的吗?

    Authorization Code 授权中,您必须用Authorization Code 交换令牌。这是通过对/token (grant_type: authorization_code) 的请求完成的,它需要您的 client_id client_secret 秘密存储在您的服务器中(又名非公开你的反应网络应用程序)。在这种情况下,它确实是服务器到服务器。

    我是否应该将访问令牌和刷新令牌都存储在前端,并拥有无状态 JWT?

    在你的情况下,我会说。如果令牌将用于在服务器端向 Spotify 发出一些 API 请求,请保留access_tokenrefresh_token 服务器端

    但是,它不再是无国籍的了吗?确实。

    “无国籍”你能做什么?

    如果您真的想要/需要无状态令牌,恕我直言,您可以使用以下选项将 access_token 存储在 Cookie 中(这是强制性的):

    • 安全:cookie 仅通过 HTTPS 发送
    • HttpOnly:无法从 Javascript 访问
    • SameSite:最好严格! (这取决于您是否需要 CORS)

    专业版:

    • 它是无状态的

    缺点:

    • 它可能是一个巨大的 cookie。
    • 任何访问您的计算机的人都可以获得 access_token,就像会话 cookie 一样。到期时间在这里很重要。另请参阅:https://stackoverflow.com/a/41076836/2437450
    • 还有什么????接受挑战。

    refresh_token的情况。

    我建议在服务器端存储刷新令牌,因为它通常是一个长寿命令牌。

    access_token过期了怎么办?

    当请求带有过期的access_token 时,您可以简单地使用服务器端存储的refresh_token 刷新access_token,完成这项工作,然后返回带有通过@987654338 存储的新access_token 的响应@标头。

    关于 JWT 的补充说明

    如果您始终拥有 JWT 并将它们存储在 Http-Only cookie 中,您可能会说您无法知道您是否从 React 应用程序登录。 嗯,我已经用 JWT 试验过一个非常好的技巧。

    一个 JWT 由 3 部分组成;标头、有效负载和签名。您真正想要在 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:标头(选项:安全)
    • a11n.d:有效载荷(选项:安全)
    • a11n.s:签名(选项:安全,Http-Only

    结果是:

    • a11n.d cookie 可以从你的 React 应用程序中访问(你甚至可以从中获取用户信息)
    • a11n.s cookie 无法从 Javascript 访问
    • 在向 Spotify 发送请求之前,您必须在服务器端从 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 解决方案。考虑到我上面的代码,您能否编辑您的答案并提供一些代码,以及您在 JWT 上的实现?为了完整起见,因为我有关于这个问题的 JWT 代码。我真的很感激你的回答。
    • 好吧,我不是 Flask 专家,但我可以添加一些示例/插图。
    • 那太棒了。而且你真的比我更专业。 ;-)
    • @8-BitBorges 如果你这么说......我添加了两个样本。
    • 我看到您的访问令牌在您将其设置为 cookie 时已被编码。我怎么去那里?
    猜你喜欢
    • 2020-06-26
    • 2020-12-18
    • 2016-05-30
    • 2011-06-12
    • 2014-11-08
    • 2020-07-13
    • 2015-03-15
    • 2019-06-01
    • 2019-08-05
    相关资源
    最近更新 更多