Skip to content

Commit

Permalink
🌱 Add fingerprinting support
Browse files Browse the repository at this point in the history
  • Loading branch information
lboyette-okta committed Jan 10, 2018
1 parent 92ff850 commit 7aed53d
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
dist
target
node_modules/*
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Read our [contributing guidelines](./CONTRIBUTING.md) if you wish to contribute.
* [unlockAccount](#unlockaccountoptions)
* [verifyRecoveryToken](#verifyrecoverytokenoptions)
* [webfinger](#webfingeroptions)
* [fingerprint] (#fingerprintoptions)
* [tx.resume](#txresume)
* [tx.exists](#txexists)
* [transaction.status](#transactionstatus)
Expand Down Expand Up @@ -267,6 +268,22 @@ authClient.webfinger({
});
```

## [fingerprint(options)]

Creates a browser fingerprint.

- `timeout` - Time in ms until the operation times out. Defaults to 15000.

```javascript
authClient.fingerprint()
.then(function(fingerprint) {
// Do something with the fingerprint
})
.fail(function(err) {
console.log(err);
})
```

## tx.resume()

Resumes an in-progress **transaction**. This is useful if a user navigates away from the login page before authentication is complete.
Expand Down
68 changes: 68 additions & 0 deletions lib/clientBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
*
* See the License for the specific language governing permissions and limitations under the License.
*/
/* eslint-disable complexity */
/* eslint-disable max-statements */

require('./vendor/polyfills');

var Q = require('q');
var oauthUtil = require('./oauthUtil');
var util = require('./util');
var tx = require('./tx');
var session = require('./session');
Expand Down Expand Up @@ -124,6 +128,16 @@ function OktaAuthBuilder(args) {
return window.document;
};

sdk.fingerprint._getUserAgent = function() {
return navigator.userAgent;
};

var isWindowsPhone = /windows phone|iemobile|wpdesktop/i;
sdk.features.isFingerprintSupported = function() {
var agent = sdk.fingerprint._getUserAgent();
return agent && !isWindowsPhone.test(agent);
};

sdk.tokenManager = new TokenManager(sdk, args.tokenManager);
}

Expand Down Expand Up @@ -178,6 +192,60 @@ proto.webfinger = function (opts) {
return http.get(this, url, options);
};

proto.fingerprint = function(options) {
options = options || {};
var sdk = this;
if (!sdk.features.isFingerprintSupported()) {
return Q.reject(new AuthSdkError('Fingerprinting is not supported on this device'));
}

var deferred = Q.defer();

// create an iframe
var iframe = document.createElement('iframe');
iframe.style.display = 'none';

function listener(e) {
if (!e || !e.data || e.origin !== sdk.options.url) {
return;
}

try {
var msg = JSON.parse(e.data);
} catch (err) {
return deferred.reject(new AuthSdkError('Unable to parse iframe response'));
}

if (!msg) { return; }
if (msg.type === 'FingerprintAvailable') {
return deferred.resolve(msg.fingerprint);
}
if (msg.type === 'FingerprintServiceReady') {
if (iframe.contentWindow) {
iframe.contentWindow.postMessage(JSON.stringify({
type: 'GetFingerprint'
}), sdk.options.url);
}
}
}
oauthUtil.addListener(window, 'message', listener);

iframe.src = sdk.options.url + '/auth/services/devicefingerprint';
document.body.appendChild(iframe);

var timeout = setTimeout(function() {
deferred.reject(new AuthSdkError('Fingerprinting timed out'));
}, options.timeout || 15000);

return deferred.promise.fin(function() {
clearTimeout(timeout);
oauthUtil.removeListener(window, 'message', listener);
if (document.body.contains(iframe)) {
iframe.parentElement.removeChild(iframe);
}
});
};

