diff --git a/API.md b/API.md index 704ca4f4..b9c5611f 100755 --- a/API.md +++ b/API.md @@ -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. diff --git a/lib/index.js b/lib/index.js index 5acf0944..01abe8a0 100755 --- a/lib/index.js +++ b/lib/index.js @@ -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, diff --git a/lib/oauth.js b/lib/oauth.js index 2e457a32..4445bac0 100755 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -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 = { @@ -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) => { diff --git a/test/index.js b/test/index.js index 2d613b93..cd6d4b7e 100755 --- a/test/index.js +++ b/test/index.js @@ -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(); diff --git a/test/mock.js b/test/mock.js index 9755fbca..20962882 100755 --- a/test/mock.js +++ b/test/mock.js @@ -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); }