Docker (React / Flask / Nginx) - Код авторизации Spotify - PullRequest
2 голосов
/ 14 марта 2020

Исходя из этого SO ответа, я пытаюсь реализовать Код авторизации Spotify , поскольку мне нужно, чтобы пользователь постоянно входил в систему.

В отличие от неявного Поток, в потоке кода авторизации приложение должно предоставить client_secret и получить токен refre sh для неограниченного доступа, поэтому обмен данными должен происходить сервер-сервер .


Nginx Proxy

Мой внутренний сервер работает с Flask при http://localhost: 5000 , а мой внешний интерфейс работает с React при http://localhost: 3000 .

Обе службы находятся за nginx обратным прокси-сервером, настроенным так:

location / {
        proxy_pass        http://client:3000;
        proxy_redirect    default;
        proxy_set_header  Upgrade $http_upgrade;
        proxy_set_header  Connection "upgrade";
        proxy_set_header  Host $host;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Host $server_name;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

location /callback {
        proxy_pass        http://web:5000;
        proxy_redirect    default;
        proxy_set_header  Upgrade $http_upgrade;
        proxy_set_header  Connection "upgrade";
        proxy_set_header  Host $host;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Host $server_name;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

В соответствии с ответом выше, я делаю следующее:

  1. Предоставление кнопки на моей странице внешнего интерфейса, которая ссылается на ваш https://accounts.spotify.com/authorize/ {...} URL. (это не должен быть запрос AJAX, иначе это вызовет CORS проблем)
  2. Пользователь продолжит предоставлять моему приложению разрешения, указанные в параметре scope, и будет перенаправлен обратно на URL, указанный вами в параметре REDIRECT_URI.
  3. Здесь вы получаете код авторизации, который вы можете использовать в конечной точке https://accounts.spotify.com/api/token/ {...}

Реакция

Здесь я предоставляю пользователю кнопку авторизации:

render() {
    var state = generateRandomString(16);
    const Credentials = {
      stateKey: 'spotify_auth_state',
      client_id: 'my_id',
      redirect_uri: 'http://localhost:5000/callback',
      scope: 'playlist-modify-public playlist-modify-private'
    }
    let url = 'https://accounts.spotify.com/authorize';
    url += '?response_type=token';
    url += '&client_id=' + encodeURIComponent(Credentials.client_id);
    url += '&scope=' + encodeURIComponent(Credentials.scope);
    url += '&redirect_uri=' + encodeURIComponent(Credentials.redirect_uri);
    url += '&state=' + encodeURIComponent(state);


   return (
      <div className="button_container">
      <h1 className="title is-3"><font color="#C86428">{"Welcome"}</font></h1>
          <div className="Line" /><br/>
            <a href={url} > Login to Spotify </a>
      </div>
    )
  }

Flask

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

# spotify auth
@spotify_auth_bp.route("/spotify_auth", methods=['GET', 'POST'])
def spotify_auth():
    #Auth Step 1: Authorization
    #  Client Keys
    CLIENT_ID =   os.environ.get('SPOTIPY_CLIENT_ID')
    CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
    # Spotify URLS
    SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
    #SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
    SPOTIFY_API_BASE_URL = "https://api.spotify.com"
    API_VERSION = "v1"
    SPOTIFY_API_URL = "{}/{}".format(SPOTIFY_API_BASE_URL, API_VERSION)

    # Server-side Parameters
    CLIENT_SIDE_URL = os.environ.get('REACT_APP_WEB_SERVICE_URL')
    REDIRECT_URI = os.environ.get('REACT_APP_WEB_SERVICE_URL')
    #PORT = 5000
    #REDIRECT_URI = "{}:{}/callback".format(CLIENT_SIDE_URL, PORT)
    SCOPE = os.environ.get('SPOTIPY_SCOPE')
    STATE = ""
    SHOW_DIALOG_bool = True
    SHOW_DIALOG_str = str(SHOW_DIALOG_bool).lower()

    auth_query_parameters = {
        "response_type": "code",
        "redirect_uri": 'http://localhost/callback',
        "scope": 'user-read-currently-playing user-read-private user-library-read user-read-email user-read-playback-state user-follow-read playlist-read-private playlist-modify-public playlist-modify-private',
        # "state": STATE,
        # "show_dialog": SHOW_DIALOG_str,
        "client_id": CLIENT_ID
    }
    url_args = "&".join(["{}={}".format(key, quote(val)) for key, val in auth_query_parameters.items()])
    auth_url = "{}/?{}".format(SPOTIFY_AUTH_URL, url_args)
    return redirect(auth_url)



@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def callback():
    # Auth Step 4: Requests refresh and access tokens
    CLIENT_ID =   'my_id'
    CLIENT_SECRET = 'my_secret'
    CLIENT_SIDE_URL = 'http://localhost'
    PORT = 5000
    REDIRECT_URI = "{}:{}/callback".format(CLIENT_SIDE_URL, PORT)

    SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"

    auth_token = request.args['code']
    code_payload = {
        "grant_type": "authorization_code",
        "code": auth_token,
        "redirect_uri": 'http://localhost/callback',
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
    }

    auth_str = '{}:{}'.format(CLIENT_ID, CLIENT_SECRET) 
    b64_auth_str = base64.urlsafe_b64encode(auth_str.encode()).decode()

    headers = {
        "Content-Type" : 'application/x-www-form-urlencoded', 
        "Authorization" : "Basic {}".format(b64_auth_str)} 

    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)
    print ('RESPONSE DATA', response_data)

    access_token = response_data["access_token"]
    refresh_token = response_data["refresh_token"]
    token_type = response_data["token_type"]
    expires_in = response_data["expires_in"]

    template =  render_template("index.html")
    response_object = {
                'status': 'success',
                'message': 'success',
                'data': [{'access_token': access_token,
                          'refresh_token': refresh_token,
                          'token_type': token_type,
                          'content': template}]
                }

    return jsonify(response_object), 200

Перенаправления в белый список с помощью Spotify

http://localhost:5000 
http://localhost:5000/callback
http://web:5000
http://web:5000/callback 
http://localhost/callback 

Однако, когда я нажимаю кнопку с двумя первыми перенаправлениями, я получаю сообщение об ошибке:

localhost refused to connect.

Почему?

Если я нажимаю кнопку с http://localhost/callback в качестве redirect_uri, я получаю: * 107 9 *

KeyError: 'access_token'

Чего мне не хватает?

ВОПРОС

Я хотел бы получить Flask конечная точка, подобная приведенной выше, где я мог бы получить токен доступа (обновляется, если срок его действия истек).

Решение, которое обойдется без кода Javascript для аутентификации и будет идеальным. Это возможно с контейнерным сервером?

1 Ответ

2 голосов
/ 18 марта 2020

Поток кода авторизации не реализован так, как должен. Началом этого потока должен быть запрос от внешнего интерфейса (реакции) к внутреннему интерфейсу (flask). Бэкэнд отвечает за запуск 302 Redirect провайдеру идентификации (Spotify) с правильными параметрами.

@spotify_auth_bp.route("/auth", methods=['GET'])
def auth():
    CODE = "code"
    CLIENT_ID =   os.environ.get('SPOTIPY_CLIENT_ID')
    SCOPE = "playlist-modify-public playlist-modify-private"
    SPOTIFY_AUTH_URL = "https://accounts.spotify.com/authorize"
    REDIRECT_URI = "http://localhost/callback"
    return redirect("{}?response_type={}&client_id={}&scope={}&redirect_uri={}".format(SPOTIFY_AUTH_URL, CODE, CLIENT_ID, SCOPE, REDIRECT_URI), code=302)

Интерфейс должен полностью не знать о провайдере идентификации, а бэкэнд не должен пересылать access_token на веб-интерфейс, но вместо этого генерируйте свои собственные токены (в идеале, как Cook ie), когда пользователь проходит аутентификацию на провайдере идентификации.

Вы вообще не используете client_secret на стороне клиента, и это не должно быть известно клиенту. Как следует из названия, он должен быть секретным и больше не секретным, как только вы включите его в код JavaScript. Сохраняя client_secret внутри бэкэнда, вы полностью скрываете его от конечных пользователей (особенно от злонамеренных).

При этом причина, по которой вы наблюдаете эту ошибку, заключается в том, что POST запрос, который должен содержать access_token в ответе, на самом деле не содержит.

Причина в том, что ?response_type=token неверен, он должен быть ?response_type=code в начальном запросе.

Источник: https://developer.spotify.com/documentation/general/guides/authorization-guide/

Вот пример конечной точки обратного вызова:

@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def 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": 'http://localhost/callback',
    } 

    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
    # Let's assume the resulting token is stored in a variable named "token"

    res = Response('http://localhost/about', status=302)
    res.set_cookie('auth_cookie', token)
    return res
...