Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: domenic/restify-oauth2
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: oauth-io/restify-oauth2-oauthd
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Able to merge. These branches can be automatically merged.
  • 15 commits
  • 13 files changed
  • 4 contributors

Commits on Jul 26, 2013

  1. oauth.io modifs + renamed project according WTFPL

    Signed-off-by: Arnaud Richard <[email protected]>
    bumpmann committed Jul 26, 2013
    Copy the full SHA
    730d06b View commit details

Commits on Feb 4, 2014

  1. Copy the full SHA
    4896e73 View commit details

Commits on Apr 23, 2014

  1. Added cors middleware

    william26 committed Apr 23, 2014
    Copy the full SHA
    58a9a49 View commit details

Commits on Apr 24, 2014

  1. Copy the full SHA
    3498fbc View commit details

Commits on Nov 20, 2014

  1. Copy the full SHA
    db7648c View commit details
  2. Merge branch 'develop'

    william26 committed Nov 20, 2014
    Copy the full SHA
    13d18af View commit details
  3. Bumped 2.2.1

    william26 committed Nov 20, 2014
    Copy the full SHA
    198d495 View commit details
  4. Merge branch 'develop'

    william26 committed Nov 20, 2014
    Copy the full SHA
    5c5c3b2 View commit details

Commits on Oct 4, 2015

  1. Merge branch 'master' into develop

    Conflicts:
    	README.md
    	lib/cc/grantToken.js
    	lib/common/makeHandleAuthenticatedResource.js
    	lib/common/makeSetup.js
    	package.json
    bumpmann committed Oct 4, 2015
    Copy the full SHA
    1e3cff4 View commit details
  2. Copy the full SHA
    6ebb8f9 View commit details

Commits on Nov 17, 2016

  1. Copy the full SHA
    b291c75 View commit details
  2. Merge branch 'master' into develop

    # Conflicts:
    #	package.json
    bumpmann committed Nov 17, 2016
    Copy the full SHA
    905b5a7 View commit details
  3. Copy the full SHA
    2cea6b0 View commit details
  4. Copy the full SHA
    08f2faa View commit details
  5. travis: stable -> develop

    bumpmann committed Nov 17, 2016
    Copy the full SHA
    224c46b View commit details
7 changes: 6 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
language: node_js
node_js:
- "0.10"
- "7.0"

branches:
only:
- master
- develop
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
# OAuth 2 Endpoints for Restify
[![Build Status](https://travis-ci.org/oauth-io/restify-oauth2-oauthd.svg?branch=develop)](https://travis-ci.org/oauth-io/restify-oauth2-oauthd)

# OAuth 2 Endpoints for Restify + OAuth.io modifications

This package provides a *very simple* OAuth 2.0 endpoint for the [Restify][] framework. In particular, it implements
the [Client Credentials][cc] and [Resource Owner Password Credentials][ropc] flows only.

## OAuth.io notice

This package is a modification of restify-oauth2 to be included in the OAuth Daemon (oauthd) project.
The main modification is to reply a 403 instead of 401/WWW-Authenticate to avoid basic auth popup in browsers in some cases. We advise to use the original restify-oauth2 if you want to use it in your projects, since the description below may be inexact.

----------------------------------

## What You Get

If you provide Restify–OAuth2 with the appropriate hooks, it will:
If you provide Restify–OAuth2–oauthd with the appropriate hooks, it will:

* Set up a [token endpoint][], which returns [access token responses][token-endpoint-success] or
[correctly-formatted error responses][token-endpoint-error].
@@ -19,19 +28,19 @@ If you provide Restify–OAuth2 with the appropriate hooks, it will:
* If no access token is sent, it ensures that `req.username` is set to `null`; furthermore, none of your hooks are
called, so you can be sure that no properties that they set are present.
* You can then check for these conditions whenever there is a resource you want to protect.
* If the user tries to access a protected resource, you can use Restify–OAuth2's `res.sendUnauthenticated()` to send
* If the user tries to access a protected resource, you can use Restify–OAuth2–oauthd's `res.sendUnauthenticated()` to send
appropriate 401 errors with helpful `WWW-Authenticate` and `Link` headers, or its `res.sendUnauthorized()` to send
appropriate 403 errors with similar headers.

## Use and Configuration

To use Restify–OAuth2, you'll need to pass it your server plus some options, including the hooks discussed below.
Restify–OAuth2 also depends on the built-in `authorizationParser` and `bodyParser` plugins, the latter with `mapParams`
To use Restify–OAuth2–oauthd, you'll need to pass it your server plus some options, including the hooks discussed below.
Restify–OAuth2–oauthd also depends on the built-in `authorizationParser` and `bodyParser` plugins, the latter with `mapParams`
set to `false`. So in short, it looks like this:

```js
var restify = require("restify");
var restifyOAuth2 = require("restify-oauth2");
var restifyOAuth2 = require("restify-oauth2-oauthd");

var server = restify.createServer({ name: "My cool server", version: "1.0.0" });
server.use(restify.authorizationParser());
@@ -42,12 +51,12 @@ restifyOAuth2.cc(server, options);
restifyOAuth2.ropc(server, options);
```

Unfortunately, Restify–OAuth2 can't be a simple Restify plugin. It needs to install a route for the token
Unfortunately, Restify–OAuth2–oauthd can't be a simple Restify plugin. It needs to install a route for the token
endpoint, whereas plugins simply run on every request and don't modify the server's routing table.

## Options

The options you pass to Restify–OAuth2 depend heavily on which of the two flows you are choosing. There are some
The options you pass to Restify–OAuth2–oauthd depend heavily on which of the two flows you are choosing. There are some
options common to both flows, but the `options.hooks` hash will vary depending on the flow. Once you provide the
appropriate hooks, you get an OAuth 2 implementation for free.

@@ -58,7 +67,7 @@ and if those values authenticate, you grant them an access token they can use fo
this over simply requiring basic access authentication headers on every request is that now you can set those tokens to
expire, or revoke them if they fall in to the wrong hands.

To install Restify–OAuth2's client credentials flow into your infrastructure, you will need to provide it with the
To install Restify–OAuth2–oauthd's client credentials flow into your infrastructure, you will need to provide it with the
following hooks in the `options.hooks` hash. You can see some [example CC hooks][] in the demo application.

#### `grantClientToken({ clientId, clientSecret }, req, cb)`
@@ -83,7 +92,7 @@ credentials to the server directly. For example, it obviates the need for the cl
allows expiration and revocation of tokens. However, it does imply that you trust your API clients, since they will
have at least one-time access to the user's credentials.

To install Restify–OAuth2's resource owner password credentials flow into your infrastructure, you will need to
To install Restify–OAuth2–oauthd's resource owner password credentials flow into your infrastructure, you will need to
provide it with the following hooks in the `options.hooks` hash. You can see some [example ROPC hooks][] in the demo
application.

@@ -154,16 +163,16 @@ The initial resource, at which people enter the server.
* If a valid token is supplied in the `Authorization` header, `req.username` is truthy, and the app responds with
links to `/public` and `/secret`.
* If no token is supplied, the app responds with links to `/token` and `/public`.
* If an invalid token is supplied, Restify–OAuth2 intercepts the request before it gets to the application, and sends
* If an invalid token is supplied, Restify–OAuth2–oauthd intercepts the request before it gets to the application, and sends
an appropriate 400 or 401 error.

## /token

The token endpoint, managed entirely by Restify–OAuth2. It generates tokens for a given client ID/client
The token endpoint, managed entirely by Restify–OAuth2–oauthd. It generates tokens for a given client ID/client
secret/username/password combination.

The client validation and token-generation logic is provided by the application, but none of the ceremony necessary for
OAuth 2 conformance, error handling, etc. is present in the application code: Restify–OAuth2 takes care of all of that.
OAuth 2 conformance, error handling, etc. is present in the application code: Restify–OAuth2–oauthd takes care of all of that.

## /public

@@ -172,7 +181,7 @@ A public resource anyone can access.
* If a valid token is supplied in the Authorization header, `req.username` contains the username, and the app uses
that to send a personalized response.
* If no token is supplied, `req.username` is `null`. The app still sends a response, just without personalizing.
* If an invalid token is supplied, Restify–OAuth2 intercepts the request before it gets to the application, and sends
* If an invalid token is supplied, Restify–OAuth2–oauthd intercepts the request before it gets to the application, and sends
an appropriate 400 or 401 error.

## /secret
@@ -183,7 +192,7 @@ A secret resource that only authenticated users can access.
data.
* If no token is supplied, `req.username` is `null`, so the application uses `res.sendUnauthenticated()` to send a nice
401 error with `WWW-Authenticate` and `Link` headers.
* If an invalid token is supplied, Restify–OAuth2 intercepts the request before it gets to the application, and sends
* If an invalid token is supplied, Restify–OAuth2–oauthd intercepts the request before it gets to the application, and sends
an appropriate 400 or 401 error.

[Restify]: http://mcavage.github.com/node-restify/
2 changes: 1 addition & 1 deletion examples/cc-with-scopes/server.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ var hooks = require("./hooks");

var server = restify.createServer({
name: "Example Restify-OAuth2 Client Credentials Server",
version: require("../../package.json").version,
version: require("../../package.json").version.replace(/-?.*$/, ""),
formatters: {
"application/hal+json": function (req, res, body, cb) {
return res.formatters["application/json"](req, res, body, cb);
2 changes: 1 addition & 1 deletion examples/cc/server.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ var hooks = require("./hooks");

var server = restify.createServer({
name: "Example Restify-OAuth2 Client Credentials Server",
version: require("../../package.json").version,
version: require("../../package.json").version.replace(/-?.*$/, ""),
formatters: {
"application/hal+json": function (req, res, body, cb) {
return res.formatters["application/json"](req, res, body, cb);
4 changes: 2 additions & 2 deletions examples/ropc/server.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ var hooks = require("./hooks");

var server = restify.createServer({
name: "Example Restify-OAuth2 Resource Owner Password Credentials Server",
version: require("../../package.json").version,
version: require("../../package.json").version.replace(/-?.*$/, ""),
formatters: {
"application/hal+json": function (req, res, body, cb) {
return res.formatters["application/json"](req, res, body, cb);
@@ -83,4 +83,4 @@ server.post('/close', function(req, res){
server.close();
});

server.listen(8080);
server.listen(8080)
4 changes: 2 additions & 2 deletions lib/cc/grantToken.js
Original file line number Diff line number Diff line change
@@ -19,8 +19,8 @@ module.exports = function grantToken(req, res, next, options) {
}

if (!token) {
res.header("WWW-Authenticate", "Basic realm=\"Client ID and secret did not authenticate.\"");
return next(makeOAuthError("Unauthorized", "invalid_client", "Client ID and secret did not authenticate."));
// res.header("WWW-Authenticate", "Basic realm=\"Client ID and secret did not authenticate.\"");
return next(makeOAuthError("Forbidden", "invalid_client", "Client ID and secret did not authenticate."));
}

var allCredentials = { clientId: clientId, clientSecret: clientSecret, token: token };
1 change: 0 additions & 1 deletion lib/common/makeHandleAuthenticatedResource.js
Original file line number Diff line number Diff line change
@@ -22,7 +22,6 @@ module.exports = function makeHandleAuthenticatedResource(errorSenders) {
if (error) {
return errorSenders.sendWithHeaders(res, next, options, error);
}

if (!authenticated) {
return errorSenders.tokenInvalid(res, next, options);
}
15 changes: 13 additions & 2 deletions lib/common/makeSetup.js
Original file line number Diff line number Diff line change
@@ -2,6 +2,13 @@

var _ = require("underscore");

var cors_middleware = function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
next();
};

module.exports = function makeSetup(grantTypes, requiredHooks, grantToken) {
var errorSenders = require("./makeErrorSenders")(grantTypes);
var handleAuthenticatedResource = require("./makeHandleAuthenticatedResource")(errorSenders);
@@ -36,7 +43,11 @@ module.exports = function makeSetup(grantTypes, requiredHooks, grantToken) {
options.tokenExpirationTime = undefined;
}

server.post(options.tokenEndpoint, function (req, res, next) {
server.opts(options.tokenEndpoint, cors_middleware, function (req, res, next) {
res.send(200);
});

server.post(options.tokenEndpoint, cors_middleware, function (req, res, next) {
grantToken(req, res, next, options);
});

@@ -52,7 +63,7 @@ module.exports = function makeSetup(grantTypes, requiredHooks, grantToken) {
if (req.method === "POST" && req.path() === options.tokenEndpoint) {
// This is handled by the route installed above, so do nothing.
next();
} else if (req.authorization.scheme) {
} else if (req.authorization && req.authorization.scheme) {
handleAuthenticatedResource(req, res, next, options);
} else {
// Otherwise Restify will set it by default, which gives false positives for application code.
23 changes: 15 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
{
"name": "restify-oauth2",
"description": "A simple OAuth 2 endpoint for Restify",
"name": "restify-oauth2-oauthd",
"description": "A simple OAuth 2 endpoint for Restify + OAuth.io modifications",
"keywords": [
"restify",
"oauth",
"oauthd",
"oauth.io",
"oauth2",
"rest",
"authorization",
"authentication",
"api"
],
"version": "4.0.4",
"version": "4.0.4-oauthd",
"author": "Domenic Denicola <domenic@domenicdenicola.com> (http://domenic.me/)",
"license": "WTFPL",
"contributors": [
"OAuth.io Team <team@oauth.io> (https://oauth.io/)"
],
"repository": {
"type": "git",
"url": "git://github.com/domenic/restify-oauth2.git"
"url": "git@github.com:oauth-io/restify-oauth2-oauthd"
},
"bugs": "http://github.com/domenic/restify-oauth2/issues",
"bugs": {
"url": "http://github.com/oauth-io/restify-oauth2-oauthd/issues",
"email": "team+bugs@oauth.io"
},
"license": "WTFPL",
"main": "lib/index.js",
"scripts": {
"test": "npm run test-ropc-unit && npm run test-cc-unit && npm run test-ropc-integration && npm run test-cc-integration && npm run test-cc-with-scopes-integration",
@@ -40,7 +47,7 @@
"coffee-script": "^1.9.3",
"jshint": "^2.8.0",
"mocha": "^2.2.5",
"restify": "^4.0",
"restify": "^4.3.0",
"sinon": "^1.16.1",
"sinon-chai": "^2.8.0",
"vows": "^0.8.1"
2 changes: 1 addition & 1 deletion test/cc-integration.coffee
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ suite
.setHeader("Authorization", "Basic MTIzOjQ1Ng==")
.setHeader("Content-Type", "application/json")
.post({ grant_type: "client_credentials" })
.expect(401)
.expect(403)
.expect("should respond with error: invalid_client", (err, res, body) ->
JSON.parse(body).should.have.property("error", "invalid_client")
)
11 changes: 6 additions & 5 deletions test/cc-unit.coffee
Original file line number Diff line number Diff line change
@@ -63,7 +63,8 @@ beforeEach ->
@pluginNext = sinon.spy()

@server =
post: sinon.spy((path, handler) => @postToTokenEndpoint = => handler(@req, @res, @tokenNext))
post: sinon.spy((path, middleware, handler) => @postToTokenEndpoint = => middleware(@req, @res, => handler(@req, @res, @tokenNext)))
opts: sinon.spy((path, middleware, handler) => @optsToTokenEndpoint = => middleware(@req, @res, => handler(@req, @res, @tokenNext)))
use: (plugin) => plugin(@req, @res, @pluginNext)

@authenticateToken = sinon.stub()
@@ -232,19 +233,19 @@ describe "Client Credentials flow", ->
describe "when `grantClientToken` calls back with `false`", ->
beforeEach -> @grantClientToken.yields(null, false)

it "should send a 401 response with error_type=invalid_client", ->
it "should send a 403 response with error_type=invalid_client", ->
@doIt()

@res.should.be.an.oauthError("Unauthorized", "invalid_client",
@res.should.be.an.oauthError("Forbidden", "invalid_client",
"Client ID and secret did not authenticate.")

describe "when `grantClientToken` calls back with `null`", ->
beforeEach -> @grantClientToken.yields(null, null)

it "should send a 401 response with error_type=invalid_client", ->
it "should send a 403 response with error_type=invalid_client", ->
@doIt()

@res.should.be.an.oauthError("Unauthorized", "invalid_client",
@res.should.be.an.oauthError("Forbidden", "invalid_client",
"Client ID and secret did not authenticate.")

describe "when `grantClientToken` calls back with an error", ->
2 changes: 1 addition & 1 deletion test/cc-with-scopes-integration.coffee
Original file line number Diff line number Diff line change
@@ -83,7 +83,7 @@ suite
.setHeader("Authorization", "Basic MTIzOjQ1Ng==")
.setHeader("Content-Type", "application/json")
.post({ grant_type: "client_credentials" })
.expect(401)
.expect(403)
.expect("should respond with error: invalid_client", (err, res, body) ->
JSON.parse(body).should.have.property("error", "invalid_client")
)
5 changes: 4 additions & 1 deletion test/ropc-unit.coffee
Original file line number Diff line number Diff line change
@@ -63,7 +63,8 @@ beforeEach ->
@pluginNext = sinon.spy()

@server =
post: sinon.spy((path, handler) => @postToTokenEndpoint = => handler(@req, @res, @tokenNext))
post: sinon.spy((path, middleware, handler) => @postToTokenEndpoint = => middleware(@req, @res, => handler(@req, @res, @tokenNext)))
opts: sinon.spy((path, middleware, handler) => @optsToTokenEndpoint = => middleware(@req, @res, => handler(@req, @res, @tokenNext)))
use: (plugin) => plugin(@req, @res, @pluginNext)

@authenticateToken = sinon.stub()
@@ -113,6 +114,7 @@ describe "Resource Owner Password Credentials flow", ->
baseDoIt = @doItBase
@doIt = =>
baseDoIt()
@optsToTokenEndpoint()
@postToTokenEndpoint()

describe "with a body", ->
@@ -164,6 +166,7 @@ describe "Resource Owner Password Credentials flow", ->
baseDoIt = @doItWithScopes
@doIt = =>
baseDoIt()
@optsToTokenEndpoint()
@postToTokenEndpoint()
@requestedScopes = ["one", "two"]
@req.body.scope = @requestedScopes.join(" ")