Compare commits

...

68 Commits

Author SHA1 Message Date
AJ ONeal d52fccfacf v1.0.8 2018-04-30 22:13:29 -06:00
AJ ONeal 589403e8bb fix https://github.com/Daplie/browser-authenticator/issues/6 2018-04-30 22:13:05 -06:00
AJ ONeal b2f4619186 update demo 2018-04-30 22:03:46 -06:00
AJ ONeal 06317d1812 conditionally load forge 2018-04-30 21:34:41 -06:00
AJ ONeal 579250c5a8 add webapp install instructions 2018-04-30 20:58:19 -06:00
AJ ONeal 4efeb8c0f6 update alt text for fit forking 2018-04-30 20:48:55 -06:00
AJ ONeal b0b6b17174 update logo 2018-04-30 20:38:50 -06:00
AJ ONeal 1487452c0c v1.0.7 2018-04-21 15:24:09 -06:00
AJ ONeal 2e4c9bba99 Update 'README.md' 2017-11-20 16:38:24 +00:00
tigerbot 4894610c97 v1.0.6 2017-03-28 11:29:13 -06:00
tigerbot 903d9d5737 added code for gh-pages demo to bower ignore 2017-03-28 11:20:12 -06:00
tigerbot 4ac56bbd26 updated the bower forge dependency to 0.7 2017-03-28 10:48:54 -06:00
Connie 50bdd31ecd Fix "Do not work in Firefox" bug
For firefox, optlink.search return empty string.
Create a function to extract query string and pass to parseQuery function.
2017-03-27 15:12:15 -06:00
AJ ONeal 407521782c auto-update banner 2016-12-30 02:38:28 -07:00
AJ ONeal fa03585ab5 auto-update ad 2016-12-30 00:52:12 -07:00
AJ ONeal 7312c405b9 Update README.md 2016-11-25 10:21:03 -07:00
AJ ONeal 4425722cb2 Update README.md 2016-11-01 18:27:58 -06:00
AJ ONeal 69991bd337 #1 add issuer to demo (for google auth) 2016-09-02 12:44:57 -06:00
AJ ONeal 89801a7604 #1 add issuer to demo (for google auth) 2016-09-02 12:42:32 -06:00
AJ ONeal 66a64dae72 bump 2015-11-03 00:31:24 -08:00
AJ ONeal 1454ff4641 add generateTotpUri 2015-11-03 00:31:06 -08:00
AJ ONeal 3b0abbc015 Merge branch 'gh-pages' of github.com:Daplie/browser-authenticator into gh-pages 2015-11-03 00:24:26 -08:00
AJ ONeal d0554d9cd8 move demo files to ./demo 2015-11-03 00:24:16 -08:00
AJ ONeal ec14626deb Update README.md 2015-10-26 16:54:58 -07:00
AJ ONeal ef5fa1a27b move image to clickable area 2015-10-26 16:52:03 -07:00
AJ ONeal 82e00dfa99 link to github, duh 2015-10-26 16:47:00 -07:00
AJ ONeal d95c5be22e add github banner to img csp 2015-10-26 16:45:46 -07:00
AJ ONeal 7f19d8d107 add github banner and examples/ 2015-10-26 16:38:31 -07:00
AJ ONeal e27beb64d3 Merge branch 'gh-pages' of github.com:Daplie/browser-authenticator into gh-pages 2015-10-26 15:10:31 -07:00
AJ ONeal a3f243363f removed excess debugging 2015-10-26 15:10:25 -07:00
AJ ONeal 3c7c5f4378 Update README.md 2015-10-26 15:09:05 -07:00
AJ ONeal 47c51f4a12 Update README.md 2015-10-26 15:07:32 -07:00
AJ ONeal f8ba322b50 fixes for iOS Mobile Safari and Chrome's iOS WebView 2015-10-26 14:49:59 -07:00
AJ ONeal ba4f458756 Merge branch 'design' into gh-pages 2015-10-26 13:51:57 -07:00
AJ ONeal b976008687 change js file name 2015-10-26 13:51:37 -07:00
AJ ONeal e999d1e37a now functional 2015-10-26 13:48:33 -07:00
Bryson Hill 30d47d62a6 add design 2015-10-24 18:50:00 -07:00
AJ ONeal d46a49a90f Update README.md 2015-10-23 12:47:54 -07:00
AJ ONeal 8735096fa5 bump 2015-10-23 03:21:39 -07:00
AJ ONeal 621984664d update botp 2015-10-23 03:20:41 -07:00
AJ ONeal aa28eaa713 try/catch WebCrypto, fallback to forge 2015-10-23 03:12:20 -07:00
AJ ONeal 20afbd65b1 handle promise rejection 2015-10-23 02:50:35 -07:00
AJ ONeal 06d72cc430 mobile safari... 2015-10-23 02:42:47 -07:00
AJ ONeal 92b62946d3 debugging mobile safari 2015-10-23 02:39:29 -07:00
AJ ONeal 51ab2d9797 try mobile safari again 2015-10-23 02:26:11 -07:00
AJ ONeal 1d7a1689ec ignore mobile safari for now 2015-10-23 02:25:05 -07:00
AJ ONeal 07ac4004b8 mobile safari webcrypto fix 2015-10-23 02:19:32 -07:00
AJ ONeal ec529d807a call, not apply... duh 2015-10-23 02:15:23 -07:00
AJ ONeal e39c6956f2 mobile safari fix 2015-10-23 02:12:29 -07:00
AJ ONeal ae560e7803 add viewport 2015-10-23 02:02:33 -07:00
AJ ONeal 125dca65f5 yes, use encodeURI 2015-10-23 01:46:04 -07:00
AJ ONeal 50e047fae4 update google chart url 2015-10-23 01:42:09 -07:00
AJ ONeal c018cfc3d4 remove encodeURI for now 2015-10-23 01:39:12 -07:00
AJ ONeal 1eb168a071 Merge branch 'gh-pages' of github.com:Daplie/browser-authenticator into gh-pages 2015-10-23 01:34:45 -07:00
AJ ONeal 186f49aedb encodeURIComponent => encodeURI 2015-10-23 01:34:39 -07:00
AJ ONeal 05427858b2 Update README.md 2015-10-23 01:17:53 -07:00
AJ ONeal f6cb3c0296 Update README.md 2015-10-23 01:16:48 -07:00
AJ ONeal e743d0b890 Merge branch 'gh-pages' of github.com:Daplie/browser-authenticator into gh-pages 2015-10-23 01:11:40 -07:00
AJ ONeal 6d28862e02 fix redirect 2015-10-23 01:11:34 -07:00
AJ ONeal c7ea427d96 Update README.md 2015-10-23 01:11:09 -07:00
AJ ONeal 38704eef8d Merge branch 'master' into gh-pages 2015-10-23 01:10:00 -07:00
AJ ONeal 9ee205fa97 update README.md 2015-10-23 01:09:51 -07:00
AJ ONeal ee738a892a excuses excuses 2015-10-23 01:09:22 -07:00
AJ ONeal 9c3ee18ff4 add otpauth 2015-10-23 00:58:19 -07:00
AJ ONeal 5dfb802d97 bugfix 2015-10-23 00:52:35 -07:00
AJ ONeal 2984d36035 add index.html 2015-10-23 00:51:29 -07:00
AJ ONeal c44c623286 add missing sha256.js 2015-10-23 00:49:58 -07:00
AJ ONeal e250c8cb44 assets for gh-pages 2015-10-23 00:42:35 -07:00
25 changed files with 1674 additions and 328 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
bower_components
# Only ignore the bower_components on the top level so we can commit whatever parts of the
# packages we need for the statically served gh-pages to work inside the demo folder.
/bower_components
# Logs
logs