module.exports = function(ajaxRequest) {
function OktaAuth(args) {
if (!(this instanceof OktaAuth)) {
Expand Down
149 changes: 149 additions & 0 deletions test/spec/fingerprint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
define(function(require) {
var OktaAuth = require('OktaAuth');
var util = require('../util/util');

describe('fingerprint', function() {
function setup(options) {
options = options || {};
var test = this;
var listener;
var postMessageSpy = jasmine.createSpy('postMessage').and.callFake(function(msg, url) {
// "receive" the message in the iframe
expect(url).toEqual('http://example.okta.com');
expect(msg).toEqual(jasmine.any(String));
expect(JSON.parse(msg).type).toEqual('GetFingerprint');
expect(listener).toEqual(jasmine.any(Function));
listener({
data: JSON.stringify({
type: 'FingerprintAvailable',
fingerprint: 'ABCD'
}),
origin: 'http://example.okta.com'
});
});

test.iframe = {
style: {},
contentWindow: {
postMessage: postMessageSpy
},
parentElement: {
removeChild: jasmine.createSpy('removeChild')
}
};

spyOn(window, 'addEventListener').and.callFake(function(name, fn) {
expect(name).toEqual('message');
listener = fn;
});
spyOn(document, 'createElement').and.returnValue(test.iframe);
spyOn(document.body, 'contains').and.returnValue(true);
spyOn(document.body, 'appendChild').and.callFake(function() {
if (options.timeout) { return; }
// mimic async page load with setTimeouts
if (options.sendOtherMessage) {
setTimeout(function() {
listener({
data: '{"not":"forUs"}',
origin: 'http://not.okta.com'
});
});
}
setTimeout(function() {
listener({
data: options.firstMessage || JSON.stringify({
type: 'FingerprintServiceReady'
}),
origin: 'http://example.okta.com'
});
});
});

var authClient = new OktaAuth({
url: 'http://example.okta.com'
});
if (typeof options.userAgent !== 'undefined') {
util.mockUserAgent(authClient, options.userAgent);
}
return authClient.fingerprint({ timeout: options.timeout });
}

it('iframe is created with the right src and it is hidden', function (done) {
return setup()
.catch(function(err) {
expect(err).toBeUndefined();
})
.then(function(fingerprint) {
var test = this;
expect(document.createElement).toHaveBeenCalled();
expect(test.iframe.style.display).toEqual('none');
expect(test.iframe.src).toEqual('http://example.okta.com/auth/services/devicefingerprint');
expect(document.body.appendChild).toHaveBeenCalledWith(test.iframe);
expect(test.iframe.parentElement.removeChild).toHaveBeenCalled();
expect(fingerprint).toEqual('ABCD');
})
.fin(function() {
done();
});
});

it('allows non-Okta postMessages', function (done) {
return setup({ sendOtherMessage: true })
.catch(function(err) {
expect(err).toBeUndefined();
})
.then(function(fingerprint) {
expect(fingerprint).toEqual('ABCD');
})
.fin(function() {
done();
});
});

it('fails if the iframe sends invalid message content', function (done) {
return setup({ firstMessage: 'invalidMessageContent' })
.then(function() {
done.fail('Fingerprint promise should have been rejected');
})
.catch(function(err) {
util.assertAuthSdkError(err, 'Unable to parse iframe response');
done();
});
});

it('fails if user agent is not defined', function (done) {
return setup({ userAgent: '' })
.then(function() {
done.fail('Fingerprint promise should have been rejected');
})
.catch(function(err) {
util.assertAuthSdkError(err, 'Fingerprinting is not supported on this device');
done();
});
});

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;)'
})
.then(function() {
done.fail('Fingerprint promise should have been rejected');
})
.catch(function(err) {
util.assertAuthSdkError(err, 'Fingerprinting is not supported on this device');
done();
});
});

it('fails after a timeout period', function (done) {
return setup({ timeout: 5 })
.then(function() {
done.fail('Fingerprint promise should have been rejected');
})
.catch(function(err) {
util.assertAuthSdkError(err, 'Fingerprinting timed out');
done();
});
});
});
});
4 changes: 4 additions & 0 deletions test/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,10 @@ define(function(require) {
spyOn(client.token.parseFromUrl, '_getLocation').and.returnValue(mockLocation);
};

util.mockUserAgent = function (client, mockUserAgent) {
spyOn(client.fingerprint, '_getUserAgent').and.returnValue(mockUserAgent);
};

util.expectErrorToEqual = function (actual, expected) {
expect(actual.name).toEqual(expected.name);
expect(actual.message).toEqual(expected.message);
Expand Down

0 comments on commit 7aed53d

Please sign in to comment.