How to Write a Client Side Feed Processor Like Yahooapis
By Sahat Yalkabov, Yahoo Software Engineer
Twitter: @EvNowAndForever
GitHub: @sahat
This guide assumes you know at least some Node.js and JavaScript. Furthermore, in order to demonstrate the entire OAuth 2.0 flow from beginning to end, I have decided not to use server-side or client-side libraries such as Passport or Satellizer.
GitHub Project
Demo
Step 1. Prerequisites
- Node.js (download)
- MongoDB (download)
Step 2. Project Structure
Create a new directory, then inside create a new file calledpackage.json that will contain our package dependencies:
{
"name": "oauth2",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"body-parser": "^1.10.0",
"express": "^5.0.0-alpha.1",
"express-session": "^1.9.3",
"jade": "^1.8.2",
"mongoose": "^3.8.21",
"request": "^2.51.0"
}
}
Next, runnpm install
to install app dependencies:
In the same directory create a file called server.js and two new directories: public (for CSS, JavaScript, Images) and views (for Templates).
Inside views directory create the following files:
- _navbar.jade
- contacts.jade
- home.jade
- layout.jade
Create a new folder css inside public directory, then inside css create a file called main.css:
And here it is all together:
Step 3. Initial Express Setup
Open server.js and paste the following code:
var path = require('path');
var express = require('express');
var session = require('express-session');
var bodyParser = require('body-parser');var app = express();
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set('port', process.env.PORT || 80);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true
}));
app.use(express.static(path.join(__dirname, 'public')));app.get('/', function(req, res) {
res.send('Welcome to home page.');
});app.listen(app.get('port'), function() {
console.log('Express server listening on port ' + app.get('port'));
});
This is a very basic Express application similar to what you would get by running Express application generator.
Notice the port is set to 80. Unfortunately, at the moment of this writing you cannot set the port number on a Callback Domain when creating a new app at https://developer.apps.yahoo.com. In other words, you can't use http://localhost:3000 as a Callback Domain.
If the Callback Domain does not accept thelocalhost hostname or any port numbers, how are we supposed to develop on a local machine? A hacky solution is to map localhost to some domain name of your choice in the /etc/hosts file. For example, in the following mapping, when you visit http://myapp.com it would be the same as visiting http://localhost.
127.0.0.1 myapp.com
Let's run the application by typingsudo node server.js
in the command line, then openhttp://myapp.com in a Browser to make sure everything is working.
Note: Running a Node.js app on port 80 requires escalated privileges. Which is why you have to use
sudo
.
Step 4. Templates
In this Express project, we will have 1 layout template, 2 page templates and 1 partial template. Since this is not a Jade templating tutorial I will not be covering anything related to Jade in this section. I will assume you are already familiar with it or have read www.learnjade.com.
Open layout.jade and paste the following:
doctype html
html
head
title #{title} | My App
link(rel='stylesheet', href='//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css')
link(rel='stylesheet', href='/css/main.css')
body
include _navbar
block content
script(src='//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js')
script(src='//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js')
Nothing special here, other than using dynamic page title and including _navbar partial template. I have pre-pended the file with an underscore, but it is not necessary to do so.
Next, open _navbar.jade and paste the following:
nav.navbar.navbar-default.navbar-static-top(role='navigation')
.container-fluid
.navbar-header
button.navbar-toggle.collapsed(type='button', data-toggle='collapse', data-target='#bs-example-navbar-collapse')
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
a.navbar-brand(href='/') Yahoo!
#bs-example-navbar-collapse.collapse.navbar-collapse
ul.nav.navbar-nav
li
a(href='/') Home
li
a(href='/contacts') Contactsform.navbar-form.navbar-left(role='search')
.form-group
input.form-control(type='text', placeholder='Search')
|
button.btn.btn-default(type='submit') Searchif user
ul.nav.navbar-nav.navbar-right
li
a(href='/logout') Logout
else
ul.navbar-form.navbar-right
li
a.btn.btn-default(href='/auth/yahoo') Sign in with Yahoo
This Bootstrap Navbar is almost straight out of http://getbootstrap.com/components/ with just a few minor changes. I have used html2jade.org to quickly convert HTML to Jade markup.
Two page templates to go! Open contacts.jade and paste the following:
extends layoutblock content
.container
table.table.table-striped.table-hover.table-bordered
thead
tr
th Contact Name
tbody
for contact in contacts
tr
if contact.type == 'name'
td #{contact.value.givenName} #{contact.value.familyName}
else
td #{contact.value}
After using Handlebar,js templates at work, I am starting to appreciate how nice it is to make simple equality comparisons above without having to create additional helpers, as it is the case with Handlebars.
Lastly, open home.jade and paste the following:
extends layoutblock content
if user
.bs-docs-header
.container
h1 Welcome, #{user.firstName}
p Using GUID and Access Token information below you can query various Yahoo APIs..container
if user
h3 Profile Image
img(src='#{user.profileImage}', width=92, height=92)
h3 Email
span= user.email
h3 GUID
span= user.guid
h3 Access Token
div.long-text #{user.accessToken}
else
.lead Please sign-in to view profile information.
To display the page replace the "/" route with the following code:
app.get('/', function(req, res) {
res.render('home', {
title: 'Home',
user: req.session.user
});
});
We are using express-session to persist the signed-in state between page transitions. Initially req.session.user
will be undefined since we haven't stored anything in the session yet, at least not until Step 6 when we get to the OAuth 2.0 authorization.
Let's refresh the Browser to see what we have so far.
I have prepared some styles to make the app a little prettier. Paste the following into main.css file:
body {
-webkit-font-smoothing: antialiased;
}.navbar-default .navbar-toggle {
background: #404040;
color: rgba(255, 255, 255, 0.7);
border: 1px solid #262626;
}.navbar-default .navbar-toggle:hover,
.navbar-default .navbar-toggle:focus {
background-color: #4a4a4a;
}.navbar-default {
background-color: #333;
border-color: #1a1a1a;
}.navbar-default .navbar-collapse,
.navbar-default .navbar-form {
border: 0;
}.navbar-default .navbar-nav > li > a {
color: rgba(255, 255, 255, 0.7);
transition: color 0.1s linear;
}.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus {
color: #fff;
}.navbar-default .navbar-nav > .active > a,
.navbar-default .navbar-nav > .active > a:hover,
.navbar-default .navbar-nav > .active > a:focus {
color: #fff;
background-color: transparent;
}.navbar-default .navbar-brand {
color: #fff;
}.navbar-default .navbar-brand:hover,
.navbar-default .navbar-brand:focus {
color: #fff;
}.navbar-form .form-control {
background: #404040;
border: 1px solid #262626;
padding: .5em .8em;
font-size: .9em;
color: rgba(255, 255, 255, 0.7);
}.btn-default {
border: 0;
background-color: #477DCA;
color: #fff;
font-weight: bold;
transition: all 0.1s linear;}
.btn-default:hover,
.btn-default:focus {
background-color: #2c5999;
color: white;
}.bs-docs-header {
position: relative;
padding: 30px 15px;
color: #cdbfe3;
text-align: center;
text-shadow: 0 1px 0 rgba(0, 0, 0, .1);
background: #6f5499 linear-gradient(to bottom, #563d7c 0, #6f5499 100%) repeat-x;
}.bs-docs-header {
margin-top: -20px;
padding-top: 60px;
padding-bottom: 60px;
font-size: 24px;
text-align: left;
}.bs-docs-header h1 {
margin-top: 0;
color: #fff;
}.long-text {
word-wrap: break-word;
}
Refresh the Browser one more time to see the new styles:
Step 5. User Schema and Database
We will be using MongoDB and Mongoose to store user accounts. Given that we have already installed all NPM dependencies all we really need to do is add a "require" statement with the rest of dependencies in server.js:
var mongoose = require('mongoose');
Right below that, add theUser Mongoose schema:
var userSchema = new mongoose.Schema({
guid: String,
email: String,
profileImage: String,
firstName: String,
lastName: String,
accessToken: String
});
Each schema maps to a MongoDB collection. And each key - guid, email, accessToken and etc., defines a property in MongoDB documents. For example, this is how a User document would look in the DB:
A schema is just a representation of your data in MongoDB. This is where you can enforce a certain field to be of a certain type. A field can also have constraints such as required, unique or only contain certain characters.
To use ouruserSchema
, we need to convert it into a Model we can work with. Add this line right after the User schema:
var User = mongoose.model('User', userSchema);
Before we can interact with the database, we must first connect to one. If you already have MongoDB installed on your machine and it is up and running, then simply add this line somewhere in theserver.js
. I typically place it right before or right aftervar app = express();
.
mongoose.connect(process.env.MONGODB || 'localhost');
By using environment variables above you can use two different sets of Client IDs, Client Secrets, Redirect URIs and Databases for production and development modes without changing a single line of code.
For example, this is how you would set up those environment variables on Heroku:
Ok, we are done here. Let's move on to the core topic of this post.
Step 6. Yahoo OAuth 2.0
Before continuing, go to https://developer.apps.yahoo.com and create a new app with the following permissions and access levels:
- Profiles - Read/Write Public and Private
- Contacts - Read
For Callback Domain use myapp.com. This is a little different from Redirect URI that you see on most other OAuth providers. Callback Domain only cares about the domain part and not full URL path.
On a related note, I am currently working on rewriting the entire app creation front-end in React. Here is a sneak peak at the new app creation page:
Note: These are not final design changes and are likely to change before the final release.
Once you obtain Client ID and Client Secret we are ready to move on.
Note: They are currently referred asConsumer Key and Consumer Secret on the existing app creation page, since OAuth 2.0 support was fairly recent addition.
Add the following code somewhere before Express routes. Alternatively you can place this code in a separate config.js file, however, I have decided to include it insideserver.js for simplicity purposes:
var clientId = process.env.CLIENT_ID || 'Your Client ID';
var clientSecret = process.env.CLIENT_SECRET || 'Your Client Secret';
var redirectUri = process.env.REDIRECT_URI || 'http://myapp.com/auth/yahoo/callback';
Note: You will have to update the http://myapp.com domain above if you have a different mapping in /etc/hosts.
For route names I have used the same route naming convention as in Passport.js examples.
Add the following Express route that will display the authorization screen:
app.get('/auth/yahoo', function(req, res) {
var authorizationUrl = 'https://api.login.yahoo.com/oauth2/request_auth';var queryParams = qs.stringify({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code'
});res.redirect(authorizationUrl + '?' + queryParams);
});
So, now when you go to http://myapp.com/auth/yahoo you should see the following page:
When a user clicks Agree,Yahoo will redirect back to the redirectUri specified above plus the code parameter, e.g./auth/yahoo/callback?code=bkwsqf8. Take a look at the OAuth 2.0 Authorization Code Requests and Responses to learn more about the OAuth 2.0 flow. It is very short and to the point.
Next, add the following Express route for handling the OAuth 2.0 redirect:
app.get('/auth/yahoo/callback', function(req, res) {
var accessTokenUrl = 'https://api.login.yahoo.com/oauth2/get_token';var options = {
url: accessTokenUrl,
headers: { Authorization: 'Basic ' + new Buffer(clientId + ':' + clientSecret).toString('base64') },
rejectUnauthorized: false,
json: true,
form: {
code: req.query.code,
redirect_uri: redirectUri,
grant_type: 'authorization_code'
}
};// 1. Exchange authorization code for access token.
request.post(options, function(err, response, body) {
var guid = body.xoauth_yahoo_guid;
var accessToken = body.access_token;
var socialApiUrl = 'https://social.yahooapis.com/v1/user/' + guid + '/profile?format=json';var options = {
url: socialApiUrl,
headers: { Authorization: 'Bearer ' + accessToken },
rejectUnauthorized: false,
json: true
};// 2. Retrieve profile information about the current user.
request.get(options, function(err, response, body) {// 3. Create a new user account or return an existing one.
User.findOne({ guid: guid }, function(err, existingUser) {
if (existingUser) {
req.session.user = existingUser;
return res.redirect('/');
}var user = new User({
guid: guid,
email: body.profile.emails[0].handle,
profileImage: body.profile.image.imageUrl,
firstName: body.profile.givenName,
lastName: body.profile.familyName,
accessToken: accessToken});
user.save(function(err) {
req.session.user = user;
res.redirect('/');
});
});
});
});
});
Note: To learn more about about all Yahoo OAuth 2.0 parameters and error responses, please see Yahoo OAuth 2.0 Guide.
When Yahoo redirects back to /auth/yahoo/callback it will append a short-lived authorization code as a query parameter that needs to be exchanged for an access token. Without an access token we cannot obtain user's profile information.
Primary responsiblities of this route are to exchange this code for an access token, retrieve profile information, create a new user account or return an existing account. (returning user)
You may have noticed rejectUnauthorized: false
request option. It is there to get aroundError: certificate not trusted request error. It started happening around the same time as I upgraded Node.js 0.10 to io.js. I have not had the chance to track down the source of this problem, but it is most likely due to installing io.js on my machine.
Step 7. Logout
Logout route simply deletes the session and redirects the user back to home page:
app.get('/logout', function(req, res) {
delete req.session.user;
res.redirect('/');
});
There is really nothing more to it.
Step 8. Contacts API
I have added this section as a bonus just to show you how easy you can fetch your Yahoo contacts once you have an access token.
Since we already have an access token, obtaining user's contacts is just a matter of hitting the correct API endpoint. Although, I admit, finding documentation on how to pass that access token to the Contacts API can be challenging. To further complicate the matter, every provider does it slightly differently. In Yahoo's case it is sent via the Authorization header, whereas some other providers require you to include access token as part of the query parameter or the POST body.
app.get('/contacts', function(req, res) {
if (!req.session.user) {
return res.redirect('/auth/yahoo');
}var user = req.session.user;
var contactsApiUrl = 'https://social.yahooapis.com/v1/user/' + user.guid + '/contacts';var options = {
url: contactsApiUrl,
headers: { Authorization: 'Bearer ' + user.accessToken },
rejectUnauthorized: false,
json: true
};request.get(options, function(err, response, body) {
var contacts = body.contacts.contact.map(function(contact) {
return contact.fields[0];
});res.render('contacts', {
title: 'Contacts',
user: req.session.user,
contacts: contacts
});
});
});
Step 9. Conclusion
I hope this tutorial will be useful to you in one way or another.
More importantly, I wanted to write a tutorial that did not rely on third-party libraries that do the entire OAuth flow for you. Sometimes it is good to understand how things work at a lower level to be able to contribute, make pull requests to libraries that work at a higher level, or just for the sake of curiosity.
You can reach out to me on Twitter at @EvNowAndForever or send me an email at sahat@me.com with any questions or comments.
Source: https://yahoodevelopers.tumblr.com/post/105969451213/implementing-yahoo-oauth2-authentication
0 Response to "How to Write a Client Side Feed Processor Like Yahooapis"
Post a Comment