-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
var app = express(); | ||
|
||
app.use(express.static(__dirname + '/public')) | ||
.use(cors()) | ||
.use(cookieParser()); | ||
|
||
app.get('/login', function(req, res) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sets a cookie on the response, |
||
|
||
// your application requests authorization | ||
var scope = 'user-read-private user-read-email'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
res.redirect('https://accounts.spotify.com/authorize?' + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 This will include
Note: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whole redirect url might look like |
||
querystring.stringify({ | ||
response_type: 'code', | ||
client_id: client_id, | ||
scope: scope, | ||
redirect_uri: redirect_uri, | ||
state: state | ||
})); | ||
}); | ||
|
||
app.get('/callback', function(req, res) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pulls two bits of information out of the url.
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The OAuth2 spec says that anything you pass to the query param, |
||
|
||
if (state === null || state !== storedState) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We clear the cookie in the users browser. |
||
var authOptions = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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')) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Any call you make to the spotify api is going to include that |
||
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('/#' + | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This turns on the webserver on port 8888 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
<!doctype html> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
<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> | ||
|
There was a problem hiding this comment.
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.