Flask and React-Spotify授权后处理令牌 [英] Flask and React - Handling tokens after Spotify Authorization

查看:64
本文介绍了Flask and React-Spotify授权后处理令牌的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我已经在我的应用中(在Spotify Auth之前)实现了JWT来进行用户登录,

I have implemented JWT for user login in my app (before Spotify Auth), like so:

@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回调之后处理令牌,如下所示:

Third Party Authorization + Second App Authentication

Now I'd like to add a second layer of authentication and handle tokens after Spotify callback, like so:

@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令牌的可能端点:


Note: this a possible endpoint for getting new Spotify tokens:

@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回调后处理令牌的最佳方法是什么?


What is the best way of handling my tokens after Spotify callback?

考虑到用户登录应用程序后,他还将不间断地使用Spotify登录,必须每60分钟刷新一次Spotify的访问令牌:

Considering that, once user is logged with the app, he will also be logged with Spotify non-stop, having to refresh Spotify's access token every 60 minutes:

  • 授权代码是否仅用于保护服务器之间的机密信息以保护秘密的应用程序凭据,然后在前端放置令牌是安全的?

  • Is Authorization Code a server-to-server flow only to protect secret app credentials, and then it is safe to have tokens at frontend?

我应该同时将Access令牌和刷新令牌都存储在前端,并拥有无状态JWT吗?

Should I keep both Access token and refresh tokens stored at frontend, and have a Stateless JWT?

我应该只在拥有状态JWT的情况下在数据库中仅保留临时访问令牌并保持刷新令牌吗?

Should I keep only temporary access token and keep refresh tokens at database, having a Stateful JWT?

我应该选择一个会话,而只保留在服务器端吗?

Should I opt for a Session, persisted only server-side, instead?

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

What is the safest way of handling my sensitive data here? And, considering the code above, how so?

推荐答案

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

A huge number of questions here! Let's take them one by one:

授权代码是否仅用于保护服务器之间的服务器凭据以保护秘密的应用程序凭据,然后在前端放置令牌是安全的?

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秘密存储在服务器中(也称为非公开)您的React Web应用程序).在这种情况下,确实是服务器到服务器.

In the Authorization Code grant, you have to exchange the Authorization Code for a token. This is done with a request to /token (grant_type: authorization_code) and it requires your client_id and client_secret which is secretly stored in your server (aka not-public in your react web app). In this context it's indeed server-to-server.

我应该同时将Access令牌和刷新令牌都存储在前端,并具有无状态JWT吗?

Should I keep both Access token and refresh tokens stored at frontend, and have a Stateless JWT?

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

In your case, I would say no. If the token will be used to do some API request to Spotify on server-side, please keep access_token and refresh_token server-side.

但是,那不再不是无状态的了吗?的确如此.

But then, it's not anymore stateless ? Indeed.

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

If you really want/need stateless tokens, IMHO you could store the access_token in a Cookie with following options (and it's mandatory):

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

PRO:

  • 它是无国籍的

CON:

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

我建议在服务器端存储刷新令牌,因为它通常是使用寿命长的令牌.

I would recommend to store refresh tokens server-side because it's usually a long-life token.

当请求带有过期的access_token时,您可以简单地使用服务器端存储的refresh_token刷新access_token,执行此工作,然后返回响应,并使用通过access_token返回响应>标头.

When a request comes with an expired access_token, you can simply refresh the access_token with server-side-stored refresh_token, do the job, and return the response with a new access_token stored through Set-Cookie header.

如果您一直有JWT并将它们存储在仅Http的cookie中,您可能会说您无法知道是否已从React应用程序登录. 好吧,我已经在JWT上尝试了一个技巧,这很不错.

If you always have JWT and you store them in Http-Only cookies, you'll probably say that you don't have any way to know if your are logged-in from your React app. Well there is a trick I already experimented with JWT which is pretty nice.

JWT由3部分组成;标头,有效负载和签名.您实际上要在Cookie中保护的是签名.确实,如果您没有正确的签名,那么JWT就是没有用的.因此,您可以做的是拆分JWT并仅使签名为Http-Only.

A JWT is composed of 3 parts; the header, the payload and the signature. What you actually want to protect in your cookies is the signature. Indeed, if you don't have the right signature the JWT is useless. So what you could do is to split the JWT and make only the signature Http-Only.

在您的情况下,其外观应为:

In your case it should look like:

@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:

You would have 3 cookies:

  • a11n.h:标头(选项:安全)
  • a11n.d:有效负载(选项:安全)
  • a11n.s:签名(选项:安全,仅HTTP )
  • a11n.h: the header (options: Secure)
  • a11n.d: the payload (options: Secure)
  • a11n.s: the signature (options: Secure, Http-Only)

结果是:

    可从您的React应用访问
  • a11n.d cookie(您甚至可以从中获取userinfo)
  • 无法从Javascript访问
  • a11n.s cookie
  • 您必须先在服务器端从cookie s 重组access_token,然后再向Spotify发送请求
  • a11n.d cookie is accessible from your React app (you can even get userinfo from it)
  • a11n.s cookie is not accessible from Javascript
  • You have to reassemble the access_token from cookies on server-side before sending request to Spotify

要重新组装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)

希望对您有帮助!

免责声明:

代码示例需要改进(错误处理,检查等).它们只是说明流程的示例.

这篇关于Flask and React-Spotify授权后处理令牌的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