Flask и React - обработка токенов после авторизации Spotify - PullRequest
6 голосов
/ 26 марта 2020

Я реализовал JWT для входа пользователя в свое приложение (до Spotify Auth), например:

Flask

@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, при этом ему необходимо каждые 10 минут обновлять sh токен доступа Spotify:

  • Является ли код авторизации потоком между серверами только для защиты секретных учетных данных приложения, и тогда безопасно иметь токены на фронте тендер?

  • Должен ли я хранить токены доступа и рефреки sh на внешнем интерфейсе и иметь JWT без состояния?

  • Должен ли я хранить только временный токен доступа и хранить в базе данных токены refre sh, имеющие JWT с состоянием?

  • Стоит ли выбирать сеанс, сохраняется только на стороне сервера, вместо этого?

Какой самый безопасный способ обработки моих конфиденциальных данных здесь? И, учитывая код выше, как так?

1 Ответ

5 голосов
/ 31 марта 2020

Огромное количество вопросов здесь! Давайте рассмотрим их один за другим:

Является ли код авторизации потоком между серверами только для защиты секретных учетных данных приложения, а затем безопасно иметь токены на внешнем интерфейсе?

В гранте Authorization Code вы должны обменять Authorization Code на токен. Это делается с помощью запроса к /token (grant_type: authorization_code), и для этого требуются ваши client_id и client_secret, которые тайно хранятся на вашем сервере (иначе not-publi * 1131). * в вашем веб-приложении. В этом контексте это действительно сервер-сервер.

Должен ли я хранить токены доступа и refre sh на внешнем интерфейсе и иметь JWT без состояния?

В вашем случае я бы сказал нет . Если токен будет использоваться для выполнения какого-либо API-запроса для Spotify на стороне сервера, , пожалуйста, оставьте access_token и refresh_token на стороне сервера .

Но тогда это уже не состояние без состояния ? Действительно.

Что вы могли бы сделать "без сохранения состояния"?

Если вы действительно хотите / нуждаетесь в токенах без сохранения состояния, ИМХО, вы можете сохранить access_token в Cook ie со следующими параметрами (и это обязательно):

  • Защищено: куки отправляются только по HTTPS
  • HttpOnly: недоступно с Javascript
  • SameSite: желательно строго! (здесь это зависит от того, нужен ли вам CORS)

PRO:

  • Это без состояния

CON:

  • Это может быть огромный повар ie.
  • Любой, кто получает доступ к вашему компьютеру, может получить access_token, точно так же, как сессионный повар ie. Время истечения здесь важно. Смотрите также: { ссылка }
  • Что-то еще ???? Подлежит вызову.

Случай refresh_token.

Я бы порекомендовал хранить токены refre sh на стороне сервера, поскольку обычно это токены с длительным сроком службы.

Что делать, когда access_token истекает?

Когда запрос приходит с истекшим access_token, вы можете просто обновить sh access_token с серверной стороны refresh_token, выполните работу и верните ответ с новым access_token, сохраненным в заголовке Set-Cookie.

Дополнительные примечания о JWT

Если у вас всегда есть JWT и вы храните их в Http -Только куки, вы, вероятно, скажете, что у вас нет никакого способа узнать, вошли ли вы из приложения 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 куки:

  • a11n.h: заголовок (параметры: безопасный)
  • a11n.d: полезная нагрузка (параметры: безопасный)
  • a11n.s: подпись (параметры: Безопасный, Http-Only )

Следствие:

  • a11n.d cook ie доступно из вашего приложения React (вы можете даже получить информацию пользователя от него)
  • a11n.s повар ie не доступен с Javascript
  • Вы должны собрать access_token от повара ie с на стороне сервера перед отправкой запроса в 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)

Надеюсь, это поможет!

Отказ от ответственности:

Образцы кода должны быть улучшены (обработка ошибок, проверки и т. Д. c). Они являются только примерами, иллюстрирующими поток.

...