Jribbble Three and Helping People OAuth
In December 2017, Dribbble announced version two of their API. In the same post, they set March 26, 2018 as the deprecation date of version one. Iâve maintained Jribbble for almost 8 years now. Itâs a JavaScript library to help fetch data from the Dribbble API. In that time itâs gone through plenty of iteration to keep up with Dribbble API changes. Jribbble 3.0 continues that trend.
To date, Dribbble API changes have been additive. API features have given users more access to more of data via the API. As Dribbble explained in their post, version two goes the other way, it removes a lot of functionality.
The reduced functionality seems fine though. In my time working on Jribbble, most conversations Iâve had with designers are about using it to get their own shots. Thatâs still available, and the focus of API version two. My guess is most users of Jribbble wonât lose out. They will need to make code changes to continue using Jribbble though.
Iâve had a few conversations with folks on GitHub and email about updating to Jribbble 3.0. The biggest change is that Jribbble is no longer a jQuery plugin. Itâs now a standalone library. I write more about the decision to remove jQuery later in this post.
The other big change is the shape of Jribbbleâs public API methods. Hereâs a 2.x vs 3.x example of a user getting their own shots with Jribbble:
// Jribbble 2.x
$.jribbble.setToken("<access_token>");
$.jribbble.users("tylergaw").shots().then(function(shots) { /* Work with shots JSON */ });
// Jribbble 3.x
jribbble.setToken("<access_token>");
jribbble.shots(function(shots) { /* Work with shots JSON */ });
The Jribbble API is no longer chainable or promise-based. Again, I detail more of the code decisions later in this post. We can still do the same thing, but now in a more concise way.
A New Barrier to Entry
With Dribbble API version one, we could make requests using read-only client access tokens. When creating an application via the Dribbble UI, they would generate the client access token. We could then use that token to make requests to the API.
In version two, those easy-to-use, auto-generated client access tokens are gone. Now, all requests must use a bearer token. To get a bearer token, you have to go through the OAuth2 flow.
Depending on your experience with OAuth, you might read that and think; âSure, no problem. OAuth is easyâ or âuhhhh, whatâs an OAuth?â or somewhere in between. At any experience level, if you only want to make a single API request to display your Dribbble shots on your personal site, itâs a rigmarole.
When I first read about the new auth I thought, âwelp, Jribbble is dead. As is all client-side JS access to the Dribbble API.â I thought this beause in most cases, you shouldnât put OAuth-generated bearer access tokens in public code. Doing so makes them available for anyone to find and act on behalf of token owners in anyâmaliciousâway they want.
I asked the Dribbble folks about this and they let me know that in this case, putting access tokens in public code would be OK because theyâre read-only. A bad actor could still snag your access token and use it to abuse the API on your behalf. Iâd guess by making a ton of nonsense requests. The Dribbble API has rate-limiting in place to help mitigate this. At the worst, Dribbble could revoke your access token or disable your app. Youâd then need to go back though the process to get a new token and/or create a new app.
This change still didnât sit well with me though. What are less code-savvy folks supposed to do? The main users of Dribbble and Jribbble are Designers. Thereâs a good chance many of them donât have experience creating and deploying OAuth flows. Thatâs exactly who Iâve had conversations with over the years. And thatâs who has reached out to me since the API version two announcment. Folks that know enough code to copy, paste, and tweak a snippet of JS, but not enough code to build and deploy a custom OAuth setup.
With that, I set out to come up with a way to help them. Because thatâs what we do here.
Options
My first idea was to create a site where people would authenticate with a Dribbble application that I owned. Theyâd show up, tap âconnectâ, confirm with Dribbble, then get directed back to my site where theyâd get an access token.
That would get the job done, but felt wrong. Having people authenticate through my app would mean their access tokens would be in my hands. If I deleted the appâor my Dribbble accountâtheir tokens would stop working. If a bad actor generated a token then used it to abuse the API, my app could get suspended. Again, taking every userâs access token with it.
I needed a way for every Jribbble user to be able to generate access tokens using an application they owned.
Glitch
If you havenât used it yet, Glitch is an excellent tool for sharing and learning code. What sets Glitch apart from similar code-sharing tools is that it gives you the ability to create and share web servers.
Thatâs exactly what I needed for this OAuth problem. I could write the server necessary for the OAuth flow, then make it available for anyone toâin the parlance of Glitchââremixâ the code to get full access to it. And they can do so without even creating a Glitch account.
So thatâs what I did. jribbble.glitch.me is template that anyone can remix to get their own OAuth flow for generating bearer tokens to use with Dribbble API version two.
I wonât say that writing a server for this is âeasyâ because itâs only easy if you already know how to do it. But, I hope that this will help folks see that itâs attainable. To demonstrate, Iâm going to plop the entire server here. The code is under 100 lines. The rest is comments to help people understand whatâs happening at each step along the way:
const express = require("express");
const tiny = require("tiny-json-http");
const nunjucks = require("nunjucks");
const cookieParser = require("cookie-parser");
// We store the relavant Dribbble URLs here for convenience.
// authUrl is passed along to our template in the `/` handler below.
const authUrl = "https://dribbble.com/oauth/authorize";
const tokenUrl = "https://dribbble.com/oauth/token";
// The path of the Callback URL we set when creating our application on Dribbble.
// We reference this URL below in an app.get
const callbackUrl = "/oauth_callback";
// This isn't oauth or dribbble specific, but storing the value of the original
// Glitch project to check if we're seeing the original or a remix.
const ogGlitchUrl = "https://jribbble.glitch.me";
// These environment variables need to be set in the `.env` file. Look to your left đ
// client id is safe as public value, if those are seen, it's OK.
// client_secret is not safe for public. You need to keep that private at all times.
const client_id = process.env.DRIBBBLE_APP_ID;
const client_secret = process.env.DRIBBBLE_APP_SECRET;
// When we receive an access_token from the api.dribbble.com server, we'll store it
// in this variable.
// Q: Why do we use `let` here instead of `const`?
// A: We use `let` so we can reassign `access_token` to a new value. Here, we set
// an initial value of `null`. Below in the `/oauth_callback` handler we set
// it to a new value of the access_token from the server.
let access_token = null;
// Standard express setup code.
const app = express();
app.use([express.static("public"), cookieParser()]);
// Set up our template library.
// IâTylerâdidn't look too deep into this, this block of code came from the nunjucks
// docs and got me up and running, so good enough for me at this time.
nunjucks.configure(["views", "public"], {
autoescape: true,
express: app
});
// This is our homepage and the page that does most of the work.
app.get("/", (req, res) => {
const pageUrl = `https://${req.get("host")}`;
// Here, we'll try to set the access_token from a cookie.
// In the callback handler below, we set the access_token cookie on successful auth.
// This isn't something you need to do in your Jribbble uses.
access_token = req.cookies.access_token;
// We use render so we can pass along variables to our template.
// In index.html any time you see or {% %}, we're referencing
// a variable we set here.
res.render("index.html", {
authUrl,
accessToken: access_token,
clientId: client_id,
// Just in case we've hit an authentication error we'll use this to display a message in the template
error: req.query.error,
// We create new boolean value here so we don't send the actual secret to the template.
// Note: IâTylerâam not sure this is 100% necessary, but it felt best to be overly
// cautious when our app secret. You don't want anyone to have that.
hasClientSecret: client_secret.length,
pageUrl,
isRemix: pageUrl !== ogGlitchUrl,
callbackUrl
});
});
// This is where our Dribbble applications will come back to after a GET to authUrl
app.get(callbackUrl, async (req, res) => {
const data = {
code: req.query.code,
client_id,
client_secret
};
try {
// We required `tiny` above in tiny-json-http
// That's a small http library I preferred to use https://github.com/brianleroux/tiny-json-http
// It's not the only way to make requests, there are many different was to accomplish
// this http post request to Dribbble
// Note we are using async/await here. If you're unfamiliar, that's OK. The number one thing
// to know is `await` makes this code act like it's pausing here and waiting for the http
// request to complete before moving on to the following lines of code.
const { body } = await tiny.post({ url: tokenUrl, data });
// As mentioned above, here we're assigning access_token a new value that is your
// shiny oauth access token that gives you public read access to your Dribbble account
access_token = body.access_token;
// NOTE: Setting a cookie want be required in your uses of Jribbble, because you will
// include the access_token in your JavaScript.
res.cookie("access_token", access_token);
// We don't want to stay on the /oauth_callback page, so redirect back home.
res.redirect("/");
} catch (err) {
// If we hit an error we'll handle that here
console.log(err);
res.redirect("/?error=đĄ");
}
});
app.get("/logout", (req, res) => {
res.clearCookie("access_token");
res.redirect("/");
});
app.listen(process.env.PORT);
Again, you can remix this at jribbble.glitch.me to get full access to the code.
Another cool thing. This example is intended for the Dribbble API, but with a few minor changes, it will work for any standard OAuth flow.
If you use that and run into troubleâor think itâs greatâlet me know on Twitter.
The Jribbble Rewrite
The main part of this project was writing Jribbble 3.0. Jribbble 2.x has six public methods to access resources from Dribbble API version one. Many of those methods have subresources like comments, likes, et al. It also has a chainable, promise-based interface. All these things add a non-trivial amount of plumbing code to make them possible.
Dribbble API version two reduces functionality to getting the current userâs profile, shots, and projects. For Dribbble-approved applications you can get a userâs likes and a list of popular shots.
The reduced functionality meant I could cut most of Jribbbleâs code. The first
thing I did was audit the usage of jQuery. The only jQuery methods in use were; $.ajax
, $.Deferred
, and $.extend
.
Because of the limited usage, I decided Jribbble 3.0 would not use jQuery.
It also didnât feel necessary to use any type of transpiling process. So no Babel or
TypeScript or the like. I would only write good-ole browser JavaScript. Also, I wanted 3.0 to work in as many browsers as possible, so I only used old-timey
JS. No arrow functions, no let
or const
, etc. It wasnât that bad. This is a small library so
restraining myself to older JS wasnât a problem. And writing it directly instead of relying on Babel keeps the file size smaller.
I could have used newer features and still probably ended up with wide-enough support, but it was a fun challenge. And it reminded me of writing JS in years past.
This was enough of a rewrite that I opened a new, blank file and started writing instead of reusing 2.0 code.
3.0 Details
Like 2.0, this version also has six public methods;
setToken
, shots
, user
, projects
, likes
, and popular
. Each method is available on the window-scoped jribbble
object.
setToken
is the same as in 2.0. Itâs how users give Jribbble
their access tokens.
jribbble.setToken("12345");
For 3.0 I decided to also allow users to provide their token as an option when calling any of the other methods. For example:
jribbble.shots({token: "12345"}, callback);
That accomplishes the same thing as setToken
in a more concise way.
From here, Iâll detail three internal functions that handle the lionâs share of what Jribbble can do.
Function one: get
I took it back in time for this one. For day-to-day work I use fetch or a wrapper like Axios to make network requests. For Jribbble I didnât want to use a third-party library or polyfills for requests. So instead of fetch, I went back to XMLHttpRequest
.
All Jribbble requests have the same requirements so I was able to abstract the functionality to a common function. I use the internal get
function for all requests to the Dribbble API:
var get = function(path, callback) {
var url = "https://api.dribbble.com/v2/" + path;
var req = new XMLHttpRequest();
req.addEventListener("load", function() {
if (callback) {
if (typeof callback === "function") {
var ret = {};
if (this.status < 400) {
try {
ret = JSON.parse(this.responseText);
} catch (err) {
ret = {
error: "There was an error parsing the server response as JSON"
};
}
} else {
ret = {
error:
"There was an error making the request to api.dribble.com.",
status: this.status
};
}
callback(ret);
}
}
});
req.open("GET", url);
req.setRequestHeader("Authorization", "Bearer " + accessToken);
req.send();
};
Not bad. Itâs a standard XMLHttpRequest
GET request with an Authorization header. The bulk of the function is guard code to protect against type errors, JSON parsing problems, and network errors. It does what it can to fail with grace if there is a problem.
I use the Authorization header to send along the userâs Dribbble access token with every request.
The get
function will work back to IE7. I figured Itâd be safe to not include the old fork to check for ActiveXObject
. Itâs been years since Iâve typed that, itâs giving me a good chuckle to see it here.
Function two: createApiMethod
The user
, projects
, likes
, and popular
methods all do the same thing. They process N-number of arguments, make a request to the Dribbble API, and call a user-provided callback. The callback receives a single argument, the JSON response from the request.
Because theyâre all similar, I didnât want to have to repeat the same code when defining each method. Instead, I abstracted the functionality to createApiMethod
.
var createApiMethod = function(path) {
return function() {
var args = processArguments.apply(null, arguments);
get(path + args.query, args.callback);
};
};
The path
parameter is passed along to get
to build the URL to the Dribbble API.
I then define each public method as a member of the api
object. Each is a function with a unique path based on its needs.
var api = {
...
user: createApiMethod("user"),
projects: createApiMethod("user/projects"),
likes: createApiMethod("user/likes"),
popular: createApiMethod("popular_shots")
};
This doesnât provide any extra functionality. It only makes it so I donât
have to repeat as much code when defining methods. createApiMethod
returns a function. Letâs look at whatâs happening in the body of that function.
var args = processArguments.apply(null, arguments);
get(path + args.query, args.callback);
The first line is the heavy lifting. Itâs function number three that Iâll write about next. For now, itâs important to know that it returns an object with query
, callback
, and resourceId
keys.
Those values, plus the path
argument, let us build the arguments needed to make the request to Dribbble with get
.
Function three: processArguments
For me, this is the most interesting bit of code in Jribbble. Every public Jribbble method except setToken
uses processArguments
. Its purpose is to inspect all arguments passed to Jribbble methods.
To show its usefulness, consider the following usage examples:
jribbble.shots(
"456789",
{token: "12345"},
function(shotObject) { /* Work with JSON */ }
);
jribbble.shots(
{token: "12345", page: 3, per_page: 5},
function(shotsArray) { /* Work with JSON*/}
);
jribbble.projects(
function(projectsArray) { /* Work with JSON */ },
{token: "12345"}
);
jribbble.user(
function(userObject) { /* Work with JSON */ }
);
Notice in each Iâm providing a different number of arguments of different types. And in the case of projects
Iâm providing the arguments in a different order. This type of flexibility isnât possible with a typical method signature. One where I define each parameter when I create the method.
For example, imagine a method signature for jribbble.shots
like this:
var shots = function(shotId, options, callback) { /* Do the work */ };
That would work for the first usage example, but what about the second? What if I donât need a single shot and I donât need to provide an options
argument?
This is where processArguments
comes into play.
First, notice when I call the function in createApiMethod
I use apply
:
var args = processArguments.apply(null, arguments);
This took me a few minutes to get my head around. Itâs also hard to write about, but here goes. I need the function that createApiMethod
returns to take zero to
three arguments. The Jribbble user, provides those. Using apply
let me pass along the arguments
object received from calls to public API methods. Then, within
processArguments
I have access to that original arguments
object. Once I have it, I convert it to an array for manipulation:
var args = [].slice.call(arguments);
An important thing to note is processArguments
doesnât define parameters:
var processArguments = function() {...};
Again, thatâs not something I can define ahead of time. This whole dance lets the public methods be flexible in number and order of parameters. It lets them say âyeah, whatever you got, Iâll take it.â
I mentioned earlier, processArguments
returns an object with query
, callback
, and resourceId
keys. The work of the function focuses on creating that object.
While I canât define parameters, I do know a few things about potential arguments. I know any callback should be a function and any options should be an object. I also know resourceId
is the identifier for a Dribbble shot. A shot id can be a string or a number.
I use that knowledge to inspect each item in the args
array I created from the arguments
object. Depending on the type I assign the itemâs value to a local variable.
...
var resourceId = null;
var opts = {};
var callback = function() {};
...
for (var i = 0; i < args.length; i += 1) {
switch (typeof args[i]) {
case "string":
case "number":
resourceId = args[i];
break;
case "object":
opts = args[i];
break;
case "function":
callback = args[i];
}
}
That snippet sets two out of three variables this function needs to return. resourceId
and callback
are ready to to go. I only use resourceId
in jribbble.shots
. The Dribbble API has different paths if youâre requesting a single shot or a list of shots. I use the valueâor lack of a valueâof resourceId
to determine the path.
What about the third item, query
? Also what is this opts
object in favor of?
In earlier examples, I showed providing an object with token, page, and per_page keys to public methods. Token is the most important one. If a user doesnât provide an access token, they canât make requests. After the for loop I check for a token key on the opts
object:
if (opts.token) {
accessToken = opts.token;
}
I define accessToken
at the root level of the main Jribbble function. This is how I provide the flexibility of setting token with the setToken
method or via an options object.
At this point in processArguments
, if thereâs no value for accessToken
thereâs no reason to continue. I throw an error and let the user know they need to update their code.
if (!accessToken) {
throw new Error(
"jribbble needs a valid access token. You can either include this as an option: jribbble.shots({token: '1234'}) or with jribbble.setToken('1234')"
);
}
If the user provided page
or per_page
, those keys will also be on the opts
object. query
needs to be a string that I can append to the URL of any request. So, I need to create that string if necessary. I know that âpageâ and âper_pageâ are the only query parameters allowed because theyâre documented. I can use that knowledge to build the string based on the user-provided values.
var params = ["page", "per_page"]
.map(function(p) {
return opts[p] ? p + "=" + opts[p] : null;
})
.filter(function(i) {
return i;
})
.join("&");
For each item in the array check to see if that key exists in opts
. If it does, return a string beginning with the item plus an equals sign plus the value in opts
. If the key isnât in opts
return null
.
At this point, we could have an array that looks like:
["page=4", null]
Thatâs if the user set a page
, but not a per_page
value.
Next, I use filter
to remove any null
values. And finally join the items of the array with an ampersand between each. In the above example weâd end up with âpage=4â.
processArguments
is ready to return an object with the three keys needed. I also need to do one last thing for query
. If the user doesnât need a query, return an empty string. If they do, prefix the params
string with a â?â so itâs ready to append to a URL.
return {
resourceId: resourceId,
callback: callback,
query: params ? "?" + params : ""
};
And thatâs processArguments
. Syntax and even code-wise itâs not fancy, but it does a job and does it in what I think is a clear way.
One more iteration complete
This was another iteration on a little library I keep going. It has a small userbase, but I know it provides value for them when itâs needed. As the Dribbble API changes, Iâll keep Jribbble in sync as best I can.
Again, the OAuth flow project is at jribbble.glitch.me or just jribbble.com. The Jribbble source is available on GitHub. If you run into trouble, please open an issue or submit a pull request.
Thanks for reading