Spotify Authentication Flow Example With Ruby Webrick Webserver

Amir
CodeX
Published in
7 min readJan 15, 2023

This tutorial will replace Node.js with Ruby in the Authentication Flow example app for Spotify Web API.

This flow first gets a code from the Spotify Accounts Service, then exchanges that code for an access token. The code-to-token exchange requires a secret key, and for security is done through direct server-to-server communication.

In this example we retrieve data from the Web API /me endpoint, that includes information about the current user.

Here we will use Ruby for server-side applications.

Setup Webserver

Many web server gems could accept and process requests. In this tutorial, I tried using Webrick.

WEBrick is an HTTP server toolkit that can be configured as an HTTPS server, a proxy server, and a virtual-host server.

First, you need to initialize the Gemfile and add webrick gem.

$ bundle init

Then add gem 'webrick' to your Gemfile and run bundle install to install the gem. Your Gemfile should look like this.

# frozen_string_literal: true

source 'https://rubygems.org'

gem 'webrick'

Then you should create a server.rb file so we can start building the web server.

# server.rb

require 'webrick'

port = 8000
server = WEBrick::HTTPServer.new Port: port

We are going to add a RequestHandler class to our web server and kill the server with crtl+c .

# server.rb
...

server.mount '/', RequestHandler

trap 'INT' do server.shutdown end

server.start

RequestHandler

# request_handler.rb


# frozen_string_literal: true

# Handle the incoming requests
class RequestHandler < WEBrick::HTTPServlet::AbstractServlet
def do_GET(request, response)
method_name = "process_#{request.path.delete_prefix('/')}".to_sym
send(method_name, request, response)
rescue NoMethodError
raise 'Not Found'
end
end

We added a simple web servlet that will process the requests. This will send the request and response objects to a method; otherwise, raise an error which means that the route doesn’t exist. Also, we need to require this file in our server file.

#server.rb
require 'webrick'

require_relative 'request_handler'


port = 8000

server = WEBrick::HTTPServer.new Port: port

server.mount '/', RequestHandler

trap 'INT' do server.shutdown end

server.start

Adding Actions

We will add basic actions so the user can see a Login With Spotify button, click on it, and be redirected to Spotify to grant access to our application and return. I’m using the OAuth examples on the Spotify Web API document page for the HTML pages.

Index

First, create a templates directory and add an index.html to it with the content below.

<!-- templates/index.html -->
<!doctype html>
<html>
<head>
<title>Example of the Authorization Code flow with Spotify</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<style type="text/css">
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 500px;
}
</style>
</head>

<body>
<div class="container">
<div id="login">
<h1>This is an example of the Authorization Code flow</h1>
<a href="/login" class="btn btn-primary">Log in with Spotify</a>
</div>
</div>
</body>
</html>

Now we will add the request handler for this. Create a request_handlers directory and add index.rb file. With the logic we had in request_handler.rb file, we need a method called process_ACTION-NAME to handle the request, which in this case, will be process_ and the action name will be empty.

# request_handlers/index.rb

# frozen_string_literal: true

def process_(_, response)
result = File.read('templates/index.html')
response.status = 200
response['Content-Type'] = 'text/html'
response.body = body
end

After this, we need to modify our server.rb file and require this new ruby file.

#server.rb
...

require_relative 'request_handler'
require_relative 'request_handlers/index'

...

Also, I moved a couple of lines to a helper called response.rb for rendering HTML pages.

# helpers/response.rb

# frozen_string_literal: true

def render_html(body, response)
response.status = 200
response['Content-Type'] = 'text/html'
response.body = body
end
# request_handlers/index.rb

# frozen_string_literal: true

def process_(_, response)
result = File.read('templates/index.html')
render_html(result, response)
end
# server.rb
...
require_relative 'helpers/response'
...

Now, if everything is fine you could start the web server with $ ruby server.rb command and in your browser http://localhost:8000 should serve something like this.

