AWS Public Sector Blog

Delegated authentication using OAuth: A case study using Spotify and AWS

A guest post by Patrick Ryan of Evans & Chambers Technology, an AWS Partner Network (APN) Public Sector Partner

Cloud-based technologies allow organizations like governments to build a new application on existing services on the internet that offers open and documented Application Programming Interfaces (APIs) to deliver reliable data. These services have an authentication model so that new users verify their identity before accessing, even if it’s offered at no cost. There are three components in this scenario: a service provider, an end user, and an application that needs to access user data.

The key technology here is OAuth. OAuth is a standard that enables access delegation. Note that OAuth is not itself a technology that does authentication. The distinction is subtle but important. OAuth allows a user to delegate some level of access to his or her data to a third-party entity without handing over complete credentials. You should never give out your access credentials, whether user ID and password, token, or biometric. Instead, grant a credential that is limited in scope and time and which can be revoked at any time.

Spotify

To support a music website that makes use of Spotify data, I wrote code to access Spotify’s full-featured API that provides access to public information on artists, tracks, playlists, and other derived data like genres. Spotify, using the OAuth terminology, supports several “scopes,” each of which allows access to read and/or write certain data. In this context, I am the “resource owner.” The “authorization server” is Spotify; the “client” is a program requesting access credentials, and the “user agent” is a program that handles the initial authentication delegation.

Spotify denotes the two authorization scenarios as “app authorization” and “user authorization.” App authorization enables temporary and partial access with an access token (or set) that I, as the fully authenticated user, generate and hand out. To use this design, I first formally register my application with Spotify. Spotify then provides an access key and secret key to include in the application code to retrieve data via the Spotify API. “User authorization” involves providing a user or application with more access, albeit still restricted, that is limited to a set amount of time. A separate refresh key replaces the old one when it expires. My application needs to upload new information to update playlists, so I chose “user authorization.”

The Spotify documentation includes sample code that shows how the whole process works. The sample login program uses Node.js. The basic approach is that you edit the sample login script to use your access tokens and the specific scopes that your application requires. After successfully authenticating, Spotify returns an access token and a refresh token. These tokens will need to be stored somewhere private so the application code can use them when invoking the Spotify API.

AWS Secrets Manager

AWS Secrets Manager provides a method to store key-value pairs and apply AWS policies around access. Secrets Manager allows you to aggregate credentials in a logical way. A “secret” is a set of one to several key-value pairs, addressed using the secret’s ARN or name. Once you store a secret, you can view the data in the console as either key-value pairs or JSON.

My application can upload a list of tracks to Spotify, replacing the contents of a playlist. I wrote the function in Ruby using AWS Lambda. I also leveraged an open source Ruby gem called rspotify that wraps the Spotify API. Since I have several functions that use rspotify, I created an AWS Lambda layer to make the code useable for all of my functions.

Two required environment variables include:

GEM_PATH                  /opt/ruby/2.5.0

SPOTIFY_ACCESS       spotify-access

The GEM_PATH is required for the function to be able to access the rspotify layer. An AWS Lambda layer is mounted under the /opt directory. I stored the secret name in the SPOTIFY_ACCESS variable named spotify-access. The role I created for the function includes access to additional services, such as Amazon Simple Storage Service (Amazon S3), includes this policy. Note that the policy shown below would grants access to any secret in the account. The best practice would be to specifically indicate which to secrets this policy provides access. The “Resource” parameter below needs to be restricted to the appropriate scope.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue",
                "secretsmanager:DescribeSecret",
                "secretsmanager:PutSecretValue"
            ],
            "Resource"arn:aws:secretsmanager:::secret:"
        }
    ]
}

Key Rotation

Key rotation is the term for managing the lifecycle of encryption keys. A properly-managed key has an expiration date. On or before the expiration date, a new key has to be generated, stored, and then provided to any service that requires use of that key.

Spotify provides me with an access token and refresh token. If my code makes an API call using an expired access token, an exception will be returned that lists “Refresh token revoked” as the error. The rspotify code is intelligent enough to silently handle that case. When that exception is raised, rspotify sends the refresh token to Spotify, requesting a new access token. Assuming that the refresh token itself is still good, the Spotify API will return a new access token. Using the token, the original API call is reinvoked.

The released version of rspotify does not provide the ability to detect and respond to the occurrence of a new access token being generated. My solution was to enhance the rspotify code and submit a pull request (PR) to the maintainer. I decided to include the ability to have an arbitrary block of code executed in the event that the access token gets refreshed.

 callback_proc = Proc.new { |new_access_token, token_lifetime |
    now = Time.now.utc.to_i  # seconds since 1/1/1970, midnight UTC
    time_to_die = now+token_lifetime
    self.save_new_access_token(new_access_token, time_to_die)
  }

 self.spotify_user = RSpotify::User.new(
   {
     'credentials' => {
        "token" => self.credentials["access_token"],
        "refresh_token" => self.credentials["refresh_token"],
        "access_refresh_callback" => callback_proc
     } ,
     'id' => self.credentials["user_id"]
   })

The save_new_access_token function is simply a wrapper around a call to the Secrets Manager API call to update the secret values. The code inside callback_proc will only get executed if the access token gets refreshed. When a new access token is requested, the Spotify API returns two values: the new access token and an integer representing the lifetime of the token in seconds.

AWS Secrets Manager includes a set of features and permissions around key rotation. When using the built-in key rotation capability, you write AWS Lambda functions to do the key generation. The functions are then called as needed via the key rotation policy.

Learn more about AWS Secrets Manager. And check out some other resources, including:

Evans & Chambers Technology is an AWS Partner Network (APN) Public Sector Partner.