diff --git a/README.md b/README.md index bd6f7e5e4..7fb852553 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Read our [contributing guidelines](./CONTRIBUTING.md) if you wish to contribute. * [unlockAccount](#unlockaccountoptions) * [verifyRecoveryToken](#verifyrecoverytokenoptions) * [webfinger](#webfingeroptions) - * [fingerprint] (#fingerprintoptions) + * [fingerprint](#fingerprintoptions) * [tx.resume](#txresume) * [tx.exists](#txexists) * [transaction.status](#transactionstatus) @@ -132,6 +132,7 @@ The goal of an authentication flow is to [set an Okta session cookie on the user - `username` - User’s non-qualified short-name (e.g. dade.murphy) or unique fully-qualified login (e.g dade.murphy@example.com) - `password` - The password of the user + - `sendFingerprint` - Enabling this will send a `X-Device-Fingerprint` header. Defaults to `false` ```javascript authClient.signIn({ diff --git a/lib/clientBuilder.js b/lib/clientBuilder.js index 8333146cc..045597d96 100644 --- a/lib/clientBuilder.js +++ b/lib/clientBuilder.js @@ -159,7 +159,23 @@ proto.features.isTokenVerifySupported = function() { // { username, password, (relayState), (context) } proto.signIn = function (opts) { - return tx.postToTransaction(this, '/api/v1/authn', opts); + var sdk = this; + var body = opts || {}; + return new Q(!body.sendFingerprint ? {} : + sdk.fingerprint() + .then(function(fingerprint) { + return { + headers: { + 'X-Device-Fingerprint': fingerprint + } + }; + }) + ) + .then(function(options) { + body = util.clone(body); + delete body.sendFingerprint; + return tx.postToTransaction(sdk, '/api/v1/authn', body, options); + }); }; proto.signOut = function () { diff --git a/lib/tx.js b/lib/tx.js index 69c992ab7..aba77843c 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -48,8 +48,8 @@ function transactionExists(sdk) { return !!sdk.tx.exists._getCookie(config.STATE_TOKEN_COOKIE_NAME); } -function postToTransaction(sdk, url, options) { - return http.post(sdk, url, options) +function postToTransaction(sdk, url, body, options) { + return http.post(sdk, url, body, options) .then(function(res) { return new AuthTransaction(sdk, res); }); diff --git a/test/spec/fingerprint.js b/test/spec/fingerprint.js index 3743d9b66..4754dfe54 100644 --- a/test/spec/fingerprint.js +++ b/test/spec/fingerprint.js @@ -1,6 +1,7 @@ define(function(require) { var OktaAuth = require('OktaAuth'); var util = require('../util/util'); + var packageJson = require('../../package.json'); describe('fingerprint', function() { function setup(options) { @@ -59,17 +60,17 @@ define(function(require) { }); }); - var authClient = new OktaAuth({ + var authClient = options.authClient || new OktaAuth({ url: 'http://example.okta.com' }); if (typeof options.userAgent !== 'undefined') { util.mockUserAgent(authClient, options.userAgent); } - return authClient.fingerprint({ timeout: options.timeout }); + return authClient; } it('iframe is created with the right src and it is hidden', function (done) { - return setup() + return setup().fingerprint() .catch(function(err) { expect(err).toBeUndefined(); }) @@ -88,7 +89,7 @@ define(function(require) { }); it('allows non-Okta postMessages', function (done) { - return setup({ sendOtherMessage: true }) + return setup({ sendOtherMessage: true }).fingerprint() .catch(function(err) { expect(err).toBeUndefined(); }) @@ -101,7 +102,7 @@ define(function(require) { }); it('fails if the iframe sends invalid message content', function (done) { - return setup({ firstMessage: 'invalidMessageContent' }) + return setup({ firstMessage: 'invalidMessageContent' }).fingerprint() .then(function() { done.fail('Fingerprint promise should have been rejected'); }) @@ -112,7 +113,7 @@ define(function(require) { }); it('fails if user agent is not defined', function (done) { - return setup({ userAgent: '' }) + return setup({ userAgent: '' }).fingerprint() .then(function() { done.fail('Fingerprint promise should have been rejected'); }) @@ -125,7 +126,7 @@ define(function(require) { it('fails if it is called from a Windows phone', function (done) { return setup({ userAgent: 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0;)' - }) + }).fingerprint() .then(function() { done.fail('Fingerprint promise should have been rejected'); }) @@ -136,7 +137,7 @@ define(function(require) { }); it('fails after a timeout period', function (done) { - return setup({ timeout: 5 }) + return setup({ timeout: true }).fingerprint({ timeout: 5 }) .then(function() { done.fail('Fingerprint promise should have been rejected'); }) @@ -145,5 +146,51 @@ define(function(require) { done(); }); }); + + util.itMakesCorrectRequestResponse({ + title: 'attaches fingerprint to signIn requests if addFingerprint is true', + setup: { + uri: 'http://example.okta.com', + calls: [ + { + request: { + method: 'post', + uri: '/api/v1/authn', + data: { username: 'not', password: 'real' }, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Okta-User-Agent-Extended': 'okta-auth-js-' + packageJson.version, + 'X-Device-Fingerprint': 'ABCD' + } + }, + response: 'success' + } + ] + }, + execute: function (test) { + return setup({ authClient: test.oa }).signIn({ + username: 'not', + password: 'real', + sendFingerprint: true + }); + } + }); + + it('fails signIn request if fingerprinting fails', function(done) { + return setup({ firstMessage: 'invalidMessageContent' }) + .signIn({ + username: 'not', + password: 'real', + sendFingerprint: true + }) + .then(function() { + done.fail('signIn promise should have been rejected'); + }) + .catch(function(err) { + util.assertAuthSdkError(err, 'Unable to parse iframe response'); + done(); + }); + }); }); });