Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Practice Pull Request #3

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions authorization_code/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* This is an example of a basic node.js script that performs
* the Authorization Code oAuth2 flow to authenticate against
* the Spotify Accounts.
*
* For more information, read
* https://developer.spotify.com/web-api/authorization-guide/#authorization_code_flow
*/

var express = require('express'); // Express web server framework
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So first off, what is OAuth2?
https://aaronparecki.com/oauth-2-simplified/
In short, it is a way for a program to request information and/or control over another 3rd party service in a limited way. What does this mean? Say you want to log into awesomeWebsite.com with Google. You click login, and then are redirected to google.com, where you enter your credentials. Then google.com tells awesomeWebsite.com that you are good to go.

This example is exactly that, but instead of awesomeWebsite.com, its the html file and the backend.

Note: in your design document you mentioned writing the backend in python using flask. This example is building a backend in node.js using express. The principles are the same, and you'll need a backend server no matter what.

In this file, there are a three endpoints.
/login
/callback
/refresh_token

I will explain each in-line. For OAuth you'll need all three usually.

var request = require('request'); // "Request" library
var cors = require('cors');
var querystring = require('querystring');
var cookieParser = require('cookie-parser');

var client_id = 'bba78fd3d447421381da369e658e3f92'; // Your client id
var client_secret = '0d8c939ee8f34320af774c02f42d96b5'; // Your secret
var redirect_uri = 'http://localhost:8888/callback'; // Your redirect uri

/**
* Generates a random string containing numbers and letters
* @param {number} length The length of the string
* @return {string} The generated string
*/
var generateRandomString = function(length) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just noise. Its making a big random string.

var text = '';
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

for (var i = 0; i < length; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};

var stateKey = 'spotify_auth_state';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the name of the cookie.

What are cookies? A cookie is a small key-value pair stored as a string by a browser. Web servers can set cookies onto visitors browsers. Cookies are used for lots of things including authorization and tracking.

This example uses spotify_auth_state to track when a user goes to the website, then goes to spotify's site to login, and then to go back the the website.


var app = express();

app.use(express.static(__dirname + '/public'))
.use(cors())
.use(cookieParser());

app.get('/login', function(req, res) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty straightforward endpoint. The website requests logging in with Spotify. See below.


var state = generateRandomString(16);
res.cookie(stateKey, state);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sets a cookie on the response, res. When the browser receives the response, it will store stateKey locally on the machine.


// your application requests authorization
var scope = 'user-read-private user-read-email';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oauth2's special magic is the concept of scopes.

This allows applications to request limited sets of permissions and information from another service. Here the application is requesting access to the user's email and private information.
When you go to spotify.com with this redirect, you should see a list of requested permissions.
Something like
AwesomeSite.com wants to use your email address. Yes/no

res.redirect('https://accounts.spotify.com/authorize?' +
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an express command. When the browser receives the http response, it will redirect to https://accounts.spotify.com/authorize? but it will also have information encoded at the end of the url.

This will include

      response_type: 'code', // how does the server want to get a response
      client_id: client_id, // unique id awesomeSite.com registers with spotify so spotify knows whos asking.
      scope: scope, // The permissions requested
      redirect_uri: redirect_uri, // this is the url to send a response back to. This will be awesomeSite.com/callback below
      state: state // the unique string we mentioned earlier.

Note: state and the cookie spotify_auth_state are different things, set to the same value. This is a security feature. We don't want someone maliciously pretending to be a visitor of awesomeSite.com and sending a request from a different computer. Later down, we compare state send via spotify to spotify_auth_state in the browser to make sure its the same computer the whole time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The whole redirect url might look like https://accounts.spotify.com/authorize?response_type=code&client_id=awesomeSite&scope=user-read-private_user-read-email&redirect_uri=https://awesomeSite.com/callback&state=randomString123

querystring.stringify({
response_type: 'code',
client_id: client_id,
scope: scope,
redirect_uri: redirect_uri,
state: state
}));
});

