Implementing the authorization code flow

In this section, we are going to implement the authorization code flow, which we will be using in the client. We need to use this authentication flow because we need to acquire special access rights from the user using our application to execute certain actions. For instance, our application will have to be able to send a request to Spotify's Web API to play a certain track on the user's active device. In order to do that, we need to request user-modify-playback-state.

Here are the steps involved in the authorization code flow:

  1. Our application will request authorization to access data, redirecting the user to a login page on Spotify's web page. There, the user can see all the access rights that the application requires.
  2. If the user approves it, the Spotify account service will send a request to the callback URI, sending a code and the state.
  3. When we get hold of the code, we send a new request passing the client_id, client_secret, grant_type, and code to acquire the access_token. This time, it will be different from the client credentials flow; we are going to get scope and a refresh_token
  4. Now, we can normally send requests to the Web API and if the access token has expired, we can do another request to refresh the access token and continue performing requests.

With that said, open the auth.py file in the musicterminal/pytify/auth directory and let's add a few more functions. First, we are going to add a function called _refresh_access_token; you can add this function after the get_auth_key function:

def _refresh_access_token(auth_key, refresh_token):

headers = {'Authorization': f'Basic {auth_key}', }

options = {
'refresh_token': refresh_token,
'grant_type': 'refresh_token',
}

response = requests.post(
'https://accounts.spotify.com/api/token',
headers=headers,
data=options
)

content = json.loads(response.content.decode('utf-8'))

if not response.ok:
error_description = content.get('error_description', None)
raise BadRequestError(error_description)

access_token = content.get('access_token', None)
token_type = content.get('token_type', None)
scope = content.get('scope', None)
expires_in = content.get('expires_in', None)

return Authorization(access_token, token_type, expires_in,
scope, None)

It basically does the same thing as the function handling the client credentials flow, but this time we send the refresh_token and the grant_type. We get the data from the response's object and create an Authorization, namedtuple.

The next function that we are going to implement will make use of the os module of the standard library, so before we start with the implementation, we need to add the following import statement at the top of the auth.py file:

import os

Now, we can go ahead and add a function called _authorization_code. You can add this function after the get_auth_key function with the following contents:

def _authorization_code(conf):

current_dir = os.path.abspath(os.curdir)
file_path = os.path.join(current_dir, '.pytify')

auth_key = get_auth_key(conf.client_id, conf.client_secret)

try:
with open(file_path, mode='r', encoding='UTF-8') as file:
refresh_token = file.readline()

if refresh_token:
return _refresh_access_token(auth_key,
refresh_token)

except IOError:
raise IOError(('It seems you have not authorize the
application '
'yet. The file .pytify was not found.'))

Here, we try opening a file called .pytify in the musicterminal directory. This file will contain the refresh_token that we are going to use to refresh the access_token every time we open our application.

After getting the refresh_token from the file, we pass it to the _refresh_access_token function, together with the auth_key. If for some reason we are unable to open the file or the file does not exist in the musicterminal directory, an exception will be raised.

The last modification we need to do now is in the authenticate function in the same file. We are going to add support for both authentication methods; it should look like this:

def authenticate(conf):
if conf.auth_method == AuthMethod.CLIENT_CREDENTIALS:
return _client_credentials(conf)

return _authorization_code(conf)

Now, we will start different authentication methods depending on what we have specified in the configuration file.

Since the authentication function has a reference to AuthMethod, we need to import it:

from .auth_method import AuthMethod

Before we try this type of authentication out, we need to create a small web app that will authorize our application for us. We are going to work on that in the next section.