diff --git a/.bowerrc b/.bowerrc deleted file mode 100644 index 69fad35..0000000 --- a/.bowerrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "directory": "bower_components" -} diff --git a/.gitignore b/.gitignore index 6c90f96..3a057c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -node_modules +app.browser +app.config +bower_components dist +keys +node_modules .tmp .sass-cache authconf.json -bower_components +nvl.log \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 26f185f..7a2bc6b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -16,11 +16,11 @@ module.exports = function (grunt) { require('time-grunt')(grunt); var modRewrite = require('connect-modrewrite'); + var serveStatic = require('serve-static'); // Configurable paths for the application var appConfig = { - app: require('./bower.json').appPath || 'app', - dist: 'dist' + app: 'app', }; var authConfig = @@ -42,18 +42,19 @@ module.exports = function (grunt) { auth_config_data: authConfigData, // Watches files for changes and runs tasks based on the changed files + // to rebundle browserify bundle use watchify of npm... watch: { livereload: { options: { livereload: '<%= connect.options.livereload %>' }, files: [ - '<%= yeoman.app %>/{,*/}*.html', - '<%= yeoman.app %>/{,*/}*.js', - '.tmp/styles/{,*/}*.css', - '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' + 'app/{,*/}*.html', + 'app/styles/{,*/}*.css', + 'app/scripts/{,*/}*.js', + 'app.config/{,*/}*.js' ], - tasks: ['newer:copy'] + tasks: ['newer:copy:browser'] } }, @@ -61,39 +62,65 @@ module.exports = function (grunt) { connect: { options: { port: "<%= auth_config_data.APP_PORT %>", // may need to convert to number? + livereload: 35729, // Change this to '0.0.0.0' to access the server from outside. hostname: '<%= auth_config_data.APP_HOST %>', - livereload: 35729 + open: false, + base: [ + 'app.config', + 'app.browser', + 'app', + ], + middleware: function (connect, options) { + var middlewares = []; + middlewares.push(connect().use( + '/node_modules', + serveStatic('./node_modules') + )); + middlewares.push(modRewrite(['^[^\\.]*$ /index.html [L]'])); + middlewares.push(connect().use( + '/bower_components', + serveStatic('./bower_components') + )); + options.base.forEach(function (base) { + return middlewares.push(serveStatic(base)); + }); + return middlewares; + }, }, livereload: { options: { open: true, + } + }, + dist: { + options: { + livereload: false, base: [ - '.tmp', - 'dist/app' + 'dist' ], - middleware: function (connect, options) { - var middlewares = []; - middlewares.push(modRewrite(['^[^\\.]*$ /index.html [L]'])); - middlewares.push(connect().use( - '/bower_components', - connect.static('./bower_components') - )); - options.base.forEach(function (base) { - return middlewares.push(connect['static'](base)); - }); - return middlewares; - } + keepalive: true, + } }, }, // Empties folders to start fresh clean: { - server: { - files: [{ - src: ['.tmp', 'dist'] - }] + config: { + files: [{ + src: ['app.config'] + }] + }, + browser: { + files: [{ + src: ['app.browser'] + }] + }, + dist: { + files: [{ + src: ['dist'] + }] } }, @@ -102,64 +129,183 @@ module.exports = function (grunt) { copy: { styles: { expand: true, - cwd: '<%= yeoman.app %>/styles', - dest: '.tmp/styles/', + cwd: 'app/styles', src: '{,*/}*.css' }, - dist: { - expand: true, - dest: 'dist', - src: [ - 'register_with_anvil_connect.sh', - '<%= yeoman.app %>/**'], + config: { options: { - process: function( content, srcpath) { + process: function (content, srcpath) { return grunt.template.process( content, {data: authConfigData}); }, }, + expand: true, + cwd: 'app.config.templ', + dest: 'app.config', + src: [ + 'anvil-config.js', + 'register_with_anvil_connect.sh' + ] + }, + browser: { + options: { + process: function (content, srcpath) { + return grunt.template.process( + content, + {data: authConfigData}); + }, + }, + files: { + 'app.browser/index.html': ['app/index.html'] + } + }, + distApp: { + expand: true, + cwd: 'app', + dest: 'dist', + src: ['**/*'], + }, + distBrowser: { + expand: true, + cwd: 'app.browser', + dest: 'dist', + src: ['**/*'], }, }, - // Run some tasks in parallel to speed up the build process - concurrent: { - server: [ - 'copy:styles', - 'copy:dist', - ] + browserify: { + options: { + browserifyOptions: { + debug: true + } + }, + browser: { + files: { + 'app.browser/scripts/dev-bundle.js': + ['app/scripts/app.js'], + 'app.browser/scripts/rp-dev-bundle.js': + ['app/scripts/rp.js'], + 'app.browser/scripts/popup-dev-bundle.js': + ['app/scripts/callback_popup.js'] + } + }, + dist: { + options: { + browserifyOptions: { + debug: false + } + }, + files: { + 'dist/scripts/app-bundle.js': ['dist/scripts/app.js'], + 'dist/scripts/rp-bundle.js': ['dist/scripts/rp.js'], + 'dist/scripts/popup-bundle.js': ['dist/scripts/callback_popup.js'] + } + } }, + uglify: { + options: { + mangle: false + }, + dist: { + files: { + 'dist/scripts/app-bundle.min.js': [ + 'node_modules/es5-shim/es5-shim.js', + 'node_modules/json3/lib/json3.min.js', + 'node_modules/promiz/promiz.js', + 'node_modules/webcrypto-shim/webcrypto-shim.js', + 'node_modules/text-encoder-lite/index.js', + 'dist/scripts/app-bundle.js' + ], + 'dist/scripts/rp-bundle.min.js': [ + 'node_modules/es5-shim/es5-shim.js', + 'node_modules/json3/lib/json3.min.js', + 'node_modules/promiz/promiz.js', + 'node_modules/webcrypto-shim/webcrypto-shim.js', + 'node_modules/text-encoder-lite/index.js', + 'dist/scripts/rp-bundle.js' + ], + 'dist/scripts/popup-bundle.min.js': [ + 'node_modules/es5-shim/es5-shim.js', + 'node_modules/json3/lib/json3.min.js', + 'node_modules/promiz/promiz.js', + 'node_modules/webcrypto-shim/webcrypto-shim.js', + 'node_modules/text-encoder-lite/index.js', + 'dist/scripts/popup-bundle.js' + ] + } + } + }, + + processhtml: { + dist: { + files: { + 'dist/index.html': ['app.browser/index.html'], + 'dist/rp.html': ['app.browser/rp.html'], + 'dist/callback_popup.html': ['app.browser/callback_popup.html'] + } + } + } }); - grunt.registerTask('chmodScript', 'Makes script executable', function(target) { + grunt.registerTask('chmodScript', '(internal) Makes script executable. Used by config task', function(target) { var fs = require('fs'); - fs.chmodSync('dist/register_with_anvil_connect.sh', '755'); + fs.chmodSync('app.config/register_with_anvil_connect.sh', '755'); }); - grunt.registerTask('build', function (target) { - grunt.log.writeln('Build app in dist folder, matching auth server configuration in %s', grunt.config('auth_config')); - grunt.log.writeln('If not yet done register client using dist/register_with_anvil_connect.sh. See README.md'); + + grunt.registerTask('build_browser', '(internal) Builds app in app.browser. Used by serve task.', function (target) { + grunt.log.writeln('Build app scripts in app.browser folder, matching auth server configuration in %s', grunt.config('auth_config')); grunt.task.run([ - 'clean', - 'copy:dist', - 'chmodScript', + 'clean:browser', + 'copy:browser', + 'browserify:browser' ]); }); + grunt.registerTask('config', 'Primary task: Generates config in app.config based on authconf.json', function (target) { + grunt.log.writeln('Generating config in app.config folder, matching auth server configuration in %s', grunt.config('auth_config')); + grunt.log.writeln('If not yet done register client using app.config/register_with_anvil_connect.sh. See README.md'); + grunt.task.run([ + 'clean:config', + 'copy:config', + 'chmodScript', + ]); + }); - grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { + grunt.registerTask('serve', 'Primary task: Build then start a connect web server\n (serve for livereload app or serve:dist) ', function (target) { if (target === 'dist') { - return grunt.task.run(['build', 'connect:dist:keepalive']); + return grunt.task.run(['build', 'connect:dist']); } + grunt.log.writeln('Builds app, starts livereload server and opens browser.'); + grunt.log.writeln('NOTE: also start `npm run watchify` to rebuild the browserify bundles on changes.'); + grunt.task.run([ - 'clean:server', - 'concurrent:server', + 'build_browser', 'connect:livereload', 'watch' ]); }); + grunt.registerTask('build', 'Builds app in /dist folder', function (target) { + grunt.log.writeln('** Build app in dist folder, matching auth server configuration in %s', grunt.config('auth_config')); + grunt.task.run([ + 'build_browser', + 'clean:dist', + 'copy:distApp', + 'copy:distBrowser', + 'browserify:dist', + 'uglify:dist', + 'processhtml:dist' + ]); + }); + + grunt.registerTask('serve_dist', 'Starts server on /dist', function (target) { + grunt.task.run([ + 'connect:dist' + ]); + }); }; diff --git a/README.md b/README.md index ec6348d..241f2a5 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,101 @@ This repository demonstrates authenticating users of an AngularJS app using Anvil Connect. ## Prerequisites + +### Setup server: Setup [Anvil Connect authorization server](https://github.com/anvilresearch/connect/blob/master/README.md) -As a result the Anvil Authorization server should run on localhost:3000. +These instructions assume that the Anvil Authorization server (issuer) runs on localhost:3000 in development mode (not using docker). However it should also be adaptable to using docker. + +One must register the application with the issuer. For this the cli is used. The [cli must be setup](https://github.com/anvilresearch/connect-docs/blob/master/cli.md) first so that you can login to the server. + +### Get connect-js client libraries + +There is currently work in progress to update the client libraries to use +webcrypto APIs instead of encryption libraries. See +[Webcrypto API · Issue #7 · anvilresearch/connect-js](https://github.com/anvilresearch/connect-js/issues/7) for more details. + +To get the webcrypto code for testing the fork is used: + +```console +$ # create or got to some suited directory then +$ git clone https://github.com/anvilresearch/connect-js.git +$ cd connect-js +$ git checkout webcrypto-api + +## These steps are described here: https://github.com/anvilresearch/connect-js/tree/webcrypto-api +$ npm install +$ npm run test +``` + +Next get this project and npm link it with the connect-js library installed +in the previous step + +```console +$ # create or got to some suited directory. This could be the same as above. +$ git clone https://github.com/anvilresearch/connect-example-angularjs.git +$ cd connect-example-angularjs +$ git checkout henrjk-use-npm +``` + +Now go to the directory where connect-js was cloned to and issue these commands: +```console +connect-js dev (webcrypto-api)$ npm link + +> anvil-connect-js@0.2.6 postinstall /Users/dev/code/connect-js +> jspm install + + Looking up npm:babel-runtime +... about 40 more lines of output ... +ok Up to date - babel-runtime as npm:babel-runtime@^5.8.24 (5.8.34) +ok Install tree has no forks. + +ok Install complete. + +> anvil-connect-js@0.2.6 prepublish /Users/dev/code/connect-js +> npm run build + + +> anvil-connect-js@0.2.6 build /Users/dev/code/connect-js +> grunt + +Running "clean:dist" (clean) task +>> 24 paths cleaned. + +Running "babel:compile" (babel) task + +Done, without errors. +/Users/dev/.nvm/versions/node/v0.12.6/lib/node_modules/anvil-connect-js -> /Users/dev/code/connect-js + +/connect-js dev (webcrypto-api)$ +``` +Then go back to the `connect-example-angularjs` directory and do the following: + +```console +connect-js dev (webcrypto-api)$ cd ../connect-example-angularjs/ +connect-example-angularjs dev (henrjk-use-npm)$ npm link anvil-connect-js +/Users/dev/code/work/connect-js-fork/connect-example-angularjs/node_modules/anvil-connect-js -> /Users/dev/.nvm/versions/node/v0.12.6/lib/node_modules/anvil-connect-js -> /Users/dev/code/work/connect-js-fork/connect-js + +connect-example-angularjs dev (use-npm)$ npm install +# this takes a few minutes... +``` + +Now you will have to come up with a authconf.json file in the projects +root folder which matches your server. This is described below. These +steps looks still valid but have not been recently tried. +Here is an outline of the steps: + +* Determine correct `authconf.json` file and place it in connect-example-angularjs root folder. +* Generate configuration files based on authconf.json: `grunt config` +* login to the server with `nvl login` +* execute script generated by grunt build: `./app.config/register_with_anvil_connect.sh` +* Observe the assigned client ID and update `authconf.json` accordingly. +* Start the app in dev mode via `grunt serve` + +This should build the browser app configured correctly for the client registration +in folder app.browser, browserify the dependencies and the open a browser +showing the ![Angular example index page](png/home_page_localhost.png) + ## Register the client and generate matching angular sources. To allow the app to connect to the authorization server a client (representing) @@ -22,13 +114,14 @@ To achieve this the configuration information for the client is stored in file You will find that this file is **not** checked in. Instead there are files with similar names which are checked in. These are good starting points for your -client registration. For example they differ -depending on whether boot2docker is involved or not. Also there can be +client registration. They differ +depending on whether there is a distinct docker host which is *not* localhost or +not. Also there are differences on how the authentication is displayed, for example using a popup or a new page. Use one of these as a starting point and copy them to `authconf.json`. For -example when using docker via boot2docker the following is a good starting +example when running using docker via boot2docker the following is a good starting point: ```console @@ -41,39 +134,45 @@ via `grunt serve` then use: cp authconf.dev.localhost.json authconf.json ``` - The Anvil Authentication server recognizes clients by an id which is generated when they are registered. Therefore the starting point is not yet final as the -id in there must be changed. To help with ensure that the client registration +id in there must be changed. To help to ensure that the client registration matches what the generated angular app uses, a registration script is generated -in `dist/register_with_anvil_connect.sh`. +in `app.config/register_with_anvil_connect.sh`. -First generate the script by: +First generate the configuration files based on authconf.json: ```console -grunt build +grunt config ``` -Use the generated `dist/register_with_anvil_connect.sh` as follows in the root directory of your Anvil Connect Authentication -server: +Next login to the cli. ```console -mac:anvil-connect dev$ /Users/dev/code/connect-example-angularjs/dist/register_with_anvil_connect.sh -Registring nv add client { - "client_name": "Angular Example App", - "default_max_age": 36000, - "redirect_uris": [ - "http://localhost:9000/callback_popup.html", - "http://localhost:9000/callback_page.html", - "http://localhost:9000/rp.html"], - "post_logout_redirect_uris": ["http://localhost:9000"], - "trusted": "true" -} +dev$ nvl login +? Select an Anvil Connect instance localhost:3000 (localhost-3000) +Selected issuer localhost:3000 (http://localhost:3000) +Warning: you are communicating over plain text. +? Enter your email example@gmail.com +? Enter your password ************ +You have been successfully logged in to localhost:3000 +``` + +After the nvl login one can use the generated +`dist/register_with_anvil_connect.sh` to register your app with the client. +However you may want to review the script and change argument values for +--name and perhaps --logo-uri. + +To register the client: + +```console +dev$ ./config/register_with_anvil_connect.sh +Registering this client with localhost-3000 Succeeded. Define CLIENT_ID as follows in authconf.json: { ... - "CLIENT_ID" : "d20dc3cf-cdfd-4e14-a070-0cb40bdb5d92", + "CLIENT_ID" : "29dcbf2a-88d6-4038-b9f9-6bf425104b59", ... } ``` @@ -81,53 +180,58 @@ Define CLIENT_ID as follows in authconf.json: The id shown will be unique to your authentication server. Replace the existing `CLIENT_ID` in `authconf.json` with the one you see in your output. +When you build the app the values in authConfig will be used to ensure that the +generated app uses matching configuration values. For js files which +require file app.config/anvil-config.js the config will be incorporated in the +bundles produced by browserify. + ## Run with angular app served by grunt serve -In this scenaria we are using a simple build server via grunt. +In this scenario we are using a simple development server via grunt. + ```console -igelmac:connect-example-angularjs dev$ grunt serve +dev$ grunt serve Using authconf.json Running "serve" task +Builds app, starts livereload server and opens browser. +NOTE: also start `npm run watchify` to rebuild the browserify bundles on changes. -Running "clean:server" (clean) task -Cleaning .tmp...OK -Cleaning dist...OK - -Running "concurrent:server" (concurrent) task - - Using authconf.json +Running "build_browser" task +Build app scripts in app.browser folder, matching auth server configuration in authconf.json - Running "copy:styles" (copy) task - Copied 1 files +Running "clean:browser" (clean) task +>> 1 path cleaned. - Done, without errors. +Running "copy:browser" (copy) task +Copied 1 file +Running "browserify:browser" (browserify) task +>> Bundle app.browser/scripts/popup-dev-bundle.js created. +>> Bundle app.browser/scripts/rp-dev-bundle.js created. +>> Bundle app.browser/scripts/dev-bundle.js created. - Execution Time (2015-07-10 19:49:12 UTC) - loading tasks 20ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 31% - copy:styles 41ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 63% - Total 65ms - - Using authconf.json +Running "connect:livereload" (connect) task +Started connect web server on http://localhost:9000 - Running "copy:dist" (copy) task - Created 5 directories, copied 10 files +Running "watch" task +Waiting... +>> File "app.config/anvil-config.js" changed. +Using authconf.json - Done, without errors. +Running "newer:copy:browser" (newer) task +No newer files to process. +Done, without errors. - Execution Time (2015-07-10 19:49:12 UTC) - loading tasks 18ms ▇▇▇▇▇ 9% - copy:dist 183ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 91% - Total 202ms -Running "connect:livereload" (connect) task -Started connect web server on http://localhost:9000 +Execution Time (2016-02-03 11:38:31 UTC) +loading tasks 12ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 43% +newer:copy:browser 15ms ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 54% +Total 28ms -Running "watch" task -Waiting... +Completed in 2.858s at Wed Feb 03 2016 12:38:31 GMT+0100 (CET) - Waiting... ``` As a result the browser should open the home page like this: @@ -136,10 +240,17 @@ As a result the browser should open the home page like this: When changes are made to the app this should readily refresh the browser. +However for the Browserify bundles to be rebuild on changes you need to also run: +``` +npm run watchify +``` + These pages should look the same as in the docker use case which shows some more pages except that these are running under `http://localhost:9000` instead of `http://boot2docker:9000`. ## Run with angular app served by docker +**Note:** This section was last updated in July 2015. + In this scenario nginx serves the angular app running inside a docker container. If you have not followed the steps in section **Prerequisites** do this first. diff --git a/app.config.templ/anvil-config.js b/app.config.templ/anvil-config.js new file mode 100644 index 0000000..b8a0a86 --- /dev/null +++ b/app.config.templ/anvil-config.js @@ -0,0 +1,7 @@ +module.exports = { + issuer: '<%=AUTH_SERVER%>', + client_id: '<%=CLIENT_ID%>', + app_server: '<%=APP_SERVER%>', + display: '<%=AUTH_DISPLAY%>', + callback: '<%=APP_AUTH_CALLBACK%>' +} \ No newline at end of file diff --git a/app.config.templ/register_with_anvil_connect.sh b/app.config.templ/register_with_anvil_connect.sh new file mode 100755 index 0000000..a2deac6 --- /dev/null +++ b/app.config.templ/register_with_anvil_connect.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# This is a template which is expanded and stored in dist when building +# the app with grunt. + +# Register client with anvil connect + +fail() { + local message + message=${1-"Failed to register client"} + echo "$message" >&2 + echo "You may need to login with 'nvl login'" >&2 + echo "Perhaps nvl is not yet setup? Consult connect-cli getting started documentation" >&2 + echo "" >&2 + echo "For other problems consult the output of the nvl command in nvl.log" >&2 + exit 2 +} + +echo "Registering this client with <%= ISSUER_NAME %>" +echo "Command output written to nvl.log" + +nvl client:register \ + --issuer "<%= ISSUER_NAME %>" \ + --trusted \ + --name "Angular example with <%= AUTH_DISPLAY %> for <%= APP_SERVER %>" \ + --uri "<%= APP_SERVER %>" \ + --logo-uri "<%= APP_SERVER %>/logo" \ + --application-type "web" \ + --response-type "id_token token" \ + --grant-type "implicit" \ + --default-max-age "3600" \ + --redirect-uri "<%= APP_SERVER %>/<%= APP_AUTH_CALLBACK %>" \ + --redirect-uri "<%= APP_SERVER %>/rp.html" \ + --post-logout-redirect-uri "<%= APP_SERVER %>/" \ + --post-logout-redirect-uri "<%= APP_SERVER %>/" > nvl.log 2>&1 + + +# duplication of --post-logout-redirect-uri see https://github.com/anvilresearch/connect-cli/issues/70 + +REGISTER_STATUS=$? +if [ $REGISTER_STATUS -ne 0 ]; then + fail "nvl client:register failed with status $REGISTER_STATUS" +fi + +# From OS X: +# $ echo "$out" | grep "_id" | grep -o -E '([[:xdigit:]]*-){1,10}[[:xdigit:]]*' +# ec8262ae-28d5-4943-8237-d8145042c3e0 +client_id=$(cat nvl.log | grep "_id" | grep -o -E '([[:xdigit:]]*-){1,10}[[:xdigit:]]*') + +[ -n "$client_id" ] || fail + +echo "Succeeded." +echo "Define CLIENT_ID as follows in authconf.json:" +printf '{\n' +printf '...\n' +printf ' "CLIENT_ID" : "%s",\n' "$client_id" +printf '...\n' +printf '}\n' diff --git a/app/callback_popup.html b/app/callback_popup.html index 2621167..57e4f03 100644 --- a/app/callback_popup.html +++ b/app/callback_popup.html @@ -1,24 +1,29 @@ - + + + + + -

Anvil Connect sign-in successful

- -

Please close this window.

+
+

Anvil Connect signing in...

+
+ + diff --git a/app/index.html b/app/index.html index f9968b6..4716eef 100644 --- a/app/index.html +++ b/app/index.html @@ -5,8 +5,9 @@ - + + @@ -15,7 +16,7 @@ @@ -29,54 +30,21 @@

Your Logo

+ - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ---> diff --git a/app/rp.html b/app/rp.html index 1cffd87..1768b09 100644 --- a/app/rp.html +++ b/app/rp.html @@ -1,106 +1,16 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/app/scripts/anvil-connect-angular.js b/app/scripts/anvil-connect-angular.js new file mode 100644 index 0000000..e296ce4 --- /dev/null +++ b/app/scripts/anvil-connect-angular.js @@ -0,0 +1,114 @@ +'use strict' + +require('angular') +var bows = require('bows') + +var log = bows('ac-angular') + +var Anvil = require('anvil-connect-js').default + +function init (providerOptions, $http, $q, $location, $window, $document) { + Anvil.init(providerOptions, { + http: { + request: function (config) { + return $http(config) + }, + getData: function (response) { + return response.data + } + }, + location: { + hash: function () { + return $location.hash() + }, + path: function () { + return $location.path() + } + }, + dom: { + getWindow: function () { + return $window + }, + getDocument: function () { + return $document[0] + } + } + }) + return Anvil +} + +angular.module('anvil', []) + + .provider('Anvil', function AnvilProvider () { + /** + * Require Authentication + */ + + function requireAuthentication ($location, Anvil) { + if (!Anvil.isAuthenticated()) { + return Anvil.promise.authorize() + .catch(function (err) { + log.debug('requireAuthentication: authorize() failed', err) + return false + }) + } + return Anvil.session + } + + Anvil.requireAuthentication = ['$location', 'Anvil', requireAuthentication] + + /** + * Require Scope + */ + + Anvil.requireScope = function (scope, fail) { + return ['$location', 'Anvil', function requireScope ($location, Anvil) { + if (!Anvil.isAuthenticated()) { + return Anvil.promise.authorize() + .catch(function (err) { + log.debug('requireScope: authorize() failed', err) + return false + }) + } else if (Anvil.session.access_claims.scope.indexOf(scope) === -1) { + $location.path(fail) + return false + } else { + return Anvil.session + } + }] + } + + /** + * Provider configuration + */ + + this.configure = function (options) { + Anvil.configure(options) + } + + /** + * Factory + */ + + Anvil.$get = [ + '$q', + '$http', + '$rootScope', + '$location', + '$document', + '$window', function ($q, $http, $rootScope, $location, $document, $window) { + init(null, $http, $q, $location, $window, $document) + Anvil.on('not-authenticated', function(ev) { + log.debug('not-authenticated event: calling $rootScope.$apply()', ev) + $location.url('/') + $rootScope.$apply() + }) + Anvil.on('authenticated', function(ev) { + log.debug('authenticated event: calling $rootScope.$apply()', ev) + $rootScope.$apply() + }) + return Anvil + }] + + return Anvil + }) diff --git a/app/scripts/app.js b/app/scripts/app.js index 3f9e69d..baeb9f7 100644 --- a/app/scripts/app.js +++ b/app/scripts/app.js @@ -1,4 +1,16 @@ -'use strict'; +'use strict' +require('./anvil-connect-angular') +require('angular-animate') +require('angular-cookies') +require('angular-resource') +require('angular-route') +require('angular-sanitize') +require('angular-touch') +var anvilConfig = require('../../app.config/anvil-config') + +var bows = require('bows') + +var log = bows('app') /** * Anvil Connect AngularJS Example App @@ -6,6 +18,7 @@ angular .module('AnvilConnectClient', [ + 'anvil', 'ngAnimate', 'ngCookies', 'ngResource', @@ -16,16 +29,13 @@ angular ]) .config(function ($locationProvider, $routeProvider, AnvilProvider) { + var auth_callback_route = '/' + anvilConfig.callback; // CONFIGURE ANVIL CONNECT - AnvilProvider.configure({ - issuer: '<%=AUTH_SERVER%>', - client_id: '<%=CLIENT_ID%>', - //redirect_uri: 'http://localhost:9000/callback.html', - redirect_uri: '<%=APP_SERVER%>/callback_<%= AUTH_DISPLAY%>.html', - display: '<%=AUTH_DISPLAY%>', - scope: 'realm email' - }); + AnvilProvider.configure( + angular.merge({ + redirect_uri: anvilConfig.app_server + auth_callback_route + }, anvilConfig)); $locationProvider.html5Mode(true); $locationProvider.hashPrefix = '!'; @@ -48,7 +58,7 @@ angular templateUrl: '/views/requiresScope.html', controller: 'RequiresScopeCtrl', resolve: { - session: AnvilProvider.requireScope('realm', '/unauthorized') + session: AnvilProvider.requireScope('profile', '/unauthorized') } }) @@ -58,16 +68,20 @@ angular }) // HANDLE CALLBACK (REQUIRED BY FULL PAGE NAVIGATION ONLY) - .when('/callback_page.html', { + .when(auth_callback_route, { resolve: { session: function ($location, Anvil) { + log.debug('' + auth_callback_route + '.resolve.session:', $location) if ($location.hash()) { - Anvil.authorize().then( - + return Anvil.promise.authorize().then( // handle successful authorization function (response) { - $location.url(localStorage['anvil.connect.destination'] || '/'); - delete localStorage['anvil.connect.destination'] + var dest = Anvil.destination(false) + // $location.url( dest || '/'); did not react for me + // there may be solutions with scope apply but this seems + // to work fine, although this may not be the best solution. + log.debug('' + auth_callback_route + ' authorize() succeeded, destination=', dest) + $location.url(dest || '/') }, // handle failed authorization @@ -77,8 +91,8 @@ angular ); } else { - $location.url(localStorage['anvil.connect.destination'] || '/'); - delete localStorage['anvil.connect.destination'] + var dest = Anvil.destination(false) + $location.url(dest || '/'); } } } @@ -88,31 +102,58 @@ angular redirectTo: '/' }); }) - .run(function (Anvil) { - Anvil.getKeys().then(function (jwks) { - console.log('Loaded JWKs', jwks) + log.debug('run() entering') + /** + * Reinstate an existing session + */ + Anvil.promise.deserialize().catch( function () { + log.debug('Ignore promise rejection when reinstating session') + }) + Anvil.promise.prepareAuthorization().then(function (result) { + log.debug('prepareAuthorization succeeded:', result) + }, function (err) { + log.error('prepareAuthorization failed:', err) }) }) - .controller('SigninCtrl', function ($scope, Anvil) { + $scope.session = Anvil.session; + + log.debug('SigninCtrl() init: adding Anvil.once("authenticated"..) listener') + Anvil.once('authenticated', function () { + log.debug('SigninCtrl() init: authenticated callback: calling $scope.$apply') + $scope.$apply(); + }) + $scope.signin = function () { - Anvil.authorize() + log.debug('SigninCtrl.signin(): entering function') + Anvil.promise.authorize() + Anvil.once('authenticated', function () { + log.debug('SigninCtrl.signin() authenticated callback: calling $scope.$apply') + $scope.$apply(); + }) }; $scope.signout = function () { Anvil.signout('/'); }; - $scope.$watch(function () { return Anvil.session }, function (newVal) { - $scope.session = newVal; - }); + $scope.$watch( + // proper formatting allows easier setting of breakpoints. + function () { + return Anvil.session + }, + function (newVal) { + $scope.session = newVal + }, + true + ); }) .controller('MainCtrl', function ($scope, Anvil) { - // ... + $scope.session = Anvil.session }) .controller('RequiresAuthenticationCtrl', function ($scope, session) { diff --git a/app/scripts/callback_popup.js b/app/scripts/callback_popup.js new file mode 100644 index 0000000..757f05f --- /dev/null +++ b/app/scripts/callback_popup.js @@ -0,0 +1,70 @@ +'use strict' +var bows = require('bows') +var Q = require('q-xhr')(window.XMLHttpRequest, require('q')) +var Anvil = require('anvil-connect-js').default +var anvilConfig = require('../../app.config/anvil-config') + +function copy(dst, src) { + for (var prop in src) { + if (src.hasOwnProperty(prop)) + dst[prop] = src[prop]; + } + return dst; +} + +window.addEventListener('load', function () { + var log = bows('popup callback') + + Anvil.init( + copy( + { + scope: 'realm' + }, + anvilConfig), + { + http: { + request: function (config) { + return Q.xhr(config) + }, + getData: function (response) { + return response.data + } + } + }); + + function show (id) { + document.getElementById('signing_in').style.display = 'none' + document.getElementById(id).style.display = 'block' + } + + var pageUrl = window.location.href + , opener = window.opener + , pageOrigin + if (opener) { + pageOrigin = opener.location.origin; + } else { + log.debug("No opener for window: ", window.location) + } + + log.debug("load callback") + log.debug("pageUrl=" , pageUrl); + log.debug("pageOrigin=" , pageOrigin); + log.debug("opener=" , opener); + if (opener) { + opener.postMessage(pageUrl, pageOrigin); + } else { + var fragment = Anvil.getUrlFragment(pageUrl) + var response = Anvil.parseFormUrlEncoded(fragment) + Anvil.promise.callback(response).then( + function (result) { + log.info("Anvil.callback succeeded: ", result) + show('signed_in') + window.close(); + }, + function (fault) { + log.info("Anvil.callback failed: ", fault) + show('not_signed_in') + } + ) + } +}); \ No newline at end of file diff --git a/app/scripts/rp.js b/app/scripts/rp.js new file mode 100644 index 0000000..ad578af --- /dev/null +++ b/app/scripts/rp.js @@ -0,0 +1,92 @@ +'use strict' +var bows = require('bows') +var Q = require('q-xhr')(window.XMLHttpRequest, require('q')) +window.Anvil = require('anvil-connect-js').default +// window allows access from setTimeout below. +var anvilConfig = require('../../app.config/anvil-config') + +var log = bows('rp.js') + +function copy(dst, src) { + for (var prop in src) { + if (src.hasOwnProperty(prop)) + dst[prop] = src[prop]; + } + return dst; +} + +Anvil.init(copy({ + redirect_uri: anvilConfig.app_server + '/rp.html', + scope: 'realm' + }, anvilConfig), { + http: { + request: function (config) { + return Q.xhr(config) + }, + getData: function (response) { + return response.data + } + } +}); + +Anvil.promise.deserialize().catch(function (err) { + Anvil.reset() +}); + +var response = (location.hash) ? Anvil.parseFormUrlEncoded(location.hash.substring(1)) : {}; + +if (location.hash) { + log.debug('Loading rp.js: parsed hash=', response) + Anvil.promise.prepareAuthorization().then( function () { + Anvil.promise.callback(response).then( + function success (session) { + log.info('RP CALLBACK SUCCESS', session, Anvil.sessionState); + window.parent.postMessage(location.hash, location.origin); + }, + function failure (fault) { + log.info('RP CALLBACK FAILURE', fault); + log.debug('location', location) + var dest = Anvil.destination() + log.debug('dest=',dest) + }) + }) +} + +// start checking the session every 5 seconds +function setTimer() { + var timer = setInterval("Anvil.checkSession('op')", 5*1000); +} + + +// listen for changes to OP browser state +function receiveMessage(event) { + if (event.origin !== Anvil.issuer) { + log.debug('Houston, we have a problem', event.origin, Anvil.issuer); + return; + } + + if (event.data === 'error') { + log.debug('ERROR FROM OP', event.data); + } + + // SESSION STATE IS THE SAME + if (event.data === 'unchanged') { + ; // do nothing + } + + // SESSION STATE HAS CHANGED + else { + log.info('session state: changed'); + Anvil.promise.uri('authorize', { + prompt: 'none', + id_token_hint: Anvil.session.id_token + }).then( function (uri) { + log.info('RP REAUTHENTICATING', uri) + window.location = uri; + }); + } +} + +window.addEventListener("message", receiveMessage, false); + +setTimer() diff --git a/authconf.dev.b2d.json b/authconf.dev.b2d.json index 5a3db51..bd6db57 100644 --- a/authconf.dev.b2d.json +++ b/authconf.dev.b2d.json @@ -4,6 +4,7 @@ "APP_HOST": "boot2docker", "APP_PORT": "9000", "APP_SERVER" : "<%= APP_PROTO %>://<%= APP_HOST %>:<%= APP_PORT %>", + "ISSUER_NAME" : "localhost-3000", "AUTH_SERVER" : "http://localhost:3000", "AUTH_DISPLAY": "popup", "APP_AUTH_CALLBACK": "callback_<%= AUTH_DISPLAY %>.html" diff --git a/authconf.dev.b2d.page.json b/authconf.dev.b2d.page.json index 5e5e9d0..cbe19c4 100644 --- a/authconf.dev.b2d.page.json +++ b/authconf.dev.b2d.page.json @@ -4,6 +4,7 @@ "APP_HOST": "boot2docker", "APP_PORT": "9000", "APP_SERVER" : "<%= APP_PROTO %>://<%= APP_HOST %>:<%= APP_PORT %>", + "ISSUER_NAME" : "localhost-3000", "AUTH_SERVER" : "http://localhost:3000", "AUTH_DISPLAY": "page", "APP_AUTH_CALLBACK": "callback_<%= AUTH_DISPLAY %>.html" diff --git a/authconf.dev.localhost.json b/authconf.dev.localhost.json index 7ee73bc..9bd9405 100644 --- a/authconf.dev.localhost.json +++ b/authconf.dev.localhost.json @@ -4,6 +4,7 @@ "APP_HOST": "localhost", "APP_PORT": "9000", "APP_SERVER" : "<%= APP_PROTO %>://<%= APP_HOST %>:<%= APP_PORT %>", + "ISSUER_NAME" : "localhost-3000", "AUTH_SERVER" : "http://localhost:3000", "AUTH_DISPLAY": "popup", "APP_AUTH_CALLBACK": "callback_<%= AUTH_DISPLAY %>.html" diff --git a/authconf.dev.localhost.page.json b/authconf.dev.localhost.page.json index d03349c..18115ac 100644 --- a/authconf.dev.localhost.page.json +++ b/authconf.dev.localhost.page.json @@ -4,6 +4,7 @@ "APP_HOST": "localhost", "APP_PORT": "9000", "APP_SERVER" : "<%= APP_PROTO %>://<%= APP_HOST %>:<%= APP_PORT %>", + "ISSUER_NAME" : "localhost-3000", "AUTH_SERVER" : "http://localhost:3000", "AUTH_DISPLAY": "page", "APP_AUTH_CALLBACK": "callback_<%= AUTH_DISPLAY %>.html" diff --git a/bin/replace-client-id.sh b/bin/replace-client-id.sh deleted file mode 100755 index 257eebe..0000000 --- a/bin/replace-client-id.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -echo This is no longer working. Use grunt instead. See README.md -exit - -client_id=$1 -proto=$2 -host=$3 -port=$4 -server_host=$5 - -# sed with in place editing is not portable between OSX and Linux (see http://stackoverflow.com/questions/5694228/sed-in-place-flag-that-works-both-on-mac-bsd-and-linux) -# Changing a file, see pitfall http://mywiki.wooledge.org/BashPitfalls#cat_file_.7C_sed_s.2Ffoo.2Fbar.2F_.3E_file and -# http://unix.stackexchange.com/a/38106 used for a function to chomp. -sponge() { - local file=$1 - local line lines - while IFS= read -r line; do - lines+=( "$line" ) - done - printf '%s\n' "${lines[@]}" > "$file" -} - -sed_app() { - local file=$1 - sed s,APP_PROTO,"$proto",g "$file" | sed s,APP_HOST,"$host",g | sed s,APP_HOST,"$host",g -} - - -if [ "$#" -eq 4 ]; then - sed s,CLIENT_ID,"$client_id",g app/rp.html | sponge app/rp.html - sed s,CLIENT_ID,"$client_id",g app/scripts/app.js | sponge app/scripts/app.js - sed s,APP_HOST,"$host",g app/rp.html | sponge app/rp.html - sed s,APP_HOST,"$host",g app/scripts/app.js | sponge app/scripts/app.js - sed s,APP_PORT,"$port",g app/rp.html | sponge app/rp.html - sed s,APP_PORT,"$port",g app/scripts/app.js | sponge app/scripts/app.js - sed s,APP_HOST,"$host",g Gruntfile.js | sponge Gruntfile.js - sed s,APP_PORT,"$port",g Gruntfile.js | sponge Gruntfile.js - sed s,SERVER_HOST,"$server_host",g app/rp.html | sponge app/rp.html - sed s,SERVER_HOST,"$server_host",g app/scripts/app.js | sponge app/scripts/app.js - sed s,SERVER_HOST,"$server_host",g app/index.html | sponge app/index.html -else - echo Please pass client id, host, port, and server host as arguments. for example: bin/replace-client-id.sh 6d327f50-0aa7-4b0a-9bab-b558d9027e27 my-server.com 9000 https://connect.anvil.io -fi diff --git a/bower.json b/bower.json deleted file mode 100644 index 7a77d67..0000000 --- a/bower.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "anvil-connect-angularjs-example", - "version": "0.0.0", - "dependencies": { - "angular": "1.2.16", - "json3": "~3.3.1", - "es5-shim": "~3.1.0", - "bootstrap": "~3.1.1", - "angular-resource": "1.2.16", - "angular-cookies": "1.2.16", - "angular-sanitize": "1.2.16", - "angular-animate": "1.2.16", - "angular-touch": "1.2.16", - "angular-route": "1.2.16", - "anvil-connect": "https://github.com/christiansmith/anvil-connect-js.git", - "jquery": "~2.1.3" - }, - "appPath": "app" -} diff --git a/package.json b/package.json index ff31924..275bf16 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,55 @@ { "name": "anvil-connect-angularjs-example", "version": "0.0.0", - "dependencies": {}, + "dependencies": { + "angular": "^1.4.8", + "angular-animate": "^1.4.8", + "angular-cookies": "^1.4.8", + "angular-resource": "^1.4.8", + "angular-route": "^1.4.8", + "angular-sanitize": "^1.4.8", + "angular-touch": "^1.4.8", + "anvil-connect-js": "^0.2.5", + "bootstrap": "^3.3.6", + "bows": "^1.4.8", + "es5-shim": "^4.5.2", + "json3": "^3.3.2", + "promiz": "^1.0.5", + "q": "^1.4.1", + "q-xhr": "^1.0.0", + "text-encoder-lite": "^1.0.0", + "webcrypto-shim": "henrjk/webcrypto-shim#add-package-json" + }, "devDependencies": { + "browserify": "^13.0.0", "connect-modrewrite": "^0.7.6", "grunt": "^0.4.1", - "grunt-concurrent": "^0.5.0", - "grunt-contrib-clean": "^0.5.0", - "grunt-contrib-concat": "^0.4.0", - "grunt-contrib-connect": "^0.7.1", - "grunt-contrib-copy": "^0.5.0", - "grunt-contrib-cssmin": "^0.9.0", + "grunt-browserify": "^4.0.1", + "grunt-contrib-clean": "^0.7.0", + "grunt-contrib-connect": "^0.11.2", + "grunt-contrib-copy": "^0.8.2", + "grunt-contrib-uglify": "^0.11.0", "grunt-contrib-watch": "^0.6.1", - "grunt-newer": "^0.7.0", - "load-grunt-tasks": "^0.4.0", - "time-grunt": "^0.3.1" - }, - "engines": { - "node": ">=0.10.0" + "grunt-filerev": "^2.3.1", + "grunt-newer": "^1.1.1", + "grunt-processhtml": "^0.3.8", + "grunt-shell": "^1.1.2", + "load-grunt-tasks": "^3.4.0", + "rimraf": "^2.5.2", + "serve-static": "^1.10.0", + "time-grunt": "^1.2.2", + "watchify": "^3.7.0" }, "scripts": { - "test": "grunt test" + "config": "grunt config", + "dev": "grunt serve", + "build": "grunt build", + "grunt-serve-build": "grunt serve_dist", + "clean": "grunt clean", + "clean-all": "grunt clean && rimraf bower_components && rimraf jspm/jspm_packages && rimraf node_modules", + "watchify": "npm run watchify-app | npm run watchify-rp | npm run watchify-popup", + "watchify-app": "watchify app/scripts/app.js --debug -o app.browser/scripts/dev-bundle.js --verbose", + "watchify-rp": "watchify app/scripts/rp.js --debug -o app.browser/scripts/rp-dev-bundle.js --verbose", + "watchify-popup": "watchify app/scripts/callback_popup.js --debug -o app.browser/scripts/popup-dev-bundle.js --verbose" } } diff --git a/register_with_anvil_connect.sh b/register_with_anvil_connect.sh deleted file mode 100755 index 7852c83..0000000 --- a/register_with_anvil_connect.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -# This is a template which is expanded and stored in dist when building -# the app with grunt. - -# Register client with anvil connect - -client='{ - "client_name": "Angular example with <%= AUTH_DISPLAY %> for <%= APP_SERVER %>", - "default_max_age": 36000, - "redirect_uris": [ - "<%= APP_SERVER %>/<%= APP_AUTH_CALLBACK %>", - "<%= APP_SERVER %>/rp.html"], - "post_logout_redirect_uris": ["<%= APP_SERVER %>"], - "trusted": true -}' - -echo "Registring nv add client $client" - -out=$(nv add client "$client") || { - echo "failed to register client" >&2 exit 2 -} - -# From OS X: -# $ echo "$out" | grep "_id" | grep -o -E '([[:xdigit:]]*-){1,10}[[:xdigit:]]*' -# ec8262ae-28d5-4943-8237-d8145042c3e0 -client_id=$(echo "$out" | grep "_id" | grep -o -E '([[:xdigit:]]*-){1,10}[[:xdigit:]]*') - -echo "Succeeded." -echo "Define CLIENT_ID as follows in authconf.json:" -printf '{\n' -printf '...\n' -printf ' "CLIENT_ID" : "%s",\n' "$client_id" -printf '...\n' -printf '}\n'