spotipy-dev / spotipy

A light weight Python library for the Spotify Web API
http://spotipy.readthedocs.org
MIT License
5.03k stars 958 forks source link

Error when authenticating with credentials in code #856

Open mrticker opened 2 years ago

mrticker commented 2 years ago

I get an error when authenticating with credentials in code, as opposed to env variables. The client works when using env variables.

from config import APP_CLIENT_ID, APP_CLIENT_SECRET

auth_manager = SpotifyClientCredentials( client_id = APP_CLIENT_SECRET, client_secret = APP_CLIENT_SECRET )
sp = spotipy.Spotify( auth_manager = auth_manager )

...

C:\usr\Miniconda3\lib\site-packages\spotipy\oauth2.py in _request_access_token(self)
    267             return token_info
    268         except requests.exceptions.HTTPError as http_error:
--> 269             self._handle_oauth_error(http_error)
    270
    271     def _add_custom_values_to_token_info(self, token_info):

C:\usr\Miniconda3\lib\site-packages\spotipy\oauth2.py in _handle_oauth_error(self, http_error)
    144             error_description = None
    145
--> 146         raise SpotifyOauthError(
    147             'error: {0}, error_description: {1}'.format(
    148                 error, error_description

SpotifyOauthError: error: invalid_client, error_description: Failed to get client
stephanebruckert commented 2 years ago

What happens in from config import APP_CLIENT_ID, APP_CLIENT_SECRET?

Did you try setting client_id and client_secret directly, eg.

SpotifyClientCredentials( client_id ="abc", client_secret ="abc" )

?

mrticker commented 2 years ago

config just sets these variables appropriately.

In general, it seems inconsistent, one time it works, another it doesn't. Currently it works so I'll close this issue for now.

mrticker commented 2 years ago

It randomly stopped working. Here's the error:

---------------------------------------------------------------------------
HTTPError                                 Traceback (most recent call last)
C:\usr\Miniconda3\lib\site-packages\spotipy\oauth2.py in _request_access_token(self)
    264             )
--> 265             response.raise_for_status()
    266             token_info = response.json()

C:\usr\Miniconda3\lib\site-packages\requests\models.py in raise_for_status(self)
   1020         if http_error_msg:
-> 1021             raise HTTPError(http_error_msg, response=self)
   1022

HTTPError: 400 Client Error: Bad Request for url: https://accounts.spotify.com/api/token

During handling of the above exception, another exception occurred:

SpotifyOauthError                         Traceback (most recent call last)
d:\devel\spotify\get_playlists.py in <module>
     39
     40         try:
---> 41                 res = sp.user_playlists(user)
     42         except SpotifyException as e:
     43                 print(e)

C:\usr\Miniconda3\lib\site-packages\spotipy\client.py in user_playlists(self, user, limit, offset)
    761                 - offset - the index of the first item to return
    762         """
--> 763         return self._get(
    764             "users/%s/playlists" % user, limit=limit, offset=offset
    765         )

C:\usr\Miniconda3\lib\site-packages\spotipy\client.py in _get(self, url, args, payload, **kwargs)
    295             kwargs.update(args)
    296
--> 297         return self._internal_call("GET", url, payload, kwargs)
    298
    299     def _post(self, url, args=None, payload=None, **kwargs):

C:\usr\Miniconda3\lib\site-packages\spotipy\client.py in _internal_call(self, method, url, payload,
params)
    219         if not url.startswith("http"):
    220             url = self.prefix + url
--> 221         headers = self._auth_headers()
    222
    223         if "content_type" in args["params"]:

C:\usr\Miniconda3\lib\site-packages\spotipy\client.py in _auth_headers(self)
    210             return {}
    211         try:
--> 212             token = self.auth_manager.get_access_token(as_dict=False)
    213         except TypeError:
    214             token = self.auth_manager.get_access_token()

C:\usr\Miniconda3\lib\site-packages\spotipy\oauth2.py in get_access_token(self, as_dict, check_cache
)
    236                 return token_info if as_dict else token_info["access_token"]
    237
--> 238         token_info = self._request_access_token()
    239         token_info = self._add_custom_values_to_token_info(token_info)
    240         self.cache_handler.save_token_to_cache(token_info)

C:\usr\Miniconda3\lib\site-packages\spotipy\oauth2.py in _request_access_token(self)
    267             return token_info
    268         except requests.exceptions.HTTPError as http_error:
--> 269             self._handle_oauth_error(http_error)
    270
    271     def _add_custom_values_to_token_info(self, token_info):

C:\usr\Miniconda3\lib\site-packages\spotipy\oauth2.py in _handle_oauth_error(self, http_error)
    144             error_description = None
    145
--> 146         raise SpotifyOauthError(
    147             'error: {0}, error_description: {1}'.format(
    148                 error, error_description

SpotifyOauthError: error: invalid_client, error_description: Failed to get client
ananddw24 commented 2 years ago

Hey,

I am facing the same error as well. Has anyone figured out a workaround..?

Peter-Schorn commented 2 years ago

Can you confirm you're receiving the error SpotifyOauthError: error: invalid_client, error_description: Failed to get client?

If so, please print the client id and make sure it is what you expect.

ananddw24 commented 2 years ago

The client id is correct but I have been just getting a 400 error for the token. I tried out the examples/app.py and was able to get it working till the sign in step and was able to get a code but i was not able to get the other way without the callback to work

On Fri, Oct 21, 2022 at 6:39 PM Peter Schorn @.***> wrote:

Can you confirm you're receiving the error SpotifyOauthError: error: invalid_client, error_description: Failed to get client ?

If so, please print the client id and make sure it is what you expect.

— Reply to this email directly, view it on GitHub https://github.com/plamere/spotipy/issues/856#issuecomment-1287502524, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACOJUGTX5CCTXTUVAWPPZFLWEMLRNANCNFSM6AAAAAAQOP76WE . You are receiving this because you commented.Message ID: @.***>

Peter-Schorn commented 2 years ago

Please post the code that produces the error, as well as the full output.

tonkado commented 1 year ago

Hello everyone! I have the same issue using Spotipy in a docker container:

File "/usr/src/app/module_spotipy.py", line 36, in get_playlist
    playlist_id = playlist_exists(sp, playlist_name)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/app/module_spotipy.py", line 43, in playlist_exists
    user_id = sp.me()['id']
              ^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 1219, in me
    return self._get("me/")
           ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 321, in _get
    return self._internal_call("GET", url, payload, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 245, in _internal_call
    headers = self._auth_headers()
              ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 236, in _auth_headers
    token = self.auth_manager.get_access_token(as_dict=False)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 525, in get_access_token
    token_info = self.validate_token(self.cache_handler.get_cached_token())
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 380, in validate_token
    token_info = self.refresh_access_token(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 596, in refresh_access_token
    self._handle_oauth_error(http_error)
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 146, in _handle_oauth_error
    raise SpotifyOauthError(
spotipy.oauth2.SpotifyOauthError: error: invalid_client, error_description: Invalid client

It works fine at first but it seems there is a problem with a token refreshment after some timeout. If I connect to container with vscode and run script manually everything is working as expected.

I'm using .env to pass the credentials:

SPOTIPY_CLIENT_ID='91c927...'
SPOTIPY_CLIENT_SECRET='8c0b8...'
SPOTIPY_REDIRECT_URI='http://localhost:8083'

The ENV variables is available when I check them in a container console.

Maybe I missed something in configuration? Would be happy to hear any ideas or thoughts.

stephanebruckert commented 1 year ago

@tonkado what does os.getenv('SPOTIPY_CLIENT_ID') return from the python code inside your docker container?

tonkado commented 1 year ago

@stephanebruckert it returns an actual client id (the same that I provide via ENV) qTeuv7k6UL

Peter-Schorn commented 1 year ago

@tonkado Are you able to get it working without docker at least?

Peter-Schorn commented 1 year ago

Furthermore, you should modify your actual project code to print the client id to the console and verify that it is what you expect (not just your env_test.py script).

tonkado commented 1 year ago

Thank you for suggestion. I've add some logging. After restart it working but at some point I've got error again:

__main__ SPOTIPY_CLIENT_ID: '91c927e2xxxxxxxxxxxxxxxxxxxxxx'
Connecting to 149.154.167.51:443/TcpFull...
Connection to 149.154.167.51:443/TcpFull complete!
on_track_found SPOTIPY_CLIENT_ID: '91c927e2xxxxxxxxxxxxxxxxxxxxxx'
Unhandled exception on my_event_handler
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 588, in refresh_access_token
    response.raise_for_status()
  File "/usr/local/lib/python3.12/site-packages/requests/models.py", line 1021, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://accounts.spotify.com/api/token

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/telethon/client/updates.py", line 497, in _dispatch_update
    await callback(event)
  File "/usr/src/app/module_telegram.py", line 30, in my_event_handler
    callback(event.raw_text)
  File "/usr/src/app/tg_monitor.py", line 25, in on_track_found
    playlist_id = get_playlist(sp)
                  ^^^^^^^^^^^^^^^^
  File "/usr/src/app/module_spotipy.py", line 36, in get_playlist
    playlist_id = playlist_exists(sp, playlist_name)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/app/module_spotipy.py", line 43, in playlist_exists
    user_id = sp.me()['id']
              ^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 1219, in me
    return self._get("me/")
           ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 321, in _get
    return self._internal_call("GET", url, payload, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 245, in _internal_call
    headers = self._auth_headers()
              ^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/client.py", line 236, in _auth_headers
    token = self.auth_manager.get_access_token(as_dict=False)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 525, in get_access_token
    token_info = self.validate_token(self.cache_handler.get_cached_token())
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 380, in validate_token
    token_info = self.refresh_access_token(
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 596, in refresh_access_token
    self._handle_oauth_error(http_error)
  File "/usr/local/lib/python3.12/site-packages/spotipy/oauth2.py", line 146, in _handle_oauth_error
    raise SpotifyOauthError(
spotipy.oauth2.SpotifyOauthError: error: invalid_client, error_description: Invalid client

CODE: it's a telegram client that wait for a message with spotify link and add this track to the playlist:

def on_track_found(track):
    id = os.getenv('SPOTIPY_CLIENT_ID')
    logging.info(f'on_track_found SPOTIPY_CLIENT_ID: {id}')
    sp = authenticate_spotify(scope="playlist-modify-public")
    playlist_id = get_playlist(sp)
    tracks = [track]
    expand_playlist_with_new_tracks(sp, playlist_id, tracks)
    sp = None

if __name__ == "__main__":
    id = os.getenv('SPOTIPY_CLIENT_ID')
    logging.info(f'__main__ SPOTIPY_CLIENT_ID: {id}')
    client = create_client()
    client.start()
    set_event_handler(client, on_track_found)
    client.run_until_disconnected()

the authenticate_spotify() function:

def authenticate_spotify(scope: str = "user-library-read") -> Spotify:
    from spotipy.oauth2 import SpotifyOAuth

    try:
        sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
        return sp
    except spotipy.oauth2.SpotifyOauthError:
        print("Authentication failed: SpotifyOauthError")
        print(spotipy.oauth2.SpotifyOauthError)
        return None
Peter-Schorn commented 1 year ago
def authenticate_spotify(scope: str = "user-library-read") -> Spotify:
    from spotipy.oauth2 import SpotifyOAuth

    try:
        sp = spotipy.Spotify(auth_manager=SpotifyOAuth(scope=scope))
        return sp
    except spotipy.oauth2.SpotifyOauthError:
        print("Authentication failed: SpotifyOauthError")
        print(spotipy.oauth2.SpotifyOauthError)
        return None

This code does not authenticate anything. It just creates an instance of Spotify. Spotify automatically launches the authorization process when you try to call a method (e.g., Spotify.playlist).

Furthermore, and most importantly, I don't see any place in the code you provided where you actually provide the client id and client secret to the SpotifyOAuth authorization manager class.

tonkado commented 1 year ago

@Peter-Schorn Then I'm totally confused 😂 Please forgive me my lack of professionalism.

I read the docs (https://spotipy.readthedocs.io/en/latest/#authorization-code-flow) and though that you can just use a ENV variables to securely store id and secret and Spotipy will use them.

or if you are reluctant to immortalize your app credentials in your source code, you
can set environment variables like so (use $env:"credentials" instead of export on Windows):

export SPOTIPY_CLIENT_ID='your-spotify-client-id'
export SPOTIPY_CLIENT_SECRET='your-spotify-client-secret'
export SPOTIPY_REDIRECT_URI='your-app-redirect-url'

Maybe I just work with docker a lot 🤦‍♂️

What is the correct way to make authorization work in a container without need to pass redirect URL with fresh token to the command line?

Peter-Schorn commented 1 year ago

spotipy will retrieve the client id and secret from the environment, exactly as you indicated above.

The issue, most likely, is simply that you have not correctly passed those environment variables to your docker container. Docker containers do NOT have access to the environment variables declared in the process that launched them. You must pass them explicitly. See https://docs.docker.com/engine/reference/commandline/run/#env.

What is the correct way to make authorization work in a container without need to pass redirect URL with fresh token to the command line?

If you can't pass the redirect uri to the program over the command line, then you must setup a server that listens for when the user is redirected to your redirect uri after they authorize your app in the browser. From there, you can continue the authorization process.

tonkado commented 1 year ago

@Peter-Schorn Thanks again for your help with ideas.

The issue, most likely, is simply that you have not correctly passed those environment variables to your docker container.

I'm using docker-compose and passing env variables via env_file option:

version: "3"
services:
  spotify-python:
    container_name: spotify-python
    image: spotify-python
    build: .
    volumes:
    - .:/usr/src/app/
    env_file:
     - .env
    ports:
    - "8083:8083"

I can confirm that during the SpotifyOAuth() execution ENV is available and correct (my message above).

As for the Client Credentials flow, it's not clear for me. I suppose that it should use the flow described at Spotify docs:

In this case it should be a request from Spotipy to 'https://accounts.spotify.com/api/token' with id and secret, and a fresh token will be returned. After token is received and cached you can use the API methods.

User authentication requires interaction with your web browser. Once you enter your credentials and give authorization, you will be redirected to a url. Paste that url you were directed to to complete the authorization.

I don't understand why it's not working like that and why there is a local URL opening/reading logic.

Peter-Schorn commented 1 year ago

I can confirm that during the SpotifyOAuth() execution ENV is available and correct (https://github.com/spotipy-dev/spotipy/issues/856#issuecomment-1434506569).

Have you also confirmed that your app can also access the client secret? Furthermore, I must admit that, if you're still getting an invalid_client error, then I'm still not confident that spotipy can access both the client id and client secret. For the sake of simplicity if nothing else, you should initialize your instance of Spotify as follows. (check if you have extraneous whitespace in your client_id and client_secret variables.)

client_id = os.getenv('SPOTIPY_CLIENT_ID')
client_secret = os.getenv('SPOTIPY_CLIENT_SECRET')
print(f"client id: '{client_id}'")
print(f"client secret: '{client_secret}'")
sp = spotipy.Spotify(
    auth_manager=SpotifyOAuth(
        client_id=client_id, client_secret=client_secret, scope=scope
    )
)

I suppose that it should use the flow described at Spotify docs:

The Spotify web API has multiple authorization flows (see this article).The Client Credentials Flow does not support authorizing with a user, and so can not be used with endpoints that access/modify user data. It says that right at the top of the page you linked:

The Client Credentials flow is used in server-to-server authentication. Since this flow does not include authorization, only endpoints that do not access user information can be accessed.

spotipy does support this authorization method too: https://github.com/spotipy-dev/spotipy/blob/572195617b3d63f05f4083a4067fe6eb0dfe448b/spotipy/oauth2.py#L160

If you want you authorize with a user, you must open a URL in the browser. Period. All of the authorization flows that support authorizing with a user require opening a URL in the browser. The reason for this is simple: The Spotify user must be given a chance to login to their Spotify account and grant your app permission to access/modify their data. They can't login to their account directly in your app because then you could steal their username and password, so they have to login on the browser.

tonkado commented 1 year ago

@Peter-Schorn Thanks again for your patience and very detailed answers. I appreciate it a lot.

It seems that the problem was with my misunderstanding of the docs. As I write before, I was sure that you can just provide credentials via ENV variables and Spotipy will use them to refresh token. In fact you need to provide credentials as params in SpotifyOAuth. I was confused because somehow make it work without providing a client_id & client_secret as params (maybe just cache).

After I added credentials to SpotifyOAuth (as you describe above) everything is working fine inside a container and no longer throw an error.

For the history:
If you want to make Spotipy work from a Docker container
then you should obtain a Spotify token first:
1. attach to a running container
2. start authorization process with open_browser=False:
sp = spotipy.Spotify(
    auth_manager=SpotifyOAuth(
        client_id=client_id, client_secret=client_secret, scope=scope, open_browser=False
    )
)
3. get the OAuth link to Spotify authorization page
4. open link in the browser and authorize access
5. after redirection -> copy link to clipboard
6. paste link to the console input

Example log of the authorization process:

2023-02-21 18:14:49 INFO     User authentication requires interaction with your web browser. Once you enter your credentials and give authorization, you will be redirected to a url.  Paste that url you were directed to to complete the authorization.
Go to the following URL: https://accounts.spotify.com/authorize?client_id=91c927e22xxxxxxxxxxxaff3b1c&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8083&scope=playlist-modify-public
Enter the URL you were redirected to: http://localhost:8083/?code=_EhBBgf3LfwIIDHa-lHBPEjrXied6Q3I4Re_-ZlGk5k9a1hF_K2Y53CfO0Ubid9vDu16gtpPyBaW8UqUl-y30Hm7-MxklRgPErmjBKYeK-
/usr/src/app # 

After that .cache file will be created and will be used to refresh token when it's expired. Everything will work in a container without need for manual update.

May I kindly suggest that the documentation for this section be updated to improve clarity? I believe it would greatly benefit users of this excellent library and help avoid any potential confusion in the future. Thank you for considering this suggestion.

Peter-Schorn commented 1 year ago

As I write before, I was sure that you can just provide credentials via ENV variables and Spotipy will use them to refresh token.

Yes, this is correct, and I never said it wasn't. I told you it was correct:

spotipy will retrieve the client id and secret from the environment, exactly as you indicated above. The issue, most likely, is simply that you have not correctly passed those environment variables to your docker container. Docker containers do NOT have access to the environment variables declared in the process that launched them. You must pass them explicitly. See https://docs.docker.com/engine/reference/commandline/run/#env.

In fact you need to provide credentials as params in SpotifyOAuth.

No you don't. You can provide the client id and client secret as environment variables exactly as the documentation says, or you could pass them into the authorization manager explicitly. I asked you to use the latter method because it's simpler.