app.get('/callback', function(req, res) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a critical part of OAuth. The client software running in the app/browser makes the initial api call. They get redirected to spotify.com. Once they sign in and agree to the permissions, they users' browser will call this http endpoint to send information from spotify back to you.


// your application requests refresh and access tokens
// after checking the state parameter

var code = req.query.code || null;
var state = req.query.state || null;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pulls two bits of information out of the url.

code - This is a limited time code that basically says User X signed in and agreed to give Permission Y to App Z and it will expire in B minutes

We will use that code to talk directly with spotify's servers. Remember: browsers lie, and can be controlled by hackers. We trust server-server communication much more.

var storedState = req.cookies ? req.cookies[stateKey] : null;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OAuth2 spec says that anything you pass to the query param, state, will get passed through unchanged to your callback. That let's you keep track of your requests.


if (state === null || state !== storedState) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the state passed back doesn't match the state in the cookie, bail out.

res.redirect('/#' +
querystring.stringify({
error: 'state_mismatch'
}));
} else {
res.clearCookie(stateKey);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We clear the cookie in the users browser.

var authOptions = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the request body we send to spotify to get the actual access token. This query can be made in python with flask or in any other language.

url: 'https://accounts.spotify.com/api/token',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL spotify provides for getting api tokens.

form: {
code: code,
redirect_uri: redirect_uri,
grant_type: 'authorization_code'
},
headers: {
'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64'))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server has access to a secret code.

Usually when you sign up for developer access to spotify, you will register to get a client id and a client secret. This is how spotify knows its your application.

},
json: true
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sending json to spotify's endpoint.

};

request.post(authOptions, function(error, response, body) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is making the api request to spotify.

if (!error && response.statusCode === 200) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the statusCode is 200, we can proceed.


var access_token = body.access_token,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tada, we now have our api token!

Here it is being stored as a local variable in a node process. You would probably store this in a database, associating a user with their token.

refresh_token = body.refresh_token;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the hell is a refresh token?

OAuth2 provides limited access to other services. A key component of that is that good api tokens expire! Spotify gives you a refresh token, which you can use to renew the api token, instead of asking the user to put their password in again and do all that redirect nonsense!


var options = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example call to the spotify api using the access token of a user.
Its calling an api endpoint, setting the Authorization header to Bearer accessTokenStringThatsProbablyAJWTButWhoKnows and sending it as JSON (although this GET request has no body in the request :/)

Any call you make to the spotify api is going to include that Authorization header. You can now talk to spotify on behalf of that user, but are restricted to a limited set of actions and information! You OAuthed!

url: 'https://api.spotify.com/v1/me',
headers: { 'Authorization': 'Bearer ' + access_token },
json: true
};

// use the access token to access the Spotify Web API
request.get(options, function(error, response, body) {
console.log(body);
});

// we can also pass the token to the browser to make requests from there
res.redirect('/#' +
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tied to the html file below.

Its telling the browser to redirect, but includes the access token and refresh token in the url, which the browser can then pull out and use to make its own requests using javascript.

I don't recommend this method as we (the web application community) have invented better ways to do this. But it works with the example code

querystring.stringify({
access_token: access_token,
refresh_token: refresh_token
}));
} else {
res.redirect('/#' +
querystring.stringify({
error: 'invalid_token'
}));
}
});
}
});

app.get('/refresh_token', function(req, res) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just the logic to refresh a token after it has expired.


// requesting access token from refresh token
var refresh_token = req.query.refresh_token;
var authOptions = {
url: 'https://accounts.spotify.com/api/token',
headers: { 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64')) },
form: {
grant_type: 'refresh_token',
refresh_token: refresh_token
},
json: true
};

request.post(authOptions, function(error, response, body) {
if (!error && response.statusCode === 200) {
var access_token = body.access_token;
res.send({
'access_token': access_token
});
}
});
});

console.log('Listening on 8888');
app.listen(8888);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This turns on the webserver on port 8888

142 changes: 142 additions & 0 deletions authorization_code/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<!doctype html>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not going to go through this whole file.

Basically its an html page that uses templates and a bit of javascript.
It looks at the url to pull out spotify access tokens and uses AJAX to make http requests from the browser.

<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="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 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="text-overflow">{{access_token}}</dd>
<dt>Refresh token</dt><dd class="text-overflow">{{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 hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while ( e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}

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.access_token,
refresh_token = params.refresh_token,
error = params.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>

Loading