Skip to content
This repository has been archived by the owner on Jan 31, 2020. It is now read-only.

Commit

Permalink
Added custom OAuth2 client authentication (hapijs#286)
Browse files Browse the repository at this point in the history
* Added custom OAuth2 client authentication

* Fixed example to define client cert to use

* clientSecret is required

* Secret as object not allowed with OAuth v1

* useParamsAuth header code split for better readability

* Revert code split on several lines to avoid git merge conflict

* Removed local test added by mistake

* Removed traling space
  • Loading branch information
cvillerm authored and ldesplat committed Nov 19, 2017
1 parent 03dd779 commit c8781d9
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 3 deletions.
5 changes: 4 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,10 @@ The `server.auth.strategy()` method requires the following strategy options:
- `password` - the cookie encryption password. Used to encrypt the temporary state cookie used by the module in between the authorization
protocol steps.
- `clientId` - the OAuth client identifier (consumer key).
- `clientSecret` - the OAuth client secret (consumer secret).
- `clientSecret` - the OAuth client secret (consumer secret). This is usually a client password formatted as a *string*,
but to allow [OAuth2 custom client authentication](https://tools.ietf.org/html/rfc6749#section-2.3) (e.g. client certificate-based authentication),
this option can be passed as an *object*. This object will be merged with the Wreck request object used to call the token endpoint.
Such an object can contain custom HTTP headers or TLS options (e.g. `{ agent: new Https.Agent({ cert: myClientCert, key: myClientKey}) }`)
- `forceHttps` - A boolean indicating whether or not you want the redirect_uri to be forced to https. Useful if your hapi application runs as http, but is accessed through https.
- `location` - Set the base redirect_uri manually if it cannot be inferred properly from server settings. Useful to override port, protocol, and host if proxied or forwarded. It may be passed either as a string (in which case request.path is appended for you), or a function which takes the client's `request` and returns a non-empty string, which is used as provided. In both cases, an empty string will result in default processing just as if the `location` option had not been specified.

Expand Down
6 changes: 5 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ internals.schema = Joi.object({
}).required(),
password: Joi.string().required(),
clientId: Joi.string().required(),
clientSecret: Joi.string().required().allow(''),
clientSecret: Joi.alternatives().when('protocol', {
is: 'oauth',
then: Joi.string().required().allow(''),
otherwise: Joi.alternatives().try(Joi.string().allow(''), Joi.object())
}).required(),
cookie: Joi.string(),
isSecure: internals.flexBoolean,
isHttpOnly: internals.flexBoolean,
Expand Down
8 changes: 7 additions & 1 deletion lib/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ exports.v2 = function (settings) {

if (settings.provider.useParamsAuth) {
query.client_id = settings.clientId;
query.client_secret = settings.clientSecret;
if (typeof settings.clientSecret === 'string') {
query.client_secret = settings.clientSecret;
}
}

const requestOptions = {
Expand All @@ -237,6 +239,10 @@ exports.v2 = function (settings) {
Hoek.merge(requestOptions.headers, settings.provider.headers);
}

if (typeof settings.clientSecret === 'object') {
Hoek.merge(requestOptions, settings.clientSecret);
}

// Obtain token

Wreck.post(settings.provider.token, requestOptions, (err, tokenRes, payload) => {
Expand Down
104 changes: 104 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,110 @@ describe('Bell', () => {
});
});

it('authenticates an endpoint via oauth2 with custom client secret options', (done) => {

const mock = new Mock.V2(false);
mock.start((provider) => {

const server = new Hapi.Server();
server.connection({ host: 'localhost', port: 80 });
server.register(Bell, (err) => {

expect(err).to.not.exist();

server.auth.strategy('custom', 'bell', {
password: 'cookie_encryption_password_secure',
isSecure: false,
clientId: 'customSecret',
clientSecret: { headers: { mycustomtoken: 'mycustomtoken' } },
provider
});

server.route({
method: '*',
path: '/login',
config: {
auth: 'custom',
handler: function (request, reply) {

reply(request.auth.credentials);
}
}
});

server.inject('/login', (res) => {

const cookie = res.headers['set-cookie'][0].split(';')[0] + ';';
expect(res.headers.location).to.contain(mock.uri + '/auth?client_id=customSecret&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A80%2Flogin&state=');

mock.server.inject(res.headers.location, (mockRes) => {

expect(mockRes.headers.location).to.contain('http://localhost:80/login?code=1&state=');

server.inject({ url: mockRes.headers.location, headers: { cookie } }, (response) => {

expect(response.result.provider).to.equal('custom');
expect(response.result.token).to.equal('mycustomtoken');
mock.stop(done);
});
});
});
});
});
});

it('authenticates an endpoint via oauth2 with custom client secret options and params auth', (done) => {

const mock = new Mock.V2(true); // will set useParamsAuth = true
mock.start((provider) => {

const server = new Hapi.Server();
server.connection({ host: 'localhost', port: 80 });
server.register(Bell, (err) => {

expect(err).to.not.exist();

server.auth.strategy('custom', 'bell', {
password: 'cookie_encryption_password_secure',
isSecure: false,
clientId: 'customSecret',
clientSecret: { headers: { mycustomtoken: 'mycustomtoken' } },
provider
});

server.route({
method: '*',
path: '/login',
config: {
auth: 'custom',
handler: function (request, reply) {

reply(request.auth.credentials);
}
}
});

server.inject('/login', (res) => {

const cookie = res.headers['set-cookie'][0].split(';')[0] + ';';
expect(res.headers.location).to.contain(mock.uri + '/auth?client_id=customSecret&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A80%2Flogin&state=');

mock.server.inject(res.headers.location, (mockRes) => {

expect(mockRes.headers.location).to.contain('http://localhost:80/login?code=1&state=');

server.inject({ url: mockRes.headers.location, headers: { cookie } }, (response) => {

expect(response.result.provider).to.equal('custom');
expect(response.result.token).to.equal('mycustomtoken');
mock.stop(done);
});
});
});
});
});
});

it('overrides cookie name', (done) => {

const mock = new Mock.V1();
Expand Down
4 changes: 4 additions & 0 deletions test/mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ exports.V2 = internals.V2 = function (options) {
payload.email = 'steve@example.com';
}

if (code.client_id === 'customSecret') {
payload.access_token = request.headers.mycustomtoken;
}

if (code.client_id === internals.CLIENT_ID_TESTER) {
expect(internals.CLIENT_SECRET_TESTER).to.equal(request.payload.client_secret);
}
Expand Down

0 comments on commit c8781d9

Please sign in to comment.