In [5]:
import requests

import os
import os.path
import sys
import importlib
import webbrowser
import base64
from datetime import datetime, timedelta
from requests.auth import HTTPBasicAuth

if os.path.isdir(os.path.join("../../..", "modules")):
    module_dir = os.path.join("../../..", "modules")
else:
    module_dir = os.path.join("../..", "modules")

module_path = os.path.abspath(module_dir)
if not module_path in sys.path:
    sys.path.append(module_path)
In [6]:
import util
importlib.reload(util)
Out[6]:
<module 'util' from '/Users/bressoud/Dropbox/cs181-DataSystems/cs181-bressoud/f20_class/modules/util.py'>

Pre-Stage

The following link is where Spotify provides instructions for the Authorization Code Flow, which is the flow covered in class to obtain delegated authority. We provide our own summary of Spotify-specific steps, and these presume that you have already created a Spotify account.

https://developer.spotify.com/documentation/web-api/quick-start/

Spotify Developer Steps

  1. Go to https://developer.spotify.com/dashboard/login, logging in to authenticate yourself as a client developer.

  2. On the Dashboard, use the Create A Client ID or, equivalently, click the My New App template box.

  3. In the "Create an App" dialog, give your app a name, perhaps something like cs181-<login>, using your Denison login.

    • Add whatever description you like.
    • Click the checkboxes.

    • You should see a message that your application has been created, and be on a page dedicated to that application.

  4. Click the Edit Settings button and fill in the form as follows:

    • For Website, I use https://datasystems.denison.edu. You can use the same, or you can use some other web site that you are affiliated with. For some API providers, they check to see if the web site exists.

    • For Redirect URIs, enter https://caileighmarshall.github.io/cs181project/ (including the trailing slash), and click Add.

    • Click Save to save your application settings edits.

  5. On your application page, click Show Client Secret, and leave the page up for the next set of steps.

Record Credentials Information

  1. In the same folder as this notebook, right-click on creds.json and Open With and select Editor.

  2. Verify that, for the "spotify" entry, that the "redirect_uri" entry has the value "https://caileighmarshall.github.io/cs181project/".

  3. For the "client_id" entry, copy and paste the 32 character Client ID from your Application Dashboard as a string for that entry.

  4. For the "client_secret" entry, copy and paste the 32 character Client Secret from your Application Dashboard as a string for that entry.

  5. Save your changes to the creds.json file. You may want to open it using the JSON navigator to make sure your JSON syntax is correct.

If the read_creds() function invocation in the cell below is not successful, check your steps again.

In [7]:
creds = util.read_creds("spotify")

Stage 1: Obtain a Code

Build URL for User Auth