207
LICENSE
View File

@ -1,202 +1,41 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright 2018 AJ ONeal
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
This is open source software; you can redistribute it and/or modify it under the
terms of either:
1. Definitions.
a) the "MIT License"
b) the "Apache-2.0 License"
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
MIT License
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Apache-2.0 License Summary
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

183
README.md
View File

@ -1,2 +1,183 @@
# browser-authenticator
# Browser Authenticator
Two- / Multi- Factor Authenication (2FA / MFA) for browser JavaScript
<!-- [![](http://i.imgur.com/sdvMdbo.png)](https://authenticator.ppl.family/) -->
![](https://blog.authy.com/assets/posts/authenticator.png)
There are a number of apps that various websites use to give you 6-digit codes to increase security when you log in:
* [Authy](https://www.authy.com/personal/) [iPhone](https://itunes.apple.com/us/app/authy/id494168017?mt=8) | [Android](https://play.google.com/store/apps/details?id=com.authy.authy&hl=en) | [Chrome](https://chrome.google.com/webstore/detail/authy/gaedmjdfmmahhbjefcbgaolhhanlaolb?hl=en) | [Linux](https://www.authy.com/personal/) | [OS X](https://www.authy.com/personal/) | [BlackBerry](https://appworld.blackberry.com/webstore/content/38831914/?countrycode=US&lang=en)
* Google Authenticator [iPhone](https://itunes.apple.com/us/app/google-authenticator/id388497605?mt=8) | [Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en)
* Microsoft Authenticator [Windows Phone](https://www.microsoft.com/en-us/store/apps/authenticator/9wzdncrfj3rj) | [Android](https://play.google.com/store/apps/details?id=com.microsoft.msa.authenticator)
* GAuth [FxOS](https://marketplace.firefox.com/app/gauth/)
There are many [Services that Support MFA](http://lifehacker.com/5938565/heres-everywhere-you-should-enable-two-factor-authentication-right-now),
including Google, Microsoft, Facebook, and Digital Ocean for starters.
This module uses [`botp`](https://git.coolaj86.com/coolaj86/botp.js) which implements `TOTP` [(RFC 6238)](https://www.ietf.org/rfc/rfc6238.txt)
(the *Authenticator* standard), which is based on `HOTP` [(RFC 4226)](https://www.ietf.org/rfc/rfc4226.txt)
to provide codes that are exactly compatible with all other *Authenticator* apps and services that use them.
Demo
====
[Live Demo](https://authenticator.ppl.family/)
at <https://authenticator.ppl.family/>
You may also be interested in [Node Authenticator](https://git.coolaj86.com/coolaj86/node-authenticator.js) over at <https://git.coolaj86.com/coolaj86/node-authenticator.js>
Usage
=====
```bash
bower install authenticator --save
```
```javascript
'use strict';
var authenticator = window.Authenticator;
authenticator.generateKey().then(function (formattedKey) {
// "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz"
authenticator.generateTotpUri(formattedKey, "john.doe@email.com", "ACME Co", 'SHA1', 6, 30);
//
// otpauth://totp/ACME%20Co:john.doe@email.com?secret=ACQOUA72D3YFA4E5UORXZTKHJ2XL3WIZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
authenticator.generateToken(formattedKey).then(function (formattedToken) {
// "957 124"
authenticator.verifyToken(formattedKey, formattedToken).then(function (result) {
// { delta: 0 }
});
authenticator.verifyToken(formattedKey, '000 000').then(function (result) {
// null
});
});
});
```
### Browsers that support WebCrypto
In total there are only a few hundred lines of uncompressed code here.
Each file is very small.
```
<script src="bower_components/unibabel/index.js"></script>
<script src="bower_components/unibabel/unibabel.hex.js"></script>
<script src="bower_components/unibabel/unibabel.base32.js"></script>
<script src="bower_components/botp/sha1-hmac.js"></script>
<script src="bower_components/botp/index.js"></script>
<script src="bower_components/authenticator/authenticator.js"></script>
```
### Browsers that do not support WebCrypto
```
<!-- forge.hmac -->
<script src="bower_components/forge/js/util.js"></script>
<script src="bower_components/forge/js/sha1.js"></script>
<script src="bower_components/forge/js/hmac.js"></script>
<!-- forge.random.getBytes -->
<script src="bower_components/forge/js/sha256.js"></script>
<script src="bower_components/forge/js/cipher.js"></script>
<script src="bower_components/forge/js/cipherModes.js"></script>
<script src="bower_components/forge/js/aes.js"></script>
<script src="bower_components/forge/js/prng.js"></script>
<script src="bower_components/forge/js/random.js"></script>
```
TODO: I'd love some help pruning the important bits out of the forge code.
Just using an alternate CPRNG would be very helpful in trimming the fat.
## API
### generateKey()
generates a 32-character (160-bit) base32 key
### generateToken(formattedKey)
generates a 6-digit (20-bit) decimal time-based token
### verifyToken(formattedKey, formattedToken)
validates a time-based token within a +/- 30 second (90 seconds) window
returns `null` on failure or an object such as `{ delta: 0 }` on success
### generateTotpUri(formattedKey, accountName, issuer, algorithm, digits, period)
generates an `OTPAUTH://` scheme URI for QR Code generation.
**OTPAuth Scheme**
* <https://github.com/google/google-authenticator/wiki/Key-Uri-Format>
* `otpauth://totp/<<ISSUER>>:<<ACCOUNT_NAME>>?secret=<<BASE32_KEY>>&issuer=<<ISSUER>>`
* `otpauth://totp/<<ISSUER>>:<<ACCOUNT_NAME>>?secret=<<BASE32_KEY>>&issuer=<<ISSUER>>&algorithm=<<ALGO>>&digits=<<INT>>&period=<<SECONDS>>`
Note that `ISSUER` is specified twice for backwards / forwards compatibility.
QR Code
-------
See <https://davidshimjs.github.io/qrcodejs/> and <https://github.com/soldair/node-qrcode>.
![](http://cdn9.howtogeek.com/wp-content/uploads/2014/10/sshot-7-22.png)
Example use with `qrcode.js` in the browser:
```javascript
'use strict';
var el = document.querySelector('.js-qrcode-canvas');
var link = "otpauth://totp/{{NAME}}?secret={{KEY}}";
var name = "Your Service";
// remove spaces, hyphens, equals, whatever
var key = "acqo ua72 d3yf a4e5 uorx ztkh j2xl 3wiz".replace(/\W/g, '').toLowerCase();
var qr = new QRCode(el, {
text: link.replace(/{{NAME}}/g, name).replace(/{{KEY}}/g, key)
});
```
Formatting
----------
All non-alphanumeric characters are ignored, so you could just as well use hyphens
or periods or whatever suites your use case.
These are just as valid:
* "acqo ua72 d3yf a4e5 - uorx ztkh j2xl 3wiz"
* "98.24.63"
0, 1, 8, and 9 also not used (so that base32).
To further avoid confusion with O, o, L, l, I, B, and g
you may wish to display lowercase instead of uppercase.
TODO: should this library replace 0 with o, 1 with l (or I?), 8 with b, 9 with g, and so on?
90-second Window
----------------
The window is set to +/- 1, meaning each token is valid for a total of 90 seconds
(-30 seconds, +0 seconds, and +30 seconds)
to account for time drift (which should be very rare for mobile devices)
and humans who are handicapped or otherwise struggle with quick fine motor skills (like my grandma).
Install WebApp
--------------
```bash
git clone --depth 1 https://git.coolaj86.com/coolaj86/browser-authenticator.js.git
```

View File

@ -5,7 +5,14 @@ var Authenticator = exports.Authenticator || exports;
var Unibabel = window.Unibabel; // || require('unibabel');
var totp = window.totp; // || require('notp').totp;
if (!window.crypto) {
function loadForge() {
var script = document.createElement('script');
script.src = '/demo/bower_components/forge/dist/forge.min.js';
script.addEventListener('load', collectRandom);
document.body.appendChild(script);
}
function collectRandom() {
document.addEventListener('mousemove', function (event) {
var ev = event || window.event;
@ -14,14 +21,22 @@ if (!window.crypto) {
});
}
if (!window.crypto) {
if (!window.forge) {
loadForge();
} else {
collectRandom();
}
}
// Generate a key
function generateOtpKey() {
// 20 cryptographically random binary bytes (160-bit key)
if (window.crypto) {
try {
var key = window.crypto.getRandomValues(new Uint8Array(20));
return Promise.resolve(key);
} else {
} catch(e) {
// Promises are supported even in Microsoft Edge
// only old IE and old android need shims
return new Promise(function (resolve, reject) {
@ -81,6 +96,18 @@ function verifyGoogleAuthToken(key, token) {
Authenticator.generateKey = generateGoogleAuthKey;
Authenticator.generateToken = generateGoogleAuthToken;
Authenticator.verifyToken = verifyGoogleAuthToken;
Authenticator.generateTotpUri = function (secret, accountName, issuer, algo, digits, period) {
// Full OTPAUTH URI spec as explained at
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
return 'otpauth://totp/'
+ encodeURI(issuer || '') + ':' + encodeURI(accountName || '')
+ '?secret=' + secret.replace(/[\s\.\_\-]+/g, '').toUpperCase()
+ '&issuer=' + encodeURIComponent(issuer || '')
+ '&algorithm=' + (algo || 'SHA1')
+ '&digits=' + (digits || 6)
+ '&period=' + (period || 30)
;
};
}(
'undefined' !== typeof window ? (window.Authenticator = {}) : module.exports

View File

@ -1,7 +1,6 @@
{
"name": "authenticator",
"version": "1.0.3",
"homepage": "https://github.com/Daplie/browser-authenticator",
"homepage": "https://git.coolaj86.com/coolaj86/browser-authenticator.js",
"authors": [
"AJ ONeal <awesome@coolaj86.com>"
],
@ -31,12 +30,16 @@
"**/.*",
"node_modules",
"bower_components",
"demo",
"example",
"index.html",
"test.html",
"test",
"tests"
],
"dependencies": {
"botp": "~3.0.1",
"forge": "~0.6.37",
"unibabel": "~2.1.0"
"botp": "~3.0.2",
"forge": "~0.7.1",
"unibabel": "~2.1.2"
}
}

5
demo/bootstrap-v3.3.5.min.css vendored Normal file

File diff suppressed because one or more lines are too long

249
demo/bower_components/botp/index.js vendored Normal file
View File

@ -0,0 +1,249 @@
(function (exports, TEST) {
'use strict';
var crypto;
var sha1Hmac = exports.sha1Hmac || function (key, bytes) {
if (!crypto) { crypto = require('crypto'); }
var hmac = crypto.createHmac('sha1', new Buffer(key));
// Update the HMAC with the byte array
return hmac.update(new Buffer(bytes)).digest('hex');
};
/**
* convert an integer to a byte array
* @param {Integer} num
* @return {Array} bytes
*/
function intToBytes(num) {
var bytes = [];
for(var i=7 ; i>=0 ; --i) {
bytes[i] = num & (255);
num = num >> 8;
}
return bytes;
}
/**
* convert a hex value to a byte array
* @param {String} hex string of hex to convert to a byte array
* @return {Array} bytes
*/
function hexToBytes(hex) {
var bytes = [];
for(var c = 0, C = hex.length; c < C; c += 2) {
bytes.push(parseInt(hex.substr(c, 2), 16));
}
return bytes;
}
var hotp = {};
/**
* Generate a counter based One Time Password
*
* @return {String} the one time password
*
* Arguments:
*
* args
* key - Key for the one time password. This should be unique and secret for
* every user as this is the seed that is used to calculate the HMAC
*
* counter - Counter value. This should be stored by the application, must
* be user specific, and be incremented for each request.
*
*/
hotp.gen = function(key, opt) {
key = key || '';
opt = opt || {};
var counter = opt.counter || 0;
// Create the byte array
return sha1Hmac(key, intToBytes(counter)).then(function (digest) {
// Get byte array
var h = hexToBytes(digest);
// Truncate
var offset = h[19] & 0xf;
var v = (h[offset] & 0x7f) << 24 |
(h[offset + 1] & 0xff) << 16 |
(h[offset + 2] & 0xff) << 8 |
(h[offset + 3] & 0xff);
v = (v % 1000000) + '';
return new Array(7-v.length).join('0') + v;
});
};
/**
* Check a One Time Password based on a counter.
*
* @return {Object} null if failure, { delta: # } on success
* delta is the time step difference between the client and the server
*
* Arguments:
*
* args
* key - Key for the one time password. This should be unique and secret for
* every user as it is the seed used to calculate the HMAC
*
* token - Passcode to validate.
*
* window - The allowable margin for the counter. The function will check
* 'W' codes in the future against the provided passcode. Note,
* it is the calling applications responsibility to keep track of
* 'W' and increment it for each password check, and also to adjust
* it accordingly in the case where the client and server become
* out of sync (second argument returns non zero).
* E.g. if W = 100, and C = 5, this function will check the passcode
* against all One Time Passcodes between 5 and 105.
*
* Default - 50
*
* counter - Counter value. This should be stored by the application, must
* be user specific, and be incremented for each request.
*
*/
hotp.verify = function(token, key, opt) {
opt = opt || {};
var window = opt.window || 50;
var counter = opt.counter || 0;
var i = counter - window;
var len = counter + window;
// Now loop through from C to C + W to determine if there is
// a correct code
function check(t) {
opt.counter = i + 1;
if (!t) {
return null;
}
if (i > len) {
return null;
}
if(t === token) {
// We have found a matching code, trigger callback
// and pass offset
return i;
}
// TODO count 0, -1, 1, -2, 2, ... instead of -2, -1, 0, 1, ...
i += 1;
return hotp.gen(key, opt).then(check);
}
opt.counter = i;
return hotp.gen(key, opt).then(check).then(function (i) {
if('number' === typeof i) {
return { delta: i - counter };
}
// If we get to here then no codes have matched, return null
return null;
});
};
var totp = {};
/**
* Generate a time based One Time Password
*
* @return {String} the one time password
*
* Arguments:
*
* args
* key - Key for the one time password. This should be unique and secret for
* every user as it is the seed used to calculate the HMAC
*
* time - The time step of the counter. This must be the same for
* every request and is used to calculat C.
*
* Default - 30
*
*/
totp.gen = function(key, opt) {
opt = opt || {};
var time = opt.time || 30;
var _t = Date.now();
// Time has been overwritten.
if(opt._t) {
if(!TEST) {
console.warn('Overwriting time in non-test environment!');
}
_t = opt._t;
}
// Determine the value of the counter, C
// This is the number of time steps in seconds since T0
opt.counter = Math.floor((_t / 1000) / time);
return hotp.gen(key, opt);
};
/**
* Check a One Time Password based on a timer.
*
* @return {Object} null if failure, { delta: # } on success
* delta is the time step difference between the client and the server
*
* Arguments:
*
* args
* key - Key for the one time password. This should be unique and secret for
* every user as it is the seed used to calculate the HMAC
*
* token - Passcode to validate.
*
* window - The allowable margin for the counter. The function will check
* 'W' codes either side of the provided counter. Note,
* it is the calling applications responsibility to keep track of
* 'W' and increment it for each password check, and also to adjust
* it accordingly in the case where the client and server become
* out of sync (second argument returns non zero).
* E.g. if W = 5, and C = 1000, this function will check the passcode
* against all One Time Passcodes between 995 and 1005.
*
* Default - 6
*
* time - The time step of the counter. This must be the same for
* every request and is used to calculate C.
*
* Default - 30
*
*/
totp.verify = function(token, key, opt) {
opt = opt || {};
var time = opt.time || 30;
var _t = Date.now();
// Time has been overwritten.
if(opt._t) {
if(!TEST) {
console.warn('Overwriting time in non-test environment!');
}
_t = opt._t;
}
// Determine the value of the counter, C
// This is the number of time steps in seconds since T0
opt.counter = Math.floor((_t / 1000) / time);
return hotp.verify(token, key, opt);
};
exports.hotp = hotp;
exports.totp = totp;
}(
'undefined' !== typeof window ? window : module.exports
, 'undefined' !== typeof process ? process.env.NODE_ENV : false
));

98
demo/bower_components/botp/sha1-hmac.js vendored Normal file
View File

@ -0,0 +1,98 @@
(function (exports) {
'use strict';
exports.sha1Hmac = function (key, bytes) {
if (!window.Unibabel) {
throw new Error("Unibabel.js is required to convert between buffers and binary strings");
}
if ('string' === typeof key) {
throw new Error("use one of Unibabel.utf8ToBuffer(key), Unibabel.base64ToBuffer(key), or Unibabel.hexToBuffer(key) before passing to sha1Hmac(key, bytes)");
}
var Unibabel = window.Unibabel;
function useForge() {
var forge = window.forge;
var hmac = forge.hmac.create();
var digest;
hmac.start('sha1', Unibabel.bufferToBinaryString(key));
hmac.update(Unibabel.bufferToBinaryString(bytes));
digest = hmac.digest().toHex();
return window.Promise.resolve(digest);
}
function useWebCrypto() {
return (window.crypto.subtle||window.crypto.webkitSubtle).importKey(
"raw"
, key
, { name: "HMAC"
, hash: { name: "SHA-1" }
}
, false
, ["sign", "verify"]
)
/*
return crypto.subtle.importKey(
"jwk", //can be "jwk" or "raw"
{ //this is an example jwk key, "raw" would be an ArrayBuffer
kty: "oct",
k: "Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE",
alg: "HS256",
ext: true,
},
{ //this is the algorithm options
name: "HMAC",
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
//length: 256, //optional, if you want your key length to differ from the hash function's block length
},
false, //whether the key is extractable (i.e. can be used in exportKey)
["sign", "verify"] //can be any combination of "sign" and "verify"
)
*/
.then(function (cryptoKey) {
return (window.crypto.subtle||window.crypto.webkitSubtle).sign(
{ name: "HMAC" }
, cryptoKey // from generateKey or importKey above
, new Uint8Array(bytes) // ArrayBuffer of data you want to sign
)
.then(function(signature){
// returns an ArrayBuffer containing the signature
return Unibabel.bufferToHex(new Uint8Array(signature));
});
});
}
if (window.crypto) {
// WebCrypto is so unreliable right now... ugh...
try {
return useWebCrypto().then(function (result) {
return result;
}, function (err) {
console.warn(err);
console.warn(err.stack);
console.warn("WebCrypto failed, trying forge.js");
return useForge();
});
} catch(e) {
console.warn(e);
console.warn(e.stack);
console.warn("WebCrypto threw exception, trying forge.js");
return useForge();
}
}
else if (window.forge) {
return useForge();
}
else {
throw new Error("WebCrypto or forge.js is required to create a sha1 hmac");
}
};
}(
'undefined' !== typeof window ? window : module.exports
, 'undefined' !== typeof process ? process.env.NODE_ENV : false
));

File diff suppressed because one or more lines are too long

104
demo/bower_components/unibabel/index.js vendored Normal file
View File

@ -0,0 +1,104 @@
(function () {
'use strict';
function utf8ToBinaryString(str) {
var escstr = encodeURIComponent(str);
// replaces any uri escape sequence, such as %0A,
// with binary escape, such as 0x0A
var binstr = escstr.replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode(parseInt(p1, 16));
});
return binstr;
}
function utf8ToBuffer(str) {
var binstr = utf8ToBinaryString(str);
var buf = binaryStringToBuffer(binstr);
return buf;
}
function utf8ToBase64(str) {
var binstr = utf8ToBinaryString(str);
return btoa(binstr);
}
function binaryStringToUtf8(binstr) {
var escstr = binstr.replace(/(.)/g, function (m, p) {
var code = p.charCodeAt(0).toString(16).toUpperCase();
if (code.length < 2) {
code = '0' + code;
}
return '%' + code;
});
return decodeURIComponent(escstr);
}
function bufferToUtf8(buf) {
var binstr = bufferToBinaryString(buf);
return binaryStringToUtf8(binstr);
}
function base64ToUtf8(b64) {
var binstr = atob(b64);
return binaryStringToUtf8(binstr);
}
function bufferToBinaryString(buf) {
var binstr = Array.prototype.map.call(buf, function (ch) {
return String.fromCharCode(ch);
}).join('');
return binstr;
}
function bufferToBase64(arr) {
var binstr = bufferToBinaryString(arr);
return btoa(binstr);
}
function binaryStringToBuffer(binstr) {
var buf;
if ('undefined' !== typeof Uint8Array) {
buf = new Uint8Array(binstr.length);
} else {
buf = [];
}
Array.prototype.forEach.call(binstr, function (ch, i) {
buf[i] = ch.charCodeAt(0);
});
return buf;
}
function base64ToBuffer(base64) {
var binstr = atob(base64);
var buf = binaryStringToBuffer(binstr);
return buf;
}
window.Unibabel = {
utf8ToBinaryString: utf8ToBinaryString
, utf8ToBuffer: utf8ToBuffer
, utf8ToBase64: utf8ToBase64
, binaryStringToUtf8: binaryStringToUtf8
, bufferToUtf8: bufferToUtf8
, base64ToUtf8: base64ToUtf8
, bufferToBinaryString: bufferToBinaryString
, bufferToBase64: bufferToBase64
, binaryStringToBuffer: binaryStringToBuffer
, base64ToBuffer: base64ToBuffer
// compat
, strToUtf8Arr: utf8ToBuffer
, utf8ArrToStr: bufferToUtf8
, arrToBase64: bufferToBase64
, base64ToArr: base64ToBuffer
};
}());

View File

@ -0,0 +1,142 @@
/*
Copyright (c) 2011, Chris Umbel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
(function (exports) {
'use strict';
var charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var byteTable = [
0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff
];
function quintetCount(buff) {
var quintets = Math.floor(buff.length / 5);
return buff.length % 5 === 0 ? quintets: quintets + 1;
}
exports.bufferToBase32 = function(plain) {
// plain MUST come in either as Array or Uint8Array
if('undefined' !== typeof Uint8Array) {
if (!(plain instanceof Uint8Array)){
plain = new Uint8Array(plain);
}
}
var i = 0;
var j = 0;
var shiftIndex = 0;
var digit = 0;
var encoded = new Array(quintetCount(plain) * 8);
/* byte by byte isn't as pretty as quintet by quintet but tests a bit
faster. will have to revisit. */
while(i < plain.length) {
var current = plain[i];
if(shiftIndex > 3) {
digit = current & (0xff >> shiftIndex);
shiftIndex = (shiftIndex + 5) % 8;
digit = (digit << shiftIndex) | ((i + 1 < plain.length) ?
plain[i + 1] : 0) >> (8 - shiftIndex);
i++;
} else {
digit = (current >> (8 - (shiftIndex + 5))) & 0x1f;
shiftIndex = (shiftIndex + 5) % 8;
if(shiftIndex === 0) { i++; }
}
encoded[j] = charTable[digit];
j++;
}
for(i = j; i < encoded.length; i++) {
encoded[i] = '=';
}
return encoded.join('');
};
exports.base32ToBuffer = function(encoded) {
var shiftIndex = 0;
var plainDigit = 0;
var plainChar;
var plainPos = 0;
var len = Math.ceil(encoded.length * 5 / 8);
var decoded;
encoded = encoded.split('').map(function (ch) {
return ch.charCodeAt(0);
});
if('undefined' !== typeof Uint8Array) {
encoded = new Uint8Array(encoded);
decoded = new Uint8Array(len);
} else {
decoded = new Array(len);
}
/* byte by byte isn't as pretty as octet by octet but tests a bit
faster. will have to revisit. */
for(var i = 0; i < encoded.length; i++) {
if(encoded[i] === 0x3d){ //'='
break;
}
var encodedByte = encoded[i] - 0x30;
if(encodedByte < byteTable.length) {
plainDigit = byteTable[encodedByte];
if(shiftIndex <= 3) {
shiftIndex = (shiftIndex + 5) % 8;
if(shiftIndex === 0) {
plainChar |= plainDigit;
decoded[plainPos] = plainChar;
plainPos++;
plainChar = 0;
} else {
plainChar |= 0xff & (plainDigit << (8 - shiftIndex));
}
} else {
shiftIndex = (shiftIndex + 5) % 8;
plainChar |= 0xff & (plainDigit >>> shiftIndex);
decoded[plainPos] = plainChar;
plainPos++;
plainChar = 0xff & (plainDigit << (8 - shiftIndex));
}
} else {
throw new Error('Invalid input - it is not base32 encoded string');
}
}
if (decoded.slice) { // Array or TypedArray
return decoded.slice(0, plainPos);
} else { // Mobile Safari TypedArray
return new Uint8Array(Array.prototype.slice.call(decoded, 0, plainPos));
}
};
}(window.Unibabel || window));

View File

@ -0,0 +1,46 @@
(function () {
'use strict';
function bufferToHex(arr) {
var i;
var len;
var hex = '';
var c;
for (i = 0, len = arr.length; i < len; i += 1) {
c = arr[i].toString(16);
if (c.length < 2) {
c = '0' + c;
}
hex += c;
}
return hex;
}
function hexToBuffer(hex) {
// TODO use Uint8Array or ArrayBuffer or DataView
var i;
var byteLen = hex.length / 2;
var arr;
var j = 0;
if (byteLen !== parseInt(byteLen, 10)) {
throw new Error("Invalid hex length '" + hex.length + "'");
}
arr = new Uint8Array(byteLen);
for (i = 0; i < byteLen; i += 1) {
arr[i] = parseInt(hex[j] + hex[j + 1], 16);
j += 2;
}
return arr;
}
// Hex Convenience Functions
window.Unibabel.hexToBuffer = hexToBuffer;
window.Unibabel.bufferToHex = bufferToHex;
}());

109
demo/demo.js Normal file
View File

@ -0,0 +1,109 @@
// This is crap demo code. Forgive me.
(function (exports) {
'use strict';
window.addEventListener("load", function () {
window.document.body.className += " in";
});
var defaultKey = 'hxdm vjec jjws rb3h wizr 4ifu gftm xboz';
var key;
var Authenticator = exports.Authenticator;
var $ = function (x) {
return document.querySelector(x);
};
function generate(ke) {
Authenticator.generateKey().then(function (k) {
var $keyEl = $('.js-key');
if (ke) {
key = ke;
}
else if ($keyEl.value) {
key = $keyEl.value;
$keyEl.placeholder = key;
$keyEl.value = '';
}
else {
key = k;
$keyEl.placeholder = key;
}
var companyName = $('.js-company-name').value;
var userAccount = $('.js-user-account').value;
var algo = $('.js-totp-algo').value || 'SHA1';
var digits = parseInt($('.js-totp-digits').value, 10) || 6;
var period = parseInt($('.js-totp-period').value, 10) || 30;
var otpauth = Authenticator.generateTotpUri(key, userAccount, companyName, algo, digits, period);
/*
var otpauth = 'otpauth://totp/'
+ encodeURI(companyName) + ':' + encodeURI(userAccount)
+ '?secret=' + key.replace(/\s+/g, '').toUpperCase()
;
*/
// obviously don't use this in production, but it's not so bad for the demo
// (hmm... no one has ever said those words and regretted them... TODO XXX generate QR locally)
var src = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(otpauth);
$('.js-otpauth').innerHTML = otpauth; // only safe to inject because I created it
$('.js-otpauth').href = otpauth;
$('img.js-qrcode').src = src;
$('.js-otp-iframe').src = 'demo/phone.html?otpuri=' + encodeURIComponent(otpauth);
Authenticator.generateToken(key).then(function (token) {
console.log('token', token);
Authenticator.verifyToken(key, token).then(function (result) {
console.log('verify', result);
Authenticator.verifyToken(key, '000 000').then(function (result) {
console.log('reject', result);
});
});
});
});
}
$('.js-verify').addEventListener('click', function () {
var token = $('.js-token').value;
if (!/.*\d{3}.*\d{3}.*/.test(token)) {
window.alert("must have a 6 digit token");
return;
}
Authenticator.verifyToken(key, token).then(function (result) {
var msg;
if (result) {
msg = 'Correct!';
} else {
msg = 'FAIL!';
}
console.info('verify', msg);
window.alert(msg);
}, function (err) {
window.alert('[ERROR]:' + err.message);
window.alert('[ERROR]:' + err.stack);
console.error('ERROR');
console.error(err);
});
});
$('.js-generate').addEventListener('click', function () {
generate(null);
});
$('.js-company-name').value = 'ACME Co';
$('.js-user-account').value = 'john@example.com';
$('.js-key').placeholder = defaultKey;
generate(defaultKey);
}(
'undefined' !== typeof window ? window : module.exports
));

BIN
demo/iPhoneMockup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

6
demo/jquery-2.0.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

173
demo/jquery.countdown.js Normal file
View File

@ -0,0 +1,173 @@
;(function ($, window, document, undefined) {
var pluginName = "countdown360",
defaults = {
radius: 15.5, // radius of arc
strokeStyle: "#9990F0", // the color of the stroke
strokeWidth: 2, // the stroke width, dynamically calulated if omitted in options
fillStyle: "#FFFFFF", // the fill color
fontColor: "#477050", // the font color
fontFamily: "Helvetica Neue", // the font family
fontSize: 28, // the font size, dynamically calulated if omitted in options
fontWeight: 100, // the font weight
autostart: true, // start the countdown automatically
seconds: 30, // the number of seconds to count down
label: ["second", "seconds"], // the label to use or false if none
startOverAfterAdding: true, // Start the timer over after time is added with addSeconds
onComplete: function () {}
};
function Plugin(element, options) {
this.element = element;
this.settings = $.extend({}, defaults, options);
if (!this.settings.fontSize) { this.settings.fontSize = this.settings.radius/1.2; }
if (!this.settings.strokeWidth) { this.settings.strokeWidth = this.settings.radius/4; }
this._defaults = defaults;
this._name = pluginName;
this._init();
}
Plugin.prototype = {
getTimeRemaining: function()
{
var timeRemaining = this._secondsLeft(this.getElapsedTime());
return timeRemaining;
},
getElapsedTime: function()
{
return Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
},
extendTimer: function (value) {
var seconds = parseInt(value),
secondsElapsed = Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
if ((this._secondsLeft(secondsElapsed) + seconds) <= this.settings.seconds) {
this.startedAt.setSeconds(this.startedAt.getSeconds() + parseInt(value));
}
},
addSeconds: function (value) {
var secondsElapsed = Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
if (this.settings.startOverAfterAdding) {
this.settings.seconds = this._secondsLeft(secondsElapsed) + parseInt(value);
this.start();
} else {
this.settings.seconds += parseInt(value);
}
},
start: function (date) {
this.updateInterval = 25;
this.startedAt = date || new Date();
this._drawCountdownShape(Math.PI*3.5, true);
this._drawCountdownLabel(0);
this._interval = setInterval(jQuery.proxy(this._draw, this), this.updateInterval);
},
stop: function (cb) {
clearInterval(this._interval);
if (cb) { cb(); }
},
_init: function () {
this.settings.width = (this.settings.radius * 2) + (this.settings.strokeWidth * 2);
this.settings.height = this.settings.width;
this.settings.arcX = this.settings.radius + this.settings.strokeWidth;
this.settings.arcY = this.settings.arcX;
this._initPen(this._getCanvas());
if (this.settings.autostart) { this.start(); }
},
_getCanvas: function () {
var $canvas = $("<canvas id=\"countdown360_" + $(this.element).attr("id") + "\" width=\"" +
this.settings.width + "\" height=\"" +
this.settings.height + "\">" +
"<span id=\"countdown-text\" role=\"status\" aria-live=\"assertive\"></span></canvas>");
$(this.element).prepend($canvas[0]);
return $canvas[0];
},
_initPen: function (canvas) {
this.pen = canvas.getContext("2d");
this.pen.lineWidth = this.settings.strokeWidth;
this.pen.strokeStyle = this.settings.strokeStyle;
this.pen.fillStyle = this.settings.fillStyle;
this.pen.textAlign = "center";
this.pen.textBaseline = "middle";
this.ariaText = $(canvas).children("#countdown-text");
this._clearRect();
},
_clearRect: function () {
this.pen.clearRect(0, 0, this.settings.width, this.settings.height);
},
_secondsLeft: function(secondsElapsed) {
return this.settings.seconds - secondsElapsed;
},
_drawCountdownLabel: function (secondsElapsed) {
this.ariaText.text(secondsLeft);
this.pen.font = this.settings.fontWeight + " " + this.settings.fontSize + "px " + this.settings.fontFamily;
var secondsLeft = this._secondsLeft(secondsElapsed),
label = secondsLeft === 1 ? this.settings.label[0] : this.settings.label[1],
drawLabel = this.settings.label && this.settings.label.length === 2,
x = this.settings.width/2;
if (drawLabel) {
y = this.settings.height/2 - (this.settings.fontSize/6.2);
} else {
y = this.settings.height/2;
}
this.pen.fillStyle = this.settings.fillStyle;
this.pen.fillText(secondsLeft + 1, x, y);
this.pen.fillStyle = this.settings.fontColor;
this.pen.fillText(secondsLeft, x, y);
if (drawLabel) {
this.pen.font = "normal small-caps " + (this.settings.fontSize/3) + "px " + this.settings.fontFamily;
this.pen.fillText(label, this.settings.width/2, this.settings.height/2 + (this.settings.fontSize/2.2));
}
},
_drawCountdownShape: function (endAngle, drawStroke) {
this.pen.fillStyle = this.settings.fillStyle;
this.pen.beginPath();
this.pen.arc(this.settings.arcX, this.settings.arcY, this.settings.radius, Math.PI*1.5, endAngle, false);
this.pen.fill();
if (drawStroke) { this.pen.stroke(); }
},
_draw: function () {
var secondsElapsed = Math.round((new Date().getTime() - this.startedAt.getTime())/1000);
var milisecondsElapsed = Math.round((Date.now() - this.startedAt.getTime()));
var whole = (Math.PI*2)/(this.settings.seconds * 1000);
var parts = milisecondsElapsed;
var endAngle = (Math.PI*3.5)
- ( (whole) * parts);
//console.log('endAngle', endAngle);
this._clearRect();
this._drawCountdownShape(Math.PI*3.5, false);
if (secondsElapsed < this.settings.seconds) {
this._drawCountdownShape(endAngle, true);
this._drawCountdownLabel(secondsElapsed);
} else {
this._drawCountdownLabel(this.settings.seconds);
this.stop();
this.settings.onComplete();
}
}
};
$.fn[pluginName] = function (options) {
var plugin;
this.each(function() {
plugin = $.data(this, "plugin_" + pluginName);
if (!plugin) {
plugin = new Plugin(this, options);
$.data(this, "plugin_" + pluginName, plugin);
}
});
return plugin;
};
})(jQuery, window, document);

1
demo/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 338.63 338.63"><defs><style>.a{fill:#5b2c98;}.b{fill:#fff;}</style></defs><title>ppl-circle-</title><circle class="a" cx="169.31" cy="169.31" r="150"/><path class="b" d="M146.9,135.45a32.24,32.24,0,0,0-6.06-10.22,29,29,0,0,0-9.22-6.9A26.69,26.69,0,0,0,120,115.78a24.92,24.92,0,0,0-12.61,3.14,25.46,25.46,0,0,0-8.91,8.63v-10.7H84.55v87.61h15.93V168.21a23.17,23.17,0,0,0,8.56,8.85,24.5,24.5,0,0,0,12.83,3.27,24.16,24.16,0,0,0,10.94-2.5,26.31,26.31,0,0,0,8.62-6.89,32.55,32.55,0,0,0,5.65-10.34,39.2,39.2,0,0,0,2-12.72A35.4,35.4,0,0,0,146.9,135.45Zm-30.12,32.69c-10.09,0-18.28-8.7-18.28-19.43a20.19,20.19,0,0,1,2-8.77v-.15a14,14,0,0,1,2.43-4.16,19.11,19.11,0,0,1,3.63-3.33,16.49,16.49,0,0,1,4.4-2.2,15.12,15.12,0,0,1,4.63-.77h.07c.37,0,.76,0,1.14,0,10.1,0,18.28,8.7,18.28,19.42S126.88,168.14,116.78,168.14Z"/><path class="b" d="M222.27,135.45a32.24,32.24,0,0,0-6.06-10.22,29,29,0,0,0-9.22-6.9,26.69,26.69,0,0,0-11.64-2.55,24.92,24.92,0,0,0-12.61,3.14,25.54,25.54,0,0,0-8.91,8.63v-10.7H159.92v87.61h15.93V168.21a23.24,23.24,0,0,0,8.56,8.85,24.52,24.52,0,0,0,12.84,3.27,24.22,24.22,0,0,0,10.94-2.5,26.27,26.27,0,0,0,8.61-6.89,32.55,32.55,0,0,0,5.65-10.34,39.47,39.47,0,0,0,2-12.72A35.4,35.4,0,0,0,222.27,135.45Zm-31.6,32.69c-10.09,0-18.28-8.7-18.28-19.43s8.19-19.42,18.28-19.42a18,18,0,0,1,2.09.13h.09a15.77,15.77,0,0,1,4.93,1.32,17.08,17.08,0,0,1,4.51,3l.37.34A19.86,19.86,0,0,1,209,148.71C209,159.44,200.76,168.14,190.67,168.14Z"/><path class="b" d="M263.94,164.76a20.11,20.11,0,0,1-3.27,1.18,11.85,11.85,0,0,1-3.16.48,6,6,0,0,1-4.75-1.9,7.84,7.84,0,0,1-1.66-5.35V92.36H235.17v72.4q0,7.37,4.1,11.35t11.59,4a41.31,41.31,0,0,0,7.73-.83,30.52,30.52,0,0,0,7.49-2.38Z"/><path class="b" d="M208,229.74c-15.45,8.56-32.81,12.13-49.22,12.13S125,238.3,109.54,229.74l-5.24,12.84c16.41,10,33.53,15.69,54.45,15.69s38.05-5.71,54.45-15.69Z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

42
demo/phone.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<title>Authenticator</title>
<meta charset="UTF-8">
<!--meta name="viewport" content="width=device-width, user-scalable=no" /-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' https://chart.googleapis.com/ data:; child-src 'self'; object-src 'none'">
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body class="fade">
<div class="phone-screen">
<div class="logo"></div>
<div class="token-issuer uppercase">your <span class="js-issuer">Company</span> token is:</div>
<div class="token-phone"> <span class="js-token">--- ---</span> </div>
<div class="js-countdown countdown"></div>
<div class="js-account-name account-name">123@abc.xyz</div>
</div>
<!-- extremely lightweight shim for hex conversion -->
<script src="bower_components/unibabel/index.js"></script>
<script src="bower_components/unibabel/unibabel.hex.js"></script>
<!-- base32 conversion (and binary string for forge) (works standalone from the above) -->
<script src="bower_components/unibabel/unibabel.base32.js"></script>
<!-- forge.* -->
<script src="bower_components/forge/dist/forge.min.js"></script>
<!-- botp.totp -->
<script src="bower_components/botp/sha1-hmac.js"></script>
<script src="bower_components/botp/index.js"></script>
<!-- Authenticator -->
<script src="../authenticator.js"></script>
<script src="jquery-2.0.0.min.js" type="text/javascript"></script>
<script src="jquery.countdown.js" type="text/javascript"></script>
<!-- The Magic -->
<script src="phone.js" type="text/javascript"></script>
</body>
</html>

89
demo/phone.js Normal file
View File

@ -0,0 +1,89 @@
(function (exports) {
'use strict';
$('body').addClass('in');
var Authenticator = exports.Authenticator;
function parseQuery(search) {
var args = search.substring(1).split('&');
var argsParsed = {};
var i, arg, kvp, key, value;
for (i=0; i < args.length; i++) {
arg = args[i];
if (-1 === arg.indexOf('=')) {
argsParsed[decodeURIComponent(arg).trim()] = true;
}
else {
kvp = arg.split('=');
key = decodeURIComponent(kvp[0]).trim();
value = decodeURIComponent(kvp[1]).trim();
argsParsed[key] = value;
}
}
return argsParsed;
}
function run() {
var countdown = $(".js-countdown").countdown360({
radius: 30,
seconds: 30,
fontColor: '#000',
autostart: false,
onComplete: function() {
console.log('done');
run();
}
});
// TODO change to token start time, regardless of the time the app began
countdown.start(new Date());
console.log('countdown360 ', countdown);
var otpauth = parseQuery(document.location.search).otpuri;
var otplink = document.createElement('a');
var otp;
var meta;
var issuer;
var accountName;
otplink.href = otpauth;
var idx = otplink.href.indexOf('?');
var queryString = (idx >= 0 ? otplink.href.slice(idx) : '');
otp = parseQuery(queryString);
meta = otplink.pathname.replace(/.*\/totp\//, '').split(':');
// TODO throw if otp.issuer !== decodeURI(meta[0])
if (meta.length > 1) {
// TODO why is there an extra leading '/' on iOS webview?
issuer = otp.issuer || decodeURI(meta[0]).replace(/^\//, '');
accountName = decodeURI(meta[1]);
}
else {
issuer = otp.issuer;
accountName = decodeURI(meta[0]);
}
$('.js-issuer').text(issuer);
$('.js-account-name').text(accountName);
Authenticator.generateToken(otp.secret).then(function (token) {
$('.js-token').text(token.replace(/(\d{3})/g, '$1 ').trim());
});
}
run();
}(window));

212
demo/style.css Normal file
View File

@ -0,0 +1,212 @@
.github-banner {
position: absolute;
top: 0;
right: 0;
border: 0;
}
.col-xs-6 {
text-align: center;
}
.authenticator {
z-index: 2;
font-size: 32px;
line-height: 1.0556;
font-weight: 100;
letter-spacing: .023em;
text-align: center;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
}
.left {
margin-top: 170px;
padding: 0 0 0 15%;
}
.qrcode {
z-index: 2;
}
.scan {
z-index: 2;
font-size: 13px;
line-height: 1.0556;
font-weight: 200;
letter-spacing: .023em;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
}
.verify {
z-index: 2;
font-size: 13px;
line-height: 1.0556;
font-weight: 200;
letter-spacing: .023em;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
margin-top: 20px;
}
.token-label {
font-size: 14px;
line-height: 1.0556;
font-weight: 200;
letter-spacing: .023em;
text-align: center;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
margin-left: -100px;
}
input {
margin: 10px 10px 5px 0;
width: 165px;
padding: 5px 20px;
border-radius: 10px;
font-size: 16px;
line-height: 1.0556;
font-weight: 200;
letter-spacing: .023em;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
}
.btn {
background: -webkit-linear-gradient(#6D99ED, #7464ED);
color: white;
border-radius: 5px;
font-size: 18px;
border: none;
padding: 5px 20px;
vertical-align: inherit;
}
.btn:hover {
color: #fff;
}
.right {
margin-top:265px;
position: relative;
}
.iPhone {
max-width: 400px;
position: absolute;
top: -250px;
left: 17%;
}
.iframe {
width: 265px;
height: 450px;
border: none !important;
z-index: 2;
}
.logo {
background-image: url("logo.svg");
background-repeat: no-repeat;
background-size: 75px 75px;
width: 75px;
height: 75px;
margin: 25px auto 0 auto;
line-height: 1.0556;
text-align: center;
z-index: 2;
}
.uppercase {
text-transform: uppercase;
}
.token-issuer {
margin-top: 25px;
z-index: 2;
font-size: 12px;
line-height: 1.0556;
font-weight: 200;
letter-spacing: .023em;
text-align: center;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
}
.account-name {
margin-top: 80px;
z-index: 2;
font-size: 18px;
line-height: 1.0556;
font-weight: 200;
letter-spacing: .023em;
text-align: center;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
margin-bottom: 10px;
}
.token-phone {
margin-top: 20px;
z-index: 2;
font-size: 48px;
line-height: 1.0556;
font-weight: 100;
letter-spacing: .043em;
text-align: center;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
}
.countdown {
text-align: center;
margin-top: 40px;
}
.bottom {
margin-top: 220px;
}
.wide {
width: 340px;
}
.console {
z-index: 2;
text-align: center;
font-size: 32px;
line-height: 1.0556;
font-weight: 100;
letter-spacing: .023em;
font-style: normal;
font-family: "Myriad Set Pro","Helvetica Neue","Helvetica","Arial",sans-serif;
}
hr {
border: 0;
height: 1px;
background: #333;
background-image: linear-gradient(to right, #ccc, #333, #ccc);
}
.iframe-container {
max-width: 400px;
position: absolute;
top: -120px;
left: 17%;
width: 400px;
height: 600px;
}
.fade {
opacity: 0;
}
.in {
opacity: 1;
transition: opacity .5s ease-in-out;
-moz-transition: opacity .5s ease-in-out;
-webkit-transition: opacity .5s ease-in-out;
}

1
examples/README.md Normal file
View File

@ -0,0 +1 @@
See index.html / demo/demo.js and demo/phone.html / demo/phone.js

108
index.html Normal file
View File

@ -0,0 +1,108 @@
<!DOCTYPE html>
<html>
<head>
<title>Authenticator</title>
<meta charset="UTF-8">
<!--meta name="viewport" content="width=device-width, user-scalable=no" /-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' https://camo.githubusercontent.com/ https://chart.googleapis.com/ data:; child-src 'self'; object-src 'none'">
<link rel="stylesheet" type="text/css" href="demo/bootstrap-v3.3.5.min.css">
<link rel="stylesheet" type="text/css" href="demo/style.css">
</head>
<body class="fade">
<div class="container-fluid">
<div class="row">
<div class="col-xs-6 left">
<h1 class="authenticator">Authenticator Test</h1>
<img alt="qrcode" class="js-qrcode qrcode" width="166" height="166" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" />
<div class="scan">
Scan with <a href="https://www.authy.com/personal/mobile/" target="_blank">Authy App</a>
</div>
<div class="verify">
<label class="token-label">Enter Verification Token:</label>
<br />
<input class="js-token token-input" type="text" placeholder="i.e. 123 456" />
<button class="js-verify btn">Verify</button>
</div>
</div>
<div class="col-xs-6 right">
<img class="iPhone" src="demo/iPhoneMockup.png" />
<div class="iframe-container">
<iframe class="js-otp-iframe iframe" src="demo/phone.html"></iframe>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-md-3"></div>
<div class="col-md-6 bottom">
<form class="form-horizontal">
<h1 class="console"> Console </h1>
<hr />
<div class="form-group">
<label class="col-sm-3 control-label">Issuer:</label>
<div class="col-sm-9">
<input type="text" class="js-company-name issuer-input wide" placeholder="Company Name">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Account:</label>
<div class="col-sm-9">
<input type="text" class="js-user-account issuer-input wide" placeholder="User Account">
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Key:</label>
<div class="col-sm-9">
<input type="text" class="js-key base-key-input wide">
</div>
</div>
<!-- TODO add options to interface -->
<input type="hidden" class="js-totp-algo" value="SHA1">
<input type="hidden" class="js-totp-digits" value="6">
<input type="hidden" class="js-totp-period" value="30">
<div class="form-group">
<div class="col-sm-offset-6 col-xs-6">
<button type="button" class="btn btn-default js-generate regenerate">Regenerate</button>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">URI:</label>
<div class="col-sm-9">
<a class="js-otpauth">otpauth://totp/company:user?secret=xxxx&amp;issuer=company</a>
</div>
</div>
</form>
</div>
<div class="col-md-3"></div>
</div>
</div>
<!-- github banner -->
<a href="https://git.coolaj86.com/coolaj86/browser-authenticator.js"><img class="github-banner" src="https://camo.githubusercontent.com/a6677b08c955af8400f44c6298f40e7d19cc5b2d/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f677261795f3664366436642e706e67" alt="Fork me with Git" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png"></a>
<!-- extremely lightweight shim for hex conversion -->
<script src="demo/bower_components/unibabel/index.js"></script>
<script src="demo/bower_components/unibabel/unibabel.hex.js"></script>
<!-- base32 conversion (and binary string for forge) (works standalone from the above) -->
<script src="demo/bower_components/unibabel/unibabel.base32.js"></script>
<!-- forge.* -->
<!--[if lt IE 11]>
<script src="demo/bower_components/forge/dist/forge.min.js"></script>
<![endif]-->
<!-- botp.totp -->
<script src="demo/bower_components/botp/sha1-hmac.js"></script>
<script src="demo/bower_components/botp/index.js"></script>
<!-- Authenticator -->
<script src="authenticator.js"></script>
<script src="demo/demo.js"></script>
</body>
</html>

30
package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "browser-authenticator",
"version": "1.0.8",
"description": "Two- / Multi- Factor Authenication (2FA / MFA / OTP) for browser JavaScript",
"main": "authenticator.js",
"homepage": "https://git.coolaj86.com/coolaj86/browser-authenticator.js",
"directories": {
"example": "examples"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.coolaj86.com/coolaj86/browser-authenticator.js"
},
"keywords": [
"botp",
"notp",
"otp",
"totp",
"hotp",
"authenticator",
"authy",
"mfa",
"2fa"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "(MIT OR Apache-2.0)"
}

View File

@ -1,68 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title>BOTP Test</title>
<meta charset="UTF-8">
<meta http-equiv="refresh" content="0;https://authenticator.ppl.family/">
</head>
<body>
<h1>Authenticator Test</h1>
<div>
Test with the <a href="https://www.authy.com/personal/mobile/" target="_blank">Authy App</a>.
</div>
<br />
<div>
<img alt="qrcode" class="js-qrcode" width="166" height="166" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" />
<p>20-byte (160-bit) Base32 Key:</p>
<h3 class="js-key">xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx</h3>
</div>
<br />
<div>
<label>Company Name:</label>
<input class="js-company-name" type="text" placeholder="Company Name" />
<label>User Account:</label>
<input class="js-user-account" type="text" placeholder="User Account" />
<button class="js-generate">Regenerate</button>
</div>
<br />
<div>
<label>Verification Token:</label>
<input class="js-token" type="text" placeholder="ex: 000 000" />
<button class="js-verify">Verify</button>
</div>
<!-- these two are only needed for webcrypto shim -->
<script src="bower_components/unibabel/index.js"></script>
<script src="bower_components/unibabel/unibabel.hex.js"></script>
<!-- base32 conversion (and binary string for forge) (works standalone from the above) -->
<script src="bower_components/unibabel/unibabel.base32.js"></script>
<!-- forge.hmac -->
<script src="bower_components/forge/js/util.js"></script>
<script src="bower_components/forge/js/sha1.js"></script>
<script src="bower_components/forge/js/hmac.js"></script>
<!-- forge.random.getBytes -->
<script src="bower_components/forge/js/sha256.js"></script>
<script src="bower_components/forge/js/cipher.js"></script>
<script src="bower_components/forge/js/cipherModes.js"></script>
<script src="bower_components/forge/js/aes.js"></script>
<script src="bower_components/forge/js/prng.js"></script>
<script src="bower_components/forge/js/random.js"></script>
<!-- botp.totp -->
<script src="bower_components/botp/sha1-hmac.js"></script>
<script src="bower_components/botp/index.js"></script>
<!-- Authenticator -->
<script src="authenticator.js"></script>
<script src="test.js"></script>
</body>
Redirecting...
</body>
</html>

71
test.js
View File

@ -1,71 +0,0 @@
// forgive the suckiness, but whatever
(function (exports) {
'use strict';
var key;
var Authenticator = exports.Authenticator;
var $ = function (x) {
return document.querySelector(x);
};
function generate(ke) {
Authenticator.generateKey().then(function (k) {
key = ke || k;
var companyName = $('.js-company-name').value;
var userAccount = $('.js-user-account').value;
// obviously don't use this in production, but it's not so bad for the demo
var src = 'https://www.google.com/chart?chs=166x166&chld=L|0&cht=qr&chl='
+ encodeURIComponent(
'otpauth://totp/'
+ encodeURIComponent(companyName)
+ ':'
+ encodeURIComponent(userAccount)
+ '?secret='
+ key.replace(/\s+/g, '').toUpperCase()
);
$('.js-key').innerHTML = key; // safe to inject because I created it
$('img.js-qrcode').src = src;
Authenticator.generateToken(key).then(function (token) {
console.log('token', token);
Authenticator.verifyToken(key, token).then(function (result) {
console.log('verify', result);
Authenticator.verifyToken(key, '000 000').then(function (result) {
console.log('reject', result);
});
});
});
});
}
$('.js-verify').addEventListener('click', function () {
var token = $('.js-token').value;
Authenticator.verifyToken(key, token).then(function (result) {
var msg;
if (result) {
msg = 'Correct!';
} else {
msg = 'FAIL!';
}
window.alert(msg);
});
});
$('.js-generate').addEventListener('click', function () {
generate(null);
});
$('.js-company-name').value = 'ACME Co';
$('.js-user-account').value = 'john@example.com';
generate('hxdm vjec jjws rb3h wizr 4ifu gftm xboz');
}(
'undefined' !== typeof window ? window : module.exports
));