Now, this button will send the user to localhost:8000/login which will redirect the user to the Spotify website. Now let’s add the logic for /login .

Login Action

We have followed the details mentioned in Spotify Authorization Code Flow documentation.

# request_handlers/login.rb

# frozen_string_literal: true

def process_login(_, response)
random_string = SecureRandom.hex
response.cookies.push WEBrick::Cookie.new('spotify_auth_state', random_string)

params = {
response_type: 'code',
client_id: $client_id,
scope: $scopes,
redirect_uri: $callback_url,
state: random_string
}
uri = URI('https://accounts.spotify.com/authorize')
uri.query = URI.encode_www_form(params)

response.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, uri.to_s)
end
# server.rb

...
require 'securerandom'
require_relative 'request_handlers/login'
...
$scopes = %w[ user-read-private
playlist-read-private
playlist-modify-private].join(' ')

$client_id = 'YOUR_APP_CLIENT_ID'
$client_secret = 'YOUR_APP_CLIENT_SECRET
$callback_url = "http://localhost:#{port}/callback"

You need to add your app client id and client secret here. Also, change the required scopes based on Spotify Authorization Scopes.

Now you need to restart the webserver and now click on Login with Spotify button and should be redirected to the Spotify website for login and granting access.

When you finish, you’ll be redirected back to http://localhost:8000/callback which we are going to add next.

Callback Action

Whenever the user returns to the callback action, we need to obtain a token with the given code in the query params.

If the user accepted your request, then your app is ready to exchange the authorization code for an Access Token. It can do this by making a POST request to the /api/token endpoint.

# request_handlers/callback.rb

# frozen_string_literal: true

def process_callback(request, response)
state_cookie = request.cookies.select { |c| c.name == 'spotify_auth_state' }.first
raise 'error' unless state_cookie.value == request.query['state']

data = {
code: request.query['code'],
grant_type: 'authorization_code',
redirect_uri: $callback_url
}

res = RestClient.post(
'https://accounts.spotify.com/api/token',
data,
{
Authorization: "Basic #{Base64.strict_encode64($client_id + ':' + $client_secret)}"
}
)

result = JSON.parse(res.body)

params = {
access_token: result['access_token'],
refresh_token: result['refresh_token']
}
uri = URI('/me')
uri.query = URI.encode_www_form(params)

response.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, uri.to_s)
end

Also, we need to modify our server.rb file to require more items.

# server.rb

...
require 'rest-client'
require 'json'
require 'base64'
...
require_relative 'request_handlers/callback'
...

Firstly, we check the given state with the spotify_auth_statecookie to ensure the user is the same; otherwise, it’s an error, and we should not proceed. Then we need to get the token from Spotify API and redirect the user to a profile page called /me .

me Action

First, we added the HTML page from OAuth examples on the Spotify Web API documentation page.

<!-- templates/me.html -->
<!doctype html>
<html>
<head>
<title>Example of the Authorization Code flow with Spotify</title>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<style type="text/css">
#login, #loggedin {
display: none;
}
.text-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 500px;
}
</style>
</head>

<body>
<div class="container">
<div id="loggedin">
<div id="user-profile">
</div>
<div id="oauth">
</div>
<button class="btn btn-default" id="obtain-new-token">Obtain new token using the refresh token</button>
</div>
</div>

<script id="user-profile-template" type="text/x-handlebars-template">
<h1>Logged in as {{display_name}}</h1>
<div class="media">
<div class="pull-left">
<img class="media-object" width="150" src="{{images.0.url}}" />
</div>
<div class="media-body">
<dl class="dl-horizontal">
<dt>Display name</dt><dd class="clearfix">{{display_name}}</dd>
<dt>Id</dt><dd>{{id}}</dd>
<dt>Email</dt><dd>{{email}}</dd>
<dt>Spotify URI</dt><dd><a href="{{external_urls.spotify}}">{{external_urls.spotify}}</a></dd>
<dt>Link</dt><dd><a href="{{href}}">{{href}}</a></dd>
<dt>Profile Image</dt><dd class="clearfix"><a href="{{images.0.url}}">{{images.0.url}}</a></dd>
<dt>Country</dt><dd>{{country}}</dd>
</dl>
</div>
</div>
</script>