The following cells are going to be used to construct a URL. An HTTP GET would be performed at this URL by the Resource Owner, taking them to a Spotify authentication and dialog, where they are asked if they approve of a particular application being able to perform certain categories of operations with their (the resource owner's) Spotify account. The user can either approve, or can deny the application the ability to do these operations. This corresponds to Step 2 of the OAuth dance.

In the cell below, we create a random string that will be used for the state parameter in the flow that follows. Note that we use util.update_creds() after we update the in-memory creds data structure, so that we do not have to manually edit the creds.json file.

In [8]:
state = util.random_string()
creds['state'] = state
util.update_creds("spotify", creds)

The link below takes you to the Spotify documentation, which you should use to determine and set

  • resource path
  • location
  • query parameters

in the cell that follows, providing the information needed to build the URL.

https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow

In [9]:
url = util.buildURL("", "")
paramsD = {
}
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-9-326d5aff3e92> in <module>
----> 1 url = util.buildURL("", "")
      2 paramsD = {
      3 }

~/Dropbox/cs181-DataSystems/cs181-bressoud/f20_class/modules/util.py in buildURL(resource_path, host, protocol, extension, port)
      7 def buildURL(resource_path, host="httpbin.org", protocol="https", 
      8              extension=None, port=None):
----> 9     if resource_path[0] != '/':
     10         resource_path = '/' + resource_path
     11 

IndexError: string index out of range

The cell below build the request itself, passing the url and the paramsD as part of assembling a GET HTTP request.

In [27]:
s = requests.Session()
req = requests.Request('GET', url, params=paramsD)

prepped = s.prepare_request(req)
user_url = prepped.url
print(user_url)
https://accounts.spotify.com/authorize?client_id=f04803fe20d94f03ab03cf7e15acae96&response_type=code&redirect_uri=https%3A%2F%2Fcaileighmarshall.github.io%2Fcs181project%2F&state=YAHQDT9D&show_dialog=true

After you run the next cell, and you (presumably) approve (i.e. you give delegated authority to the application), you will be redirected to the redirect_uri site. When you get there, copy the long string of characters, called a code and displayed in red, and then come back to this notebook.

In [28]:
webbrowser.open(user_url)
Out[28]:
True

Communicate Code from User to App

For a regular application, the redirect_uri would take the resource owner to a web server under the control of the app, which would take the code provided, and put it in a database.

We simulate that with copy-and-paste. This part corresponds to Step 5 and Step 6 of the OAuth dance.

Copy the code between the string delimiters, and then execute the following cell.

In [69]:
code = ""

Our Equivalent of conveying, through the user, the code generated by the provider and approved by the user, and storing it in our "database" of our credentials file.

In [30]:
creds['code'] = code
util.update_creds("spotify", creds)

Stage 2: Exchange Code for Token and Refresh Token

The code is not enough. Although the app has the code, and it has been securely coveyed via an authenticated user, the application must securely authenticate and interact with the provider to obtain the token, which is then sufficient for API requests.

In the same documentation page as before (), scroll down to their step 2, titled "Have your application request refresh and access tokens". This request and response correspond to Step 7 and Step 8 of the OAuth dance.

Using the documentation, fill in the parameters for buildURL and for the POST body as a dictionary mapping parameters to values.

For specifying the client id and client secret, the documentation specifies two alternatives, in one, we assemble a header line that encodes the information, in the other, we include two more POST FORM parameter values. Since we are already specifying a dictionary for the POST FORM, we will use the second alternative.

In [31]:
url = util.buildURL("", "")

formD = {
}

The following uses your information above to assemble a POST HTTP request, and then looks at the url and body from the assembled request.

In [42]:
s = requests.Session()
req = requests.Request('POST', url, data=formD)

prepped = s.prepare_request(req)
In [43]:
prepped.url
Out[43]:
'https://accounts.spotify.com/api/token'
In [44]:
prepped.body
Out[44]:
'grant_type=authorization_code&code=AQAkp6y8f-3X-0gIh-2Dh2DB_F6D5Jz62SKDgqeQBMoxkkvziCpzERziiTGe0y9VgN7OnVdr9OiYXwBG86B1tAm9o97Sk7DVFaJ6Rke8qfRic2RXNaWprA6GcjE5wY0o8EPvW3o9C3nOnw0vQb6KbuIkf6CL6BXvqW-n7YWHBjhi2StRTCKVLPLz9EWSNq-0mdntVV4Rt_pJFOgP7NcEABM-D_PX6A&redirect_uri=https%3A%2F%2Fcaileighmarshall.github.io%2Fcs181project%2F&client_id=f04803fe20d94f03ab03cf7e15acae96&client_secret=53082b3a33d44daf9e9ba6f6d0380577'

Finally, we send the request to exchange the code for a token. If there was a problem with the assembled request, we print out the status code along with the body of the response, which often has information that can be used to help debug the problem.

In [45]:
response = s.send(prepped)
if response.status_code != 200:
    print("Error.  Status code:", response.status_code)
    print(response.text)
In [46]:
print(response.json())
{'access_token': 'BQC5xOgNFovK6tgTHlhlOssHVIEUIHMSMqIl8K_LUGLD6Y6UQPq5AkDY1ZykBnw8jr7LRuTvwkKfLx-bi5eK62jiZFYwlMOF9-3vvESmTykHCcnvmhdIQMhHxAiCcFvdmEWVqxf3bkCYP7An1g-9P0mpCCKsIHmOyA', 'token_type': 'Bearer', 'expires_in': 3600, 'refresh_token': 'AQCbIFI0NX7_JopVtbhwL5ksNjADSTYnexBvFhqLWkuWj26FqJHFqJUrKm6ZYKvMON5MGBhJflSU0haxprprrluustVYbz1Z2zYY-iGh0KOzQlKdoBv1O0pgwpxiJA3vRP4', 'scope': ''}
In [47]:
tokenD = response.json()

We then update our "database" with the access_token and the other information needed to make data requests:

In [77]:
creds['token'] = tokenD['access_token']
creds['refresh'] = tokenD['refresh_token']
creds['scope'] = tokenD['scope']
expiration = datetime.now() + timedelta(seconds=tokenD['expires_in'])
creds['expires'] = str(expiration)
util.update_creds("spotify", creds)

Try Out Token

In the following, we use a request to get the current user's profile, as documented here: https://developer.spotify.com/documentation/web-api/reference/users-profile/get-current-users-profile/

The following cell is like other API requests we have seen, we set up an endpoint and a location, and, in addition to any other request parameters we might need as Path Parameters, Query Parameters, Header Parameters, or POST-body Parameters, we include a Header parameter, where the header name is Authorization, and the value is "Bearer <access_token>". While other providers that use OAuth2 for delegated authority must have a request parameter that conveys the access token, it could take a different form, like a query parameter, for instance.

In [ ]:
creds = util.read_creds("spotify")
assert 'token' in creds
In [ ]:
my_token = creds['token']
In [49]:
location = "api.spotify.com"
resource_path = "/v1/me"
url = buildURL(resource_path, location)

authHeader = "Bearer {}".format(my_token)
headerD = {"Authorization": authHeader}

response=requests.get(url, headers=headerD)
if response.status_code != 200:
    print("Error.  Status code:", response.status_code)
    print(response.text)
In [50]:
response.json()
Out[50]:
{'display_name': 'David',
 'external_urls': {'spotify': 'https://open.spotify.com/user/ds79j7dgug3xsp4n3xgy22bo2'},
 'followers': {'href': None, 'total': 0},
 'href': 'https://api.spotify.com/v1/users/ds79j7dgug3xsp4n3xgy22bo2',
 'id': 'ds79j7dgug3xsp4n3xgy22bo2',
 'images': [],
 'type': 'user',
 'uri': 'spotify:user:ds79j7dgug3xsp4n3xgy22bo2'}

Refresh Token

In [53]:
creds = read_creds("spotify")
assert 'token' in creds
assert 'refresh' in creds

location = "accounts.spotify.com"
resource_path = "/api/token"
url = buildURL(resource_path, location)

auth = HTTPBasicAuth(creds['client_id'], creds['client_secret'])
bodyD = {
    'grant_type': 'refresh_token',
    'refresh_token': creds['refresh']
}

response = requests.post(url, auth=auth, data=bodyD)
if response.status_code != 200:
    print("Error.  Status code:", response.status_code)
    print(response.text)
In [54]:
response.json()
Out[54]:
{'access_token': 'BQDVrwoj7e6cVvr-r6zDTbP3R2_o6X_hzzj1nu3qwnA5Q3EV_4kvCZRTHDs7HSZx5El_JuR07nRSIaib56ZR6uQtw1GZkWI_5zwUPIViVTA5NZwazrRraorPCDXvnb5FqZzhuuz2chQ8wRmGpEZ_lUX0VyoTnLAXFg',
 'token_type': 'Bearer',
 'expires_in': 3600,
 'scope': ''}
In [60]:
creds['token'] = tokenD['access_token']
del creds['refresh']
creds['scope'] = tokenD['scope']
expiration = datetime.now() + timedelta(seconds=tokenD['expires_in'])
creds['expires'] = str(expiration)
update_creds("spotify", creds)
In [ ]: