Skip to content
This repository has been archived by the owner on Apr 16, 2019. It is now read-only.

Commit

Permalink
Client connection manager. Closes #40. Closes #41
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Jan 19, 2016
1 parent 3f2a698 commit 5797814
Show file tree
Hide file tree
Showing 9 changed files with 652 additions and 11 deletions.
60 changes: 59 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ Oz is a web authorization protocol based on industry best practices. Oz combines
secure solution for granting and authenticating third-party access to an API on behalf of a user or
an application.

Protocol version: **1.0.0**
Protocol version: **3.0.0** (Same as v1.0.0 but moved the expired ticket indicator from a header
attribute to the error payload).

[![Build Status](https://secure.travis-ci.org/hueniverse/oz.png)](http://travis-ci.org/hueniverse/oz)

Expand All @@ -25,6 +26,10 @@ Protocol version: **1.0.0**
- [`ticket` response](#ticket-response)
- [`Oz.client`](#ozclient)
- [`Oz.client.header(uri, method, ticket, [options])`](#ozclientheaderuri-method-ticket-options)
- [`new Oz.client.Connection(options)`](#new-ozclientconnectionoptions)
- [`connection.request(path, ticket, options, callback)`](#connectionrequestpath-ticket-options-callback)
- [`connection.app(path, options, callback)`](#connectionapppath-options-callback)
- [`connection.reissue(ticket, callback)`](#connectionreissueticket-callback)
- [`Oz.endpoints`](#ozendpoints)
- [Endpoints options](#endpoints-options)
- [`encryptionPassword`](#encryptionpassword)
Expand Down Expand Up @@ -229,6 +234,59 @@ authenticated Oz requests where:
- `ticket` - the authorization [ticket](#ticket-response).
- `options` - additional Hawk `Hawk.client.header()` options.

#### `new Oz.client.Connection(options)`

Creates an **oz** client connection manager for easier access to protected resources. The client
manages the ticket lifecycle and will automatically refresh the ticken when expired. Accepts the
following options:
- `endpoints` - an object containing the server protocol endpoints:
`app` - the application credentials endpoint path. Defaults to `'/oz/app'`.
`reissue` - the ticket reissue endpoint path. Defaults to `'/oz/reissue'`.
- `uri` - required, the server full root uri without path (e.g. 'https://example.com').
- `credentials` - required, the application **hawk** credentials.

##### `connection.request(path, ticket, options, callback)`

Requests a protected resource where:
- `path` - the resource path (e.g. '/resource').
- `ticket` - the application or user ticket. If the ticket is expired, it will automatically
attempt to refresh it.
- `options` - optional configuration object where:
- `method` - the HTTP method (e.g. 'GET'). Defaults to `'GET'`.
- `payload` - the request payload object or string. Defaults to no payload.
- `callback` - the callback method using the signature `function(err, result, code, ticket)` where:
- `err` - an error condition.
- `result` - the requested resource (parsed to object if JSON).
- `code` - the HTTP response code.
- `ticket` - the ticket used to make the request (may be different from the ticket provided
when the ticket was expired and refreshed).

##### `connection.app(path, options, callback)`

Requests a protected resource using a shared application ticket where:
- `path` - the resource path (e.g. '/resource').
- `options` - optional configuration object where:
- `method` - the HTTP method (e.g. 'GET'). Defaults to `'GET'`.
- `payload` - the request payload object or string. Defaults to no payload.
- `callback` - the callback method using the signature `function(err, result, code, ticket)` where:
- `err` - an error condition.
- `result` - the requested resource (parsed to object if JSON).
- `code` - the HTTP response code.
- `ticket` - the ticket used to make the request (may be different from the ticket provided
when the ticket was expired and refreshed).

Once an application ticket is obtained internally using the provided **hawk** credentials in the
constructor, it will be reused by called to `connection.app()`. If it expires, it will
automatically refresh and stored for future usage.

##### `connection.reissue(ticket, callback)`

Reissues (refresh) a ticket where:
- `ticket` - the ticket being reissued.
- `callback` - the callback method using the signature `function(err, reissued)` where:
- `err` - an error condition.
- `reissued` - the reissued ticket.

### `Oz.endpoints`

The endpoint methods provide a complete HTTP request handler implementation which is designed to
Expand Down
157 changes: 156 additions & 1 deletion lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@

// Load modules

const Boom = require('boom');
const Hawk = require('hawk');
const Hoek = require('hoek');
const Wreck = require('wreck');


// Declare internals

const internals = {};
const internals = {
defaults: {
endpoints: {
app: '/oz/app',
reissue: '/oz/reissue'
}
}
};


// Generate header
Expand All @@ -23,3 +32,149 @@ exports.header = function (uri, method, ticket, options) {
return Hawk.client.header(uri, method, settings);
};


exports.Connection = internals.Connection = function (options) {

this.settings = Hoek.applyToDefaults(internals.defaults, options);
this._appTicket = null;
};


internals.Connection.prototype.request = function (path, ticket, options, callback) {

const method = options.method || 'GET';
this._request(method, path, options.payload, ticket, (err, result, code) => {

if (err) {
return callback(err);
}

if (code !== 401 ||
!result ||
!result.expired) {

return callback(null, result, code, ticket);
}

// Try to reissue ticket

this.reissue(ticket, (refreshError, reissued) => {

if (refreshError) {
return callback(err); // Pass original request error
}

// Try resource again and pass back the ticket reissued (when not app)

this._request(method, path, options.payload, reissued, (err, result2, code2) => {

return callback(err, result2, code2, reissued);
});
});
});
};


internals.Connection.prototype.app = function (path, options, callback) {

const finalize = (err, result, code, ticket) => {

if (err) {
return callback(err);
}

this._appTicket = ticket; // In case ticket was refreshed
return callback(null, result, code, ticket);
};

if (this._appTicket) {
return this.request(path, this._appTicket, options, finalize);
}

this._requestAppTicket((err) => {

if (err) {
return finalize(err);
}

return this.request(path, this._appTicket, options, finalize);
});
};


internals.Connection.prototype.reissue = function (ticket, callback) {

this._request('POST', this.settings.endpoints.reissue, null, ticket, (err, result, code) => {

if (err) {
return callback(err);
}

if (code !== 200) {
return callback(Boom.internal(result.message));
}

return callback(null, result);
});
};


internals.Connection.prototype._request = function (method, path, payload, ticket, callback) {

const body = (payload !== null ? JSON.stringify(payload) : null);
const uri = this.settings.uri + path;
const headers = {};

if (typeof payload === 'object') {
headers['content-type'] = 'application/json';
}

const header = exports.header(uri, method, ticket);
headers.Authorization = header.field;

Wreck.request(method, uri, { headers: headers, payload: body }, (err, response) => {

if (err) {
return callback(err);
}

Wreck.read(response, { json: true }, (err, result) => {

if (err) {
return callback(err);
}

Hawk.client.authenticate(response, ticket, header.artifacts, {}, (err, attributes) => {

return callback(err, result, response.statusCode);
});
});
});
};


internals.Connection.prototype._requestAppTicket = function (callback) {

const uri = this.settings.uri + this.settings.endpoints.app;
const header = exports.header(uri, 'POST', this.settings.credentials);
Wreck.request('POST', uri, { headers: { Authorization: header.field } }, (err, response) => {

if (err) {
return callback(err);
}

Wreck.read(response, { json: true }, (err, result) => {

if (err) {
return callback(err);
}

if (response.statusCode !== 200) {
return callback(Boom.internal('Client registration failed with unexpected response', { code: response.statusCode, payload: result }));
}

this._appTicket = result;
return callback();
});
});
};
1 change: 1 addition & 0 deletions lib/endpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ internals.schema.reissue = Joi.object({
scope: Joi.array().items(Joi.string())
});


exports.reissue = function (req, payload, options, callback) {

payload = payload || {};
Expand Down
4 changes: 3 additions & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ exports._authenticate = function (req, encryptionPassword, checkExpiration, opti
if (checkExpiration &&
ticket.exp <= Hawk.utils.now()) {

return credsCallback(Hawk.utils.unauthorized('Expired ticket', { reason: 'expired' }));
const error = Hawk.utils.unauthorized('Expired ticket');
error.output.payload.expired = true;
return credsCallback(error);
}

return credsCallback(null, ticket);
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "oz",
"description": "Web Authorization Protocol",
"version": "2.0.0",
"version": "3.0.0",
"repository": "git://github.com/hueniverse/oz",
"main": "lib/index.js",
"keywords": [
Expand All @@ -14,16 +14,17 @@
"node": ">=4.x.x"
},
"dependencies": {
"joi": "7.x.x",
"hoek": "3.x.x",
"boom": "3.x.x",
"iron": "3.x.x",
"cryptiles": "3.x.x",
"hawk": "4.x.x"
"hawk": "^4.1.x",
"hoek": "3.x.x",
"iron": "3.x.x",
"joi": "7.x.x",
"wreck": "7.x.x"
},
"devDependencies": {
"code": "2.x.x",
"lab": "7.x.x"
"lab": "8.x.x"
},
"scripts": {
"test": "lab -a code -t 100 -L",
Expand Down
Loading

0 comments on commit 5797814

Please sign in to comment.