Spotify Authentication Flow Example With Ruby Webrick Webserver
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_state
cookie 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.