Skip to content
This repository has been archived by the owner on Feb 25, 2019. It is now read-only.

Commit

Permalink
Use WebCrypto API
Browse files Browse the repository at this point in the history
This is a larger change which combines the work made overs some time.
In addition to switch the code to use the webcrypto API other
changes were made.
The following is a summary of these changes:

* Uses ES6 as source language which also replaces coffescript for tests.

* Currently the intent is to publish this under npm once it was reviewed
  and accepted into the mainline.

* Version 0.2.0 was introduced to indicate that the former API was
  broken. In particular all functions returning promises are now
  namespaced under Anvil.promise.

* A fallback mechanism is available to allow using crypto libraries
  when WebCrypto is not available or for testing on localhost.

* Claims are now checked

* Chrome replaces PhantomJS as karma test suite browser as PhantomJS does not
  support WebCrypto.

* Uses jspm for development and tests.

* Refactored duplicated code that was initially copied to support both
  angular.js and vanilla js. Differences in the code are now provider by
  the client setting adapter interfaces for HTTP, browser location and DOM
  access.

* Removed bower dependency.

* Removed production dependency of angular js and jquery.

* Removed dependencies on previously used crypto libraries:
- CryptoJS
- KJUR.jws.JWS
- sjcl

* New dependencies:
- jspm
Libs:
- tiny-emitter for events
- bows for logging (see commit messages below)
- text-encoder-lite (encode/decode str as utf-8, see below)
DEV:
- Q for promises (dev),
- q-xhr for HTTP access without jquery (dev)
- webcrypto-shim to support Safari and IE. Used via script tag.

The original work on this change was done over many commits which were
later combined. The following lists the  original headline and comments
for some of these commits. Although some of these might be outdated it
should mostly be useful to get further some details about has been
summarized above.

Significant original commits:

COMMIT: Add plain javascript version

This brings in the following new dependencies:

o Q for promises (https://github.com/kriskowal/q).
Bluebird seemed too large to me for the browser. Q seems very focused.
Angulars' $q service is inspired by Q albeit not the same.

o q-xhr for http requests (https://github.com/nathanboktae/q-xhr).
This is a backport from angular's $http service. It is quite
lightweight.

COMMIT: Handle disconnected popup in passwordless login.

The passwordless login method sends the user a link in an email. When
the
user follows this link a new browser window opens.

However the original popup window will not redirect and remains open
with a page allowing to resend the link.

This change has the popups parent window listens whether an
authentication is established. For this an 'authenticated' event
was introduced. The 'authenticated' event is emitted in response to a
local storage 'anvil.connect' event when the user is authenticated.
This reacts to an authentiation performed in another window or tab.

For the eventing a new dependency was added to `tiny-emitter`.
Here are a few alternative event emitters:
- https://github.com/Olical/EventEmitter
seems very elaborate, a bit too much weight for our scenario
- https://github.com/component/emitter
a similar alternative to tiny-emitter. More or less a coin-toss.
- https://github.com/jeromeetienne/microevent.js
seemed too microi (perhaps)!
- https://github.com/joaquimserafim/tiny-eventemitter.
not too popular. Might be an alternative too.

Presumably this could also be handled with the server. In this case the
'Sent mail' page would have to monitor whether server whether the
session is established. However one would want to avoid publishing the
email link to that window as one could easily steal it and allowing
anyone to login. Perhaps divulging the secret could be avoided similar
to
how OpenID Connect session management works.

Another disadvantage would be that the server would have to be either
polled or sending server events. Again it might be possible to do this
with the existing session events.

An advantage would be that this would allow the normal OpenID connect
authentication flow to happen.

One issue with this solution is that the page opened by the emailed link
activation may not self close as some browser may not allow this from a
script in this case. I did see this work in Chrome and Safari but not
Firefox.

COMMIT: Add support for better logging.

During development for sure there is some need to log how the client
interacts with connect.

While one might want a capability to submit client errors to a server,
this is not what this is for.

Instead this allows to have essentially console statement which are can
be enabled optionally by setting

`localStorage.debug = true`

in the browser dev tools console. To stop logging use

`delete localStorage.debug`

The logging library used is https://github.com/latentflip/bows.

I have read about https://github.com/visionmedia/debug which seems more
popular but it was mentioned that it does not link to the code in its
messages. This is not helpful in my opinion.

I played around a bit with bows and it pretty good to me.

COMMIT: Anvil.nonce and Anvil.sha256url to use web crypto.

Both Anvil.nonce and Anvil.sha256url now return promises.

Unfortunately this means that all consumers will have to adjust.
However apparently there is no real way around this fact.

This required implementing the following conversion or encodings for
ArrayBuffers:
1. encode/decode str as utf-8
2. convert buffer to base64url string
3. in the context of seriale base64 conversion to/from buffer
was needed.

Implementing utf-8 support should be relatively straightforward with
ES6. However I decided to use the npm:text-encoder-lite library instead
which is mentioned by MDN (https://developer.mozilla.org). It appears
that at the moment there is not yet widespread support in the browsers
for TextEncode/TextDecode and so this seemed like a pretty good
approach. However it might also be on option to piggy back on node
packages when using browserify or jspm. However I am not sure this is
desirable.

To install this library I had to shim it this should perhaps be
submitted to the jspm registry. See package.json for the override.

COMMIT: Anvil to use webcrypto except for JWT validation.

Methods changed were:
- callback
- authorize

Also started using ES6 promises and got rid of apiDefer which changes
methods:
- request

This also affected the JWT validators.

COMMIT: add verifyJWT function

There is an issue with the specific jwk used by our tests previously.
Apparently it is not fully compliant and Chrome rejects it.
Currently Firefox is more leniant here, but the Chrome folks aim to
push
Firefox to also implement stronger compliance here.

See the following related issues:
- OADA/rsa-pem-to-jwk#1
- https://code.google.com/p/chromium/issues/detail?id=383998

The key has been fixed by:
1. Decoding its n value to a hex string
"009e1b9b22bf7cba0430...e33a63"
2. Trim off the leading 00 byte:
"9e1b9b22bf7cba0430...e33a63"
3. Compute the base64url representation of this usingi
ab2base64urlstr(hex2ab(<step2-value>))
resulting in value:
"nhu...M6Yw"

COMMIT: callback to use webcrypto

remove crypto code using KJUR.jws.JWS

COMMIT: switch to window.crypto.getRandomValues(bytes)

COMMIT: Added webcrypto-shim to support Safari/IE?

Tested that this works with Safari 9.0.2 on OS-X. That is all
tests pass except the test which generates our own key and signs a
token. Note however that this capability is not actually needed to
consume tokens.

COMMIT: remove dependencies sjcl and jquery

jquery is no longer provided here as the plain js should do
as well. However currently only angular is tested.

Removes:
- "capaj/jspm-hot-reloader"
- "jquery": "github:components/jquery@2.1.4",
- "kriskowal/q": "github:kriskowal/q@2.0.2",
q is still used through "q": "npm:q@1.4.1",
- "sjcl": "npm:sjcl@1.0.3",

COMMIT: Fix test isolation issue.

Failing test of:
describe('Check jwk sign verification', () => {
describe('self generated key', () => {
...
beforeEach(done => {

On OS X the signing fails and this casused
failure in the beforeEach done to be called twice.

While investigating change console.log to bows.

Split off extended test useful to gen tokens

The extended tests are not required for verifying jws tokens.
However in case one needs to generate new tokens the code can
be useful.
To observe the bows log statement one must set debug=true in
localStorage.

The extended tests can be run as follows:
1. npm run karma-browsers-extended
2. npm run karma-run

Currently the extended test is known to fail on [Safari 9.0.2
(Mac OS X
10.10.5)] and passes on
- [Firefox 43.0.0 (Mac OS X 10.10.0)]
- [Chrome 47.0.2526 (Mac OS X 10.10.5)]

COMMIT: Make Chrome browser for 'npm test'

webcrypto is not available on PhantomJS so it does not make
sense to
target it at the moment.
This may change when it is available.

COMMIT: remove one describe level in jwk tests

COMMIT: Work around MS Edge not taken use field well.

This could be an Edge bug. If so this should probably be
removed in the future.

See
https://connect.microsoft.com/IE/feedbackdetail/view/2242108/webcryptoapi-importing-jwk-with-use-field-fails

COMMIT: directories.lib src was needed for jspm17@beta

COMMIT: Use webcrypto-shim outside of npm.

webcrypto-shim currently is neither published to npm nor has a
release on github.
For now we are using it outside of package manage that is with
a script tag.

COMMIT: Refactor have module provide crypto ops needed.

This introduces an API implemented currently only by
encryptor-webcrypto which
is intended to support providing an alternative implementation
for example
based on the previous libraries used.

The mechanism on how to tell connect-js an alternative
implementation has not
yet been defined.

COMMIT: Improve jwsvalidator api and introduce fallback mechanism.

Fallback mechanism is in fallbacks.js. Fallbacks are only
used if subtle.crypto is not available.
For testing one can force fallbacks to be used if the origin
is at http://localhost

COMMIT: Now all promise functions are available under Anvil.promise.

Also upgraded version to 0.2.0 to indicate a breaking
change.

Update README.md except for usage section and remove stale
bower.json.

COMMIT: Update travis.ml and karma.conf.js

travis has not been tested yet. Presumably would happen once
a pull request is done

COMMIT: Add checking of claims
  • Loading branch information
henrjk committed Mar 26, 2016
1 parent 631a6fa commit f0f062f
Show file tree
Hide file tree
Showing 41 changed files with 3,693 additions and 1,705 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
3 changes: 3 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
config.js
lib/**
jspm_packages/**
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
node_modules
bower_components
jspm_packages
lib
bundle
bundlesfx
npm-debug.log
7 changes: 7 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle/
bundlesfx/
node_modules/
jspm_packages/
src/
test/
test.extended/
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ addons:

before_install:
- npm install -g npm
- npm install -g bower
- npm install -g node-gyp
- node-gyp clean
- export CHROME_BIN=chromium-browser
Expand All @@ -27,7 +26,7 @@ install:
- export CXX=g++-4.8
- $CXX --version
- npm install
- bower install
- jspm install

script:
- npm run test-all-travis
Expand Down
33 changes: 32 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@
'use strict'
module.exports = function (grunt) {
grunt.initConfig({

watch: {
esm: {
files: ['src/{,*/}*.js'],
tasks: ['babel']
}
},
babel: {
compile: {
options: {
sourceMap: true,
presets: ['es2015']
},
files: [{
expand: true,
cwd: 'src/',
src: ['**/*.js'],
dest: 'lib'
}]
}
},
clean: {
dist: ['lib/*.js', 'lib/*.map']
}
})
grunt.loadNpmTasks('grunt-contrib-clean')
grunt.loadNpmTasks('grunt-release')
grunt.initConfig({})
grunt.loadNpmTasks('grunt-babel')
grunt.loadNpmTasks('grunt-contrib-watch')

grunt.registerTask('default', ['clean', 'babel'])
}
182 changes: 152 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@

## Install

once this would be published on npm:
```bash
$ bower install anvil-connect --save
$ npm install anvil-connect-js
$ npm install
$ jspm install
```

Verify that tests pass:
```bash
$ npm run test
```

### API Documentation

#### Anvil.configure(options)
#### Initialization and provider configuration

##### Anvil.configure(options)
<!--
lorem ipsum dolor amit
Expand All @@ -25,39 +35,152 @@ lorem ipsum dolor amit
// ...
```
-->
#### Anvil.toFormUrlEncoded(obj)
#### Anvil.parseFormUrlEncoded(str)
#### Anvil.getUrlFragment(url)
#### Anvil.popup(popupWidth, popupHeight)
#### Anvil.session
#### Anvil.serialize()
#### Anvil.deserialize()
#### Anvil.reset()
#### Anvil.uri()
#### Anvil.nonce()
#### Anvil.sha256url()
#### Anvil.headers()
#### Anvil.request()
#### Anvil.userInfo()
#### Anvil.callback(response)
#### Anvil.authorize()
#### Anvil.signout(path)
#### Anvil.destination(path)
#### Anvil.checkSession(id)
#### Anvil.updateSession(event)
#### Anvil.isAuthenticated()
#### Anvil.getKeys()

##### Anvil.init(providerOptions, apis)
since 0.2.0

providerOptions same as for Anvil.configure().

Examples:

* src/anvil-connect-angular.js
* src/anvil-connect-plain.js

#### Anvil.promise.prepareAuthorization
since 0.2.0

Does initializations which may require network calls.
Returns a promise.

#### Main API methods

##### Anvil.session

Current session object.

##### Emits 'authenticated' event
since 0.2.0
This uses TinyEmitter so that on can use the corresponding
method on the Anvil instance.

Example:
```JavaScript
Anvil.once('authenticated', function (session) {
// do something like this:
log('authenticated', session)
})
```

##### Anvil.isAuthenticated()

This method returns truthy if the user's id token has been established in
the session.


##### Anvil.toFormUrlEncoded(obj)
##### Anvil.parseFormUrlEncoded(str)
##### Anvil.getUrlFragment(url)
##### Anvil.promise.deserialize()
since 0.2.0

Establishes session based on localStorage and/or cookies.

Returns a promise

##### Anvil.promise.authorize()
since 0.2.0: was promise before but is no longer available under Anvil.authorize()

##### Anvil.promise.callback(response)
since 0.2.0: was promise before but is no longer available under Anvil.callback()

##### Anvil.promise.uri()
since 0.2.0

Can be used to connect the connect server.

Returns a promise

Example:
```JavaScript
Anvil.promise.uri('authorize', {
prompt: 'none',
id_token_hint: Anvil.session.id_token}).then( function (uri) {
window.location = uri
})
```

##### Anvil.signout(path)

Signs out with the connect server.

This redirects the current page to the signout endpoint.
The server is expected to redirect the browser to the path.
The (destination) path is stored in localStorage and can be retrieved with
`Anvil.destination()`.
The functions also calls Anvil#reset

Example:
```JavaScript
Anvil.signout('/')
```
##### Anvil.reset()

Clears browser session state in localStorage, cookies and Anvil object.

##### Anvil.destination(path)

Gets/set/deletes Anvil destination path in localStorage.

Examples:
```JavaScript
// Set the destination
Anvil.destination('/')

// Get the destination
Anvil.destination()

// Clear the destination
Anvil.destination(false)
```

### Support for session protocol
##### Anvil.checkSession(id)
##### Anvil.updateSession(event)

#### Internal API.
The internal API is published mostly to support unit testing.

It may be changed at any time.
##### Anvil.popup(popupWidth, popupHeight)
##### Anvil.promise.serialize()
since 0.2.0 this is a promise
##### Anvil.promise.nonce()
since 0.2.0 this is a promise
##### Anvil.promise.sha256url()
since 0.2.0 this is a promise
##### Anvil.headers()
##### Anvil.promise.request()
since 0.2.0: was promise before but is no longer available under Anvil.request()
##### Anvil.promise.userInfo()
since 0.2.0: was promise before but is no longer available under Anvil.userInfo()

### AngularJS Usage

Be sure to [register your app as a client](https://github.com/anvilresearch/connect-docs/blob/master/clients.md#registration) with your Anvil Connect provider to obtain credentials.
**NOTE**: The information below applies to master and is mostly stale.

It is suggested to look at https://github.com/henrjk/connect-example-angularjs/
for the webcrypto supporting version.

A main difference is that the new example uses npm and browserify. Of course
you may adapt and use different tooling.

Note that the latest sources are in ES2015 (ES6).

Be sure to [register your app as a client](https://github.com/anvilresearch/connect-docs/blob/master/clients.md#registration) with your Anvil Connect provider to obtain credentials.

#### Authenticate with a popup window

First copy `callback.html` from this repository into your public assets, and add `anvil-connect.angular.js` to your `index.html` file.
First copy `callback.html` from this repository into your public assets, and add `anvil-connect-angular.js` to your `index.html` file.

```html
<script src="bower_components/angular/angular.js"></script>
Expand Down Expand Up @@ -89,12 +212,12 @@ angular.module('App', ['...', 'anvil'])
})
```

You can inject the Anvil service into your controllers and call `Anvil.authorize()` wherever you want to initiate an OpenID Connect authentication flow.
You can inject the Anvil service into your controllers and call `Anvil.promise.authorize()` wherever you want to initiate an OpenID Connect authentication flow.

```javascript
.controller(function ($scope, ..., Anvil) {
$scope.signin = function () {
Anvil.authorize();
Anvil.promise.authorize();
};
})
```
Expand Down Expand Up @@ -126,7 +249,7 @@ angular.module('App', ['...', 'anvil'])
.when('/callback', {
resolve: {
session: function ($location, Anvil) {
Anvil.authorize().then(
Anvil.promise.authorize().then(
function (response) {
$location.url('/');
},
Expand All @@ -142,4 +265,3 @@ angular.module('App', ['...', 'anvil'])

})
```

Loading

0 comments on commit f0f062f

Please sign in to comment.