<script id="oauth-template" type="text/x-handlebars-template">
<h2>oAuth info</h2>
<dl class="dl-horizontal">
<dt>Access token</dt><dd class="">{{access_token}}</dd>
<dt>Refresh token</dt><dd class="">{{refresh_token}}</dd>
</dl>
</script>

<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/2.0.0-alpha.1/handlebars.min.js"></script>
<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
<script>
(function() {

/**
* Obtains parameters from the hash of the URL
* @return Object
*/
function getHashParams() {
var params = new URLSearchParams(window.location.search);

return params;
}

var userProfileSource = document.getElementById('user-profile-template').innerHTML,
userProfileTemplate = Handlebars.compile(userProfileSource),
userProfilePlaceholder = document.getElementById('user-profile');

var oauthSource = document.getElementById('oauth-template').innerHTML,
oauthTemplate = Handlebars.compile(oauthSource),
oauthPlaceholder = document.getElementById('oauth');

var params = getHashParams();

var access_token = params.get('access_token'),
refresh_token = params.get('refresh_token'),
error = params.get('error');

if (error) {
alert('There was an error during the authentication');
} else {
if (access_token) {
// render oauth info
oauthPlaceholder.innerHTML = oauthTemplate({
access_token: access_token,
refresh_token: refresh_token
});

$.ajax({
url: 'https://api.spotify.com/v1/me',
headers: {
'Authorization': 'Bearer ' + access_token
},
success: function(response) {
userProfilePlaceholder.innerHTML = userProfileTemplate(response);

$('#login').hide();
$('#loggedin').show();
}
});
} else {
// render initial screen
$('#login').show();
$('#loggedin').hide();
}

document.getElementById('obtain-new-token').addEventListener('click', function() {
$.ajax({
url: '/refresh_token',
data: {
'refresh_token': refresh_token
}
}).done(function(data) {
access_token = data.access_token;
oauthPlaceholder.innerHTML = oauthTemplate({
access_token: access_token,
refresh_token: refresh_token
});
});
}, false);
}
})();
</script>
</body>
</html>

Now we need an action to render this page.

# request_handlers/me.rb

# frozen_string_literal: true

def process_me(_, response)
result = File.read('templates/me.html')
render_html(result, response)
end
# server.rb
...
require_relative 'request_handlers/me'
...

Now, if you restart your web server and grant access to the application with your Spotify account, in the end, you should see a page like this.

Refresh Token Action

Each Access Token is valid for 1 hour, and you need to obtain a new access token with your refresh token. On this page, there is a Obtain new token using the refresh token button, which will make a request to our web server and ask for a new access token. The action is called /refresh_token and need to return the response with application/json Content-Type header.

First, we are going to add render_json method to our helper.

# helpers/response.rb
...

def render_json(body, response)
response.status = 200
response['Content-Type'] = 'application/json'
response.body = body
end

Now we are going to add the refresh_token action and modify our server.rb file as well.

# request_handlers/refresh_token.rb


def process_refresh_token(request, response)
url = 'https://accounts.spotify.com/api/token'
data = {
refresh_token: request.query['refresh_token'],
grant_type: 'refresh_token'
}
res = RestClient.post url,
data,
{
Authorization: "Basic #{Base64.strict_encode64($client_id + ':' + $client_secret)}"
}

render_json(res.body, response)
end
# server.rb

...
require_relative 'request_handlers/refresh_token'
...

You can finally clone all the files from azolf/spotify-web-api-auth-examples-ruby repository on Github.

--

--

Amir
CodeX
Writer for

Tech enthusiast with a love for Football, Formula 1, and Snooker. A fan of Real Madrid, Mercedes AMG Petronas F1 Team, Ronnie O'Sullivan, and Coldplay.