├── .gitattributes ├── app ├── .buildignore ├── robots.txt ├── favicon.ico ├── images │ ├── camera.png │ ├── edit.png │ ├── logo.png │ ├── plus.png │ ├── spinner.gif │ ├── success.png │ ├── factors │ │ ├── sms.png │ │ └── google-authenticator.png │ ├── app-store-button.png │ ├── google-play-button.png │ └── google-authenticator-logo.png ├── scripts │ ├── controllers │ │ ├── registration.js │ │ ├── unverified.js │ │ ├── error.js │ │ ├── mfa-redirect.js │ │ ├── verify.js │ │ ├── registrationform.js │ │ ├── reset.js │ │ ├── forgot.js │ │ ├── mfa-setup.js │ │ ├── mfa-verify.js │ │ └── login.js │ ├── directives │ │ ├── validateonblur.js │ │ ├── namevalidation.js │ │ ├── passwordmatchvalidation.js │ │ ├── formgroup.js │ │ ├── emailvalidation.js │ │ ├── formcontrol.js │ │ └── passwordpolicyvalidation.js │ ├── url-rewriter.js │ ├── app.js │ ├── services │ │ └── stormpath.js │ └── app-mock.js ├── views │ ├── unverified.html │ ├── mfa-redirect.html │ ├── error.html │ ├── password-error-messages.html │ ├── verify.html │ ├── reset.html │ ├── forgot.html │ ├── registration.html │ ├── mfa-verify.html │ ├── login.html │ └── mfa-setup.html ├── go.html ├── error.html ├── styles │ └── grid │ │ ├── reset.less │ │ ├── grid.less │ │ └── text.less └── index.html ├── .bowerrc ├── .gitignore ├── test ├── runner.html ├── spec │ ├── services │ │ └── stormpath.js │ ├── directives │ │ ├── formgroup.js │ │ ├── formcontrol.js │ │ ├── namevalidation.js │ │ ├── emailvalidation.js │ │ ├── validateonblur.js │ │ ├── passwordmatchvalidation.js │ │ └── passwordpolicyvalidation.js │ └── controllers │ │ ├── unverified.js │ │ ├── forgot.js │ │ ├── registration.js │ │ ├── registrationform.js │ │ ├── reset.js │ │ ├── login.js │ │ ├── hostedlogin.js │ │ └── verify.js ├── protractor │ ├── .eslintrc │ ├── unverified.js │ ├── .jshintrc │ ├── error.js │ ├── page-objects │ │ ├── submit-form.js │ │ ├── idsite-app.js │ │ ├── login-form.js │ │ ├── forgot-password-form.js │ │ └── registration-form.js │ ├── verify.js │ ├── suite │ │ └── password.js │ ├── forgot.js │ ├── reset.js │ ├── register.js │ ├── login.js │ └── util.js └── .jshintrc ├── protractor.conf.sauce.js ├── bower.json ├── .editorconfig ├── .jshintrc ├── .travis.yml ├── protractor.conf.js ├── karma-e2e.conf.js ├── karma.conf.js ├── package.json ├── CHANGELOG.md ├── localtunnel.js ├── README.md ├── LICENSE └── Gruntfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/images/camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/camera.png -------------------------------------------------------------------------------- /app/images/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/edit.png -------------------------------------------------------------------------------- /app/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/logo.png -------------------------------------------------------------------------------- /app/images/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/plus.png -------------------------------------------------------------------------------- /app/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/spinner.gif -------------------------------------------------------------------------------- /app/images/success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/success.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | app/bower_components 6 | coverage 7 | .env 8 | -------------------------------------------------------------------------------- /app/images/factors/sms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/factors/sms.png -------------------------------------------------------------------------------- /app/images/app-store-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/app-store-button.png -------------------------------------------------------------------------------- /app/images/google-play-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/google-play-button.png -------------------------------------------------------------------------------- /app/images/google-authenticator-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/google-authenticator-logo.png -------------------------------------------------------------------------------- /app/images/factors/google-authenticator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/idsite-src/HEAD/app/images/factors/google-authenticator.png -------------------------------------------------------------------------------- /app/scripts/controllers/registration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('RegistrationCtrl', function ($scope) { 5 | return $scope; 6 | }); 7 | -------------------------------------------------------------------------------- /app/scripts/controllers/unverified.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('UnverifiedCtrl', function ($scope,Stormpath,$location) { 5 | if(!Stormpath.isRegistered){ 6 | $location.path('/'); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /protractor.conf.sauce.js: -------------------------------------------------------------------------------- 1 | var baseConfig = require('./protractor.conf.js').config; 2 | 'use strict'; 3 | 4 | baseConfig.directConnect = false; 5 | baseConfig.sauceUser = process.env.SAUCE_USERNAME; 6 | baseConfig.sauceKey = process.env.SAUCE_ACCESS_KEY; 7 | 8 | exports.config = baseConfig; -------------------------------------------------------------------------------- /app/scripts/controllers/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('ErrorCtrl', function ($scope,Stormpath) { 5 | $scope.errors = Stormpath.errors; 6 | $scope.inError = false; 7 | $scope.$watchCollection('errors',function(){ 8 | $scope.inError = $scope.errors.length > 0; 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /app/views/unverified.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Check Your Email 6 |
7 |

Your account has been successfully created! Check your email and activate your account.

8 |
9 |
10 |
-------------------------------------------------------------------------------- /app/scripts/controllers/mfa-redirect.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('MfaRedirectCtrl', function ($scope, $routeParams, Stormpath) { 5 | $scope.source = $routeParams.source; 6 | $scope.jwt = $routeParams.jwt; 7 | 8 | function redirectWithJwt() { 9 | Stormpath.ssoEndpointRedirect($scope.jwt); 10 | } 11 | 12 | setTimeout(redirectWithJwt, 1000 * 3); 13 | }); 14 | -------------------------------------------------------------------------------- /app/scripts/directives/validateonblur.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('validateOnBlur', function () { 5 | return { 6 | restrict: 'A', 7 | link: function postLink(scope, element) { 8 | element.on('blur',function(){ 9 | scope.$apply(function(){ 10 | scope.validate(element); 11 | }); 12 | }); 13 | } 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /test/spec/services/stormpath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: Stormpath', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | // instantiate service 9 | var Stormpath; 10 | beforeEach(inject(function (_Stormpath_) { 11 | Stormpath = _Stormpath_; 12 | })); 13 | 14 | it('should do something', function () { 15 | expect(!!Stormpath).to.equal(true); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stormpath-idp", 3 | "version": "0.5.1", 4 | "dependencies": { 5 | "angular": "~1.5.5", 6 | "json3": "~3.2.6", 7 | "es5-shim": "~2.1.0", 8 | "angular-route": "~1.5.5", 9 | "bootstrap": "~3.1.1", 10 | "stormpath.js": "^0.8.0" 11 | }, 12 | "devDependencies": { 13 | "angular-mocks": "1.2.15", 14 | "angular-scenario": "1.2.15" 15 | }, 16 | "resolutions": { 17 | "angular": "~1.5.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/scripts/directives/namevalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('nameValidation', function () { 5 | return { 6 | restrict: 'A', 7 | link: function postLink(scope) { 8 | scope.validate = function(element){ 9 | scope.clearErrors(); 10 | var t = element.val() === ''; 11 | scope.validationError = t; 12 | return t; 13 | }; 14 | } 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /test/protractor/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "after": false, 4 | "afterEach": false, 5 | "angular": false, 6 | "before": false, 7 | "beforeEach": false, 8 | "browser": false, 9 | "by": false, 10 | "describe": false, 11 | "element": false, 12 | "inject": false, 13 | "it": false, 14 | "jasmine": false, 15 | "spyOn": false 16 | }, 17 | "env": { 18 | "es6": true, 19 | "node": true 20 | }, 21 | "extends": "eslint:recommended" 22 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /app/scripts/directives/passwordmatchvalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('passwordMatchValidation', function () { 5 | return { 6 | restrict: 'A', 7 | link: function postLink(scope) { 8 | scope.validate = function(element){ 9 | var t = (scope.fields.password.value !== '' && (element.val()!==scope.fields.password.value)); 10 | scope.validationError = t; 11 | return t; 12 | }; 13 | } 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false, 23 | "browser": false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/scripts/controllers/verify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('VerifyCtrl', function ($scope, Stormpath) { 5 | 6 | $scope.status = 'loading'; 7 | 8 | Stormpath.init.then(function initSuccess(){ 9 | Stormpath.verifyEmailToken(function(err){ 10 | if(err){ 11 | $scope.status='failed'; 12 | $scope.error = String(err.userMessage || err.developerMessage || err.message || err); 13 | }else{ 14 | $scope.status='verified'; 15 | } 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/directives/formgroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: formGroup', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/directives/formcontrol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: formControl', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/directives/namevalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: nameValidation', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/directives/emailvalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: emailValidation', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/directives/validateonblur.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: validateOnBlur', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/controllers/unverified.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: UnverifiedCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var UnverifiedCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | UnverifiedCtrl = $controller('UnverifiedCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should do something', function () { 20 | expect(true).to.equal(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/controllers/forgot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ForgotCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var ForgotCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | ForgotCtrl = $controller('ForgotCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should initially set scope.sent to false', function () { 20 | expect(scope.sent).to.equal(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/controllers/registration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: RegistrationCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var RegistrationCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | RegistrationCtrl = $controller('RegistrationCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should do something', function () { 20 | expect(true).to.equal(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/spec/directives/passwordmatchvalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: passwordMatchValidation', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /test/spec/directives/passwordpolicyvalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Directive: passwordPolicyValidation', function () { 4 | 5 | // load the directive's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var element, 9 | scope; 10 | 11 | beforeEach(inject(function ($rootScope) { 12 | scope = $rootScope.$new(); 13 | })); 14 | 15 | it('should make hidden element visible', inject(function ($compile) { 16 | element = angular.element('
'); 17 | element = $compile(element)(scope); 18 | expect(element.text()).to.equal(''); 19 | })); 20 | }); 21 | -------------------------------------------------------------------------------- /app/scripts/url-rewriter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(window) { 4 | 5 | function rewriter(url){ 6 | // put a ? before the first query param expression, doing so will allow 7 | // angular to nicely consume the values and make them available through 8 | // the $location.search() api 9 | var match = url.match(/([^#&?]+=[^#&?]+)/g,'?$1'); 10 | if(match){ 11 | var b = match.join('&').replace(/^\//,''); 12 | var a = url.replace(b,''); 13 | a = a.replace(/[&?\/]$/,''); 14 | return a + '?' + b; 15 | }else{ 16 | return url; 17 | } 18 | 19 | } 20 | window.SpHashRewriter = rewriter; 21 | })(window); -------------------------------------------------------------------------------- /test/spec/controllers/registrationform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: RegistrationFormCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var RegistrationFormCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | RegistrationFormCtrl = $controller('RegistrationFormCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should set a fields object', function () { 20 | expect(typeof scope.fields).to.equal('object'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/views/mfa-redirect.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | Verification Successful 7 | Additional Factor Verified 8 |
9 |

10 | You'll be redirected to your application shortly. 11 | You're all set. You'll be redirected to your application shortly. 12 |

13 |

14 | 15 |

16 |
17 | -------------------------------------------------------------------------------- /app/go.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

1.) Open your dev tools and the network inspector so that you are ready to see the action! 5 |

2.) Paste the oauth request link here: 6 |

7 | 8 |

9 |

3.) After you paste the link, it will be available for clicking here:

10 | (link not pasted yet!) 11 |

4.) Once you click the link you will be taken through the redirect to the login page. You can come back to this page at any time and paste in a new link to start the process over again.

12 | 13 | -------------------------------------------------------------------------------- /test/protractor/unverified.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var util = require('./util'); 10 | 11 | describe.skip('Verification view', function() { 12 | 13 | describe('if I arrive here directly', function() { 14 | 15 | before(function(){ 16 | browser.get( 17 | browser.params.appUrl + '#/unverified' + util.fakeAuthParams('1') 18 | ); 19 | }); 20 | it('should take me to the login view', function() { 21 | browser.sleep(1000); 22 | util.getCurrentUrl(function(url){ 23 | expect(url).to.match(/\/#\/\?/); 24 | }); 25 | }); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /app/views/error.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

 

5 |

Sorry! There was a problem.

6 |
7 |
8 |

9 | Please use your browser's back button to return to the previous page, then try again. 10 |

11 |
12 | 13 |

14 | If this problem persists, please provide this error information to your support team: 15 |

16 | 17 |
18 |
19 | {{e}} 20 |
21 |
22 | 23 |
24 |
25 |
-------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /app/views/password-error-messages.html: -------------------------------------------------------------------------------- 1 | Password is too short 2 | Password is too long 3 | Password requires a number 4 | Password requires a symbol 5 | Password requires a diacritical character 6 | Password requires a uppercase letter 7 | Password requires a lowercase letter -------------------------------------------------------------------------------- /test/protractor/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "by": false, 29 | "describe": false, 30 | "element": false, 31 | "inject": false, 32 | "it": false, 33 | "jasmine": false, 34 | "spyOn": false 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /app/scripts/directives/formgroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('formGroup', function () { 5 | return { 6 | restrict: 'A', 7 | scope: true, 8 | link: function postLink(scope, element, attrs) { 9 | scope.validationError = false; 10 | scope.errors = {}; 11 | scope.$watch('validationError',function(){ 12 | element.toggleClass(attrs.errorClass||'has-error',scope.validationError); 13 | }); 14 | scope.$watchCollection('errors',function(){ 15 | var errorCount = Object.keys(scope.errors).filter(function(k){ 16 | return scope.errors[k]; 17 | }).length; 18 | element.toggleClass(attrs.errorClass||'has-error',scope.validationError || errorCount>0); 19 | }); 20 | 21 | } 22 | }; 23 | }); 24 | -------------------------------------------------------------------------------- /test/spec/controllers/reset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ResetCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var ResetCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | ResetCtrl = $controller('ResetCtrl', { 15 | $scope: scope, 16 | Stormpath: { 17 | init: { 18 | then: function(cb){cb();} 19 | }, 20 | verifyPasswordToken: function(cb){ 21 | // verification success 22 | cb(null); 23 | } 24 | } 25 | }); 26 | })); 27 | 28 | it('should set scope.status to success', function () { 29 | expect(scope.status).to.equal('verified'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/spec/controllers/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: LoginCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var LoginCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | LoginCtrl = $controller('LoginCtrl', { 15 | $scope: scope, 16 | Stormpath: { 17 | init: { 18 | then: function(cb){cb();} 19 | }, 20 | providers: {}, 21 | idSiteModel: { 22 | passwordPolicy: null 23 | }, 24 | getProvider: function(){} 25 | } 26 | }); 27 | })); 28 | 29 | it('should set scope.ready to true', function () { 30 | expect(scope.ready).to.equal(true); 31 | }); 32 | }); -------------------------------------------------------------------------------- /app/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |

 

15 |

 

16 |

Sorry, an error has occurred

17 |

 

18 |

Please contact the owner of this site for assistance

19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /test/spec/controllers/hostedlogin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ErrorCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('stormpathIdpApp')); 7 | 8 | var ErrorCtrl, 9 | scope; 10 | 11 | var stormpath = { 12 | errors: [] 13 | }; 14 | 15 | // Initialize the controller and a mock scope 16 | beforeEach(inject(function ($controller, $rootScope) { 17 | scope = $rootScope.$new(); 18 | ErrorCtrl = $controller('ErrorCtrl', { 19 | $scope: scope, 20 | Stormpath: stormpath 21 | }); 22 | })); 23 | 24 | it('should observe new errors and set inError to true', function () { 25 | var anError = 'an error'; 26 | stormpath.errors.push(anError); 27 | expect(scope.errors[0]).to.equal(anError); 28 | scope.$digest(); 29 | expect(scope.inError).to.equal(true); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /app/scripts/directives/emailvalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('emailValidation', function () { 5 | var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 6 | return { 7 | restrict: 'A', 8 | link: function postLink(scope) { 9 | scope.errors = { 10 | duplicateUser: false 11 | }; 12 | scope.setError = function(k,v){ 13 | scope.errors[k] = v; 14 | }; 15 | scope.validate = function(element){ 16 | scope.clearErrors(); 17 | var val = element.val().trim(); 18 | var t = val==='' ? true : (!re.test(val)); 19 | scope.validationError = t; 20 | return t; 21 | }; 22 | } 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | - '6' 5 | script: travis_retry grunt build && npm run protractor-sauce 6 | before_script: 7 | - npm install -g bower grunt-cli 8 | - bower install 9 | env: 10 | global: 11 | - secure: AsoJDUsQ/X+zsB76UuBkYeDEnEIAgbtBg+n6wzd6l4fSDGdPfLQaVfRgW9JR0++nwrG4/WSd+DfdH/jJ1izyHX4Ap+Q6KxRNEnGZwAsIWeytshs2rwIrW7kFahPG8t2pgN/NII8yRPJIlyp2SCpUh1dRMhGcwsy4r4lsWSriRV8= 12 | - secure: vf0ZwRFA96Vfa6SgUXc0uvbSmFdX5LfrXYkickaTJ5ln8oIsfdzxv7+Fu80f5WBbYDynRrrBPADubEVl8YgsshlSrha7UYG2ybK1j1RbtN2CVUHL8nEr49hTwxN9SoNCJMFgxYJ0BDTwelvSJHuUBoiqphjVHaUIn0iREk96eIU= 13 | - secure: WrLnXlTHBHXK8IJzQsIQ0KWlXjAZi7Ns0pRbI1xgZ6PNogFw+qwdvI9fnNP/jbc9SkmQgESJep2NtDfeRFZogUEcr9+0oMndeEf1tIY98aQhZWIj5MuKAXmSUkESrIx2Dqj50k7VTO9ESw+KIPaMTsjyE2fCmcykp+gxLnle37Q= 14 | - secure: jrc0vyKqReqyvdFkStajRNWP1uzNvCtR/rmWN4plUPyzd5a7rRvs2pnJWpVQ9xS69n/muD9uJiHAV9v/jjzRvHS2UOn8HlFPjPda9lksEy641IiOnEwG2G8FR9YBJnyus4YX/vFr12wFBV4Rs6wSRkco2malP8KOFHIJPvs4mR0= 15 | -------------------------------------------------------------------------------- /test/protractor/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var ErrorPage = function(){ 10 | 11 | this.arrive = function arrive(){ 12 | browser.ignoreSynchronization = true; 13 | console.log('browser.params',browser.params); 14 | browser.get( 15 | 'error.html' 16 | ); 17 | browser.sleep(1000); 18 | }; 19 | this.hasErrorMessage = function(){ 20 | return element(by.css('[wd-error-message]')).isPresent(); 21 | }; 22 | }; 23 | 24 | 25 | describe.skip('error.html page', function() { 26 | var page = new ErrorPage(); 27 | before(function(){ 28 | page.arrive(); 29 | }); 30 | 31 | after(function(){ 32 | browser.ignoreSynchronization = false; 33 | }); 34 | 35 | it('should show an error message', function() { 36 | expect(page.hasErrorMessage()).to.eventually.equal(true); 37 | }); 38 | 39 | 40 | }); -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('./test/protractor/util'); 4 | var q = require('q'); 5 | 6 | exports.config = { 7 | directConnect: true, 8 | framework: 'mocha', 9 | specs: ['test/protractor/*.js'], 10 | exclude: ['test/protractor/util.js'], 11 | mochaOpts: { 12 | reporter: 'spec', 13 | timeout: 20000 14 | }, 15 | params:{ 16 | // The configurable logo URL for ID Site, to assert that it works 17 | logoUrl: 'https://stormpath.com/images/template/logo-nav.png' 18 | }, 19 | 20 | capabilities: { 21 | browserName: 'chrome', 22 | version: '41', 23 | platform: 'OS X 10.10', 24 | name: "chrome-tests" 25 | }, 26 | 27 | onPrepare: function() { 28 | return browser.driver.wait(function() { 29 | return util.ready(); 30 | }, 20000); 31 | }, 32 | onCleanUp: function(exitCode) { 33 | var deferred = q.defer(); 34 | 35 | util.cleanup(function(){ 36 | deferred.resolve(exitCode); 37 | }); 38 | return deferred.promise; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /test/protractor/page-objects/submit-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var SubmitForm = function(){ 4 | 5 | this.submit = function(){ 6 | return element(by.css(this.cssRoot+'button')).submit(); 7 | }; 8 | this.submitIsDisabled = function() { 9 | return element(by.css(this.cssRoot +' button[type=submit]')).getAttribute('disabled'); 10 | }; 11 | this.waitForForm = function(){ 12 | /* 13 | TODO - implenet something in the stormpath.js client 14 | which keeps track of the "is waiting for a network request" 15 | state. then, we can get rid of these wait functions and have 16 | a common "wait for client" function. 17 | */ 18 | var self = this; 19 | return browser.driver.wait(function(){ 20 | return self.isPresent(); 21 | },10000); 22 | }; 23 | this.waitForSubmitResult = function(){ 24 | var self = this; 25 | browser.driver.wait(function(){ 26 | return self.submitIsDisabled().then(function(value) { 27 | return value === null; 28 | }); 29 | },10000); 30 | }; 31 | }; 32 | 33 | module.exports = SubmitForm; -------------------------------------------------------------------------------- /app/views/verify.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Your account has been successfully created 6 | Sorry, there was an error while verifying your account 7 |
8 |
9 |

10 | Success! Your account is ready, please 11 | log in 12 |

13 |
14 |
15 | {{error}} 16 |
17 |
18 |

19 | Please try again by clicking on the verification link that was sent to your email address. 20 |

21 |

22 | If you continue to have problems you may need to 23 | register 24 | again. 25 |

26 |
27 |
28 |
29 |
-------------------------------------------------------------------------------- /test/protractor/page-objects/idsite-app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('../util'); 4 | 5 | 6 | var IdSiteApp = function(){ 7 | this.pageTitle = function(){ 8 | return browser.getTitle(); 9 | }; 10 | this.logoImageUrl = function(){ 11 | return element(by.css('.logo')).getAttribute('src'); 12 | }; 13 | this.arriveWithJwt = function arriveWithJwt(path,done){ 14 | util.getJwtUrl(path,function(url){ 15 | browser.get(url); 16 | // Stormpath.js needs some time to load the ID Site model 17 | browser.sleep(2000).then(done); 18 | }); 19 | }; 20 | this.clickRegistrationLink = function() { 21 | console.log('click'); 22 | element(by.css('[wd-can-register]')).click(); 23 | }; 24 | this.waitForUrlChange = function() { 25 | 26 | var currentUrl; 27 | 28 | return browser.driver.getCurrentUrl().then(function storeCurrentUrl(url) { 29 | currentUrl = url; 30 | }).then(function waitForUrlToChangeTo() { 31 | return browser.wait(function waitForUrlToChangeTo() { 32 | return browser.driver.getCurrentUrl().then(function compareCurrentUrl(url) { 33 | return url !== currentUrl; 34 | }); 35 | }); 36 | }); 37 | 38 | }; 39 | }; 40 | 41 | module.exports = IdSiteApp; -------------------------------------------------------------------------------- /app/styles/grid/reset.less: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ */ 2 | /* v1.0 | 20080212 */ 3 | 4 | html, body, div, span, applet, object, iframe, 5 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 6 | a, abbr, acronym, address, big, cite, code, 7 | del, dfn, em, font, img, ins, kbd, q, s, samp, 8 | small, strike, strong, sub, sup, tt, var, 9 | b, u, i, center, 10 | dl, dt, dd, ol, ul, li, 11 | fieldset, form, label, legend, 12 | table, caption, tbody, tfoot, thead, tr, th, td { 13 | margin: 0; 14 | padding: 0; 15 | border: 0; 16 | outline: 0; 17 | font-size: 100%; 18 | vertical-align: baseline; 19 | background: transparent; 20 | } 21 | body { 22 | line-height: 1; 23 | } 24 | ol, ul { 25 | list-style: none; 26 | } 27 | blockquote, q { 28 | quotes: none; 29 | } 30 | blockquote:before, blockquote:after, 31 | q:before, q:after { 32 | content: ''; 33 | content: none; 34 | } 35 | 36 | /* remember to define focus styles! */ 37 | :focus { 38 | outline: 0; 39 | } 40 | 41 | /* remember to highlight inserts somehow! */ 42 | ins { 43 | text-decoration: none; 44 | } 45 | del { 46 | text-decoration: line-through; 47 | } 48 | 49 | /* tables still need 'cellspacing="0"' in the markup */ 50 | table { 51 | border-collapse: collapse; 52 | border-spacing: 0; 53 | } -------------------------------------------------------------------------------- /app/scripts/controllers/registrationform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('RegistrationFormCtrl', function ($scope,Stormpath) { 5 | 6 | function errorParser(errObject){ 7 | var err = errObject || {}; 8 | return String(err.message || err.userMessage || err.developerMessage || err); 9 | } 10 | 11 | $scope.fields = {}; 12 | 13 | $scope.submit = function(){ 14 | $scope.knownError = $scope.unknownError = false; 15 | var inError = Object.keys($scope.fields).filter(function(f){ 16 | var field = $scope.fields[f]; 17 | return field.validate(); 18 | }); 19 | var data = Object.keys($scope.fields).reduce(function(acc,f){ 20 | acc[f] = $scope.fields[f].value; 21 | return acc; 22 | },{}); 23 | delete data.passwordConfirm; 24 | if(inError.length===0){ 25 | $scope.submitting = true; 26 | Stormpath.register(data,function(err){ 27 | $scope.submitting = false; 28 | if(err){ 29 | if(err.status===409){ 30 | $scope.fields.email.setError('duplicateUser', true); 31 | }else if (err.code){ 32 | $scope.knownError = errorParser(err); 33 | }else{ 34 | $scope.unknownError = errorParser(err); 35 | } 36 | } 37 | }); 38 | } 39 | }; 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /app/scripts/controllers/reset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('ResetCtrl', function ($scope,Stormpath,$location) { 5 | 6 | $scope.status = 'loading'; 7 | 8 | $scope.fields = {}; 9 | 10 | var verification; 11 | 12 | Stormpath.init.then(function initSuccess(){ 13 | Stormpath.verifyPasswordToken(function(err,pwTokenVerification){ 14 | if(err){ 15 | if(err.status===404){ 16 | $location.path('/forgot/retry'); 17 | }else{ 18 | $scope.status='failed'; 19 | $scope.error = err.userMessage || err; 20 | } 21 | }else{ 22 | $scope.status='verified'; 23 | verification = pwTokenVerification; 24 | } 25 | }); 26 | }); 27 | $scope.submit = function(){ 28 | var errorCount = Object.keys($scope.fields).filter(function(f){ 29 | var field = $scope.fields[f]; 30 | return field.validate(); 31 | }).length; 32 | if(errorCount>0){ 33 | return; 34 | } 35 | var newPassword = $scope.fields.password.value; 36 | $scope.submitting = true; 37 | Stormpath.setNewPassword(verification,newPassword,function(err){ 38 | $scope.submitting = false; 39 | if(err){ 40 | $scope.unknownError = String(err.userMessage || err.developerMessage || err); 41 | }else{ 42 | $scope.status = 'success'; 43 | } 44 | }); 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /app/scripts/directives/formcontrol.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('formControl', function () { 5 | return { 6 | restrict: 'A', 7 | link: function postLink(scope, element, attrs) { 8 | var fieldname = attrs.name; 9 | if(!scope.fields){ 10 | scope.fields = {}; 11 | } 12 | scope.fields[fieldname] = { 13 | value: element.val(), 14 | validationError: false, 15 | errors: scope.errors || {}, 16 | setError: function(k,v){ 17 | if(typeof scope.setError === 'function'){ 18 | scope.setError(k,v); 19 | } 20 | }, 21 | validate: function(){ 22 | return typeof scope.validate === 'function' ? scope.validate(element) : true; 23 | } 24 | }; 25 | 26 | scope.clearErrors = function(){ 27 | Object.keys(scope.errors).map(function(k){scope.errors[k]=false;}); 28 | }; 29 | 30 | element.on('input',function(){ 31 | scope.$apply(function(scope){ 32 | scope.fields[fieldname].value = element.val(); 33 | }); 34 | }); 35 | scope.$watchCollection('errors',function(a){ 36 | angular.extend(scope.fields[fieldname].errors,a||{}); 37 | }); 38 | scope.$watchCollection('fields.'+fieldname+'.errors',function(a){ 39 | angular.extend(scope.errors,a||{}); 40 | }); 41 | } 42 | }; 43 | }); 44 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function(){ 4 | 5 | angular 6 | .module('stormpathIdpApp', [ 7 | 'ngRoute' 8 | ]) 9 | .config(function ($routeProvider) { 10 | $routeProvider 11 | .when('/', { 12 | templateUrl: 'views/login.html', 13 | controller: 'LoginCtrl' 14 | }) 15 | .when('/register', { 16 | templateUrl: 'views/registration.html', 17 | controller: 'RegistrationCtrl' 18 | }) 19 | .when('/forgot/:retry?', { 20 | templateUrl: 'views/forgot.html', 21 | controller: 'ForgotCtrl' 22 | }) 23 | .when('/reset', { 24 | templateUrl: 'views/reset.html', 25 | controller: 'ResetCtrl' 26 | }) 27 | .when('/verify', { 28 | templateUrl: 'views/verify.html', 29 | controller: 'VerifyCtrl' 30 | }) 31 | .when('/unverified', { 32 | templateUrl: 'views/unverified.html', 33 | controller: 'UnverifiedCtrl' 34 | }) 35 | .when('/mfa/setup/:factor?', { 36 | templateUrl: 'views/mfa-setup.html', 37 | controller: 'MfaSetupCtrl' 38 | }) 39 | .when('/mfa/verify/:factor?/:firstVerification?', { 40 | templateUrl: 'views/mfa-verify.html', 41 | controller: 'MfaVerifyCtrl' 42 | }) 43 | .when('/mfa/redirect/:source?/:jwt?', { 44 | templateUrl: 'views/mfa-redirect.html', 45 | controller: 'MfaRedirectCtrl' 46 | }) 47 | .otherwise({ 48 | redirectTo: '/' 49 | }); 50 | }); 51 | })(window); 52 | -------------------------------------------------------------------------------- /test/protractor/verify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var util = require('./util'); 10 | 11 | var VerificationView = function(){ 12 | var cssRoot = '.verify-view '; 13 | this.isShowingSuccess = function(){ 14 | return element(by.css(cssRoot+'.verified')).isDisplayed(); 15 | }; 16 | this.isShowingError = function(){ 17 | return element(by.css(cssRoot+'.verification-failed')).isDisplayed(); 18 | }; 19 | }; 20 | 21 | 22 | describe.skip('Email verification view', function() { 23 | 24 | describe('with a valid token', function() { 25 | var view = new VerificationView(); 26 | before(function(){ 27 | browser.get( 28 | browser.params.appUrl + '#/verify' + util.fakeAuthParams('1',{sp_token:'avalidtoken'}) 29 | ); 30 | browser.sleep(3000); 31 | }); 32 | it('should show me the success message', function() { 33 | expect(view.isShowingSuccess()).to.eventually.equal(true); 34 | }); 35 | }); 36 | 37 | describe('with an invalid token', function() { 38 | var view = new VerificationView(); 39 | before(function(){ 40 | browser.get( 41 | browser.params.appUrl + '#/verify' + util.fakeAuthParams('1',{sp_token:'invalid'}) 42 | ); 43 | browser.sleep(3000); 44 | }); 45 | it('should tell me that there was an error', function() { 46 | expect(view.isShowingError()).to.eventually.equal(true); 47 | }); 48 | }); 49 | 50 | }); -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['ng-scenario'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: ['Chrome'], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /app/scripts/controllers/forgot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('ForgotCtrl', function ($scope,Stormpath,$routeParams,$rootScope) { 5 | $scope.sent = false; 6 | $scope.ready = false; 7 | $scope.retry = $routeParams.retry || false; 8 | $scope.fields = {}; 9 | $rootScope.$on('$locationChangeStart',function(e){ 10 | if($scope.sent){ 11 | e.preventDefault(); 12 | } 13 | }); 14 | Stormpath.init.then(function initSuccess(){ 15 | $scope.organizationNameKey = Stormpath.getOrganizationNameKey(); 16 | $scope.showOrganizationField = Stormpath.client.jwtPayload.sof; 17 | $scope.disableOrganizationField = $scope.organizationNameKey !== ''; 18 | $scope.ready = true; 19 | }); 20 | $scope.submit = function(){ 21 | $scope.notFound = false; 22 | var inError = Object.keys($scope.fields).filter(function(f){ 23 | return $scope.fields[f].validate(); 24 | }); 25 | if(inError.length>0){ 26 | return; 27 | } 28 | var data = { 29 | email: $scope.fields.email.value.trim() 30 | }; 31 | if($scope.organizationNameKey){ 32 | data.accountStore = { 33 | nameKey: $scope.organizationNameKey 34 | }; 35 | } 36 | if(Stormpath.client.jwtPayload.ash){ 37 | data.accountStore = { 38 | href: Stormpath.client.jwtPayload.ash 39 | }; 40 | } 41 | $scope.submitting = true; 42 | Stormpath.sendPasswordResetEmail(data,function(){ 43 | $scope.sent = true; 44 | $scope.submitting = false; 45 | }); 46 | }; 47 | }); 48 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['mocha','chai'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'app/bower_components/angular/angular.js', 15 | 'app/bower_components/angular-mocks/angular-mocks.js', 16 | 'app/bower_components/angular-route/angular-route.js', 17 | 18 | 'app/scripts/*.js', 19 | 'app/scripts/**/*.js', 20 | 'app/vendor/**/*.js', 21 | 'test/mock/**/*.js', 22 | 'test/spec/**/*.js' 23 | ], 24 | 25 | // list of files / patterns to exclude 26 | exclude: ['app/scripts/*mock*'], 27 | 28 | // web server port 29 | port: 8080, 30 | 31 | // level of logging 32 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 33 | logLevel: config.LOG_INFO, 34 | 35 | 36 | // enable / disable watching file and executing tests whenever any file changes 37 | autoWatch: false, 38 | 39 | 40 | // Start these browsers, currently available: 41 | // - Chrome 42 | // - ChromeCanary 43 | // - Firefox 44 | // - Opera 45 | // - Safari (only Mac) 46 | // - PhantomJS 47 | // - IE (only Windows) 48 | browsers: ['Chrome'], 49 | 50 | reporters: ['progress','coverage'], 51 | preprocessors: { 52 | 'app/scripts/**/*.js': 'coverage' 53 | }, 54 | 55 | 56 | // Continuous Integration mode 57 | // if true, it capture browsers, run tests and exit 58 | singleRun: false 59 | }); 60 | }; 61 | -------------------------------------------------------------------------------- /test/protractor/suite/password.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | module.exports = function(beforeFn,FormConstructor){ 10 | 11 | describe('Password validation', function() { 12 | var form; 13 | beforeEach(function(){ 14 | beforeFn(); 15 | form = new FormConstructor(); 16 | }); 17 | 18 | describe('if I enter a too short password', function(){ 19 | it('should show the password too short message',function(){ 20 | form.typeAndBlurPassword('a'); 21 | browser.sleep(1000); 22 | expect(form.isShowingPasswordError('minLength')).to.eventually.equal(true); 23 | }); 24 | }); 25 | describe('if I enter a too long password', function(){ 26 | it('should show the password too long message',function(){ 27 | form.typeAndBlurPassword('aaaaaaaaaaaaaaaaaaaaaaaaaa'); 28 | browser.sleep(1000); 29 | expect(form.isShowingPasswordError('maxLength')).to.eventually.equal(true); 30 | }); 31 | }); 32 | describe('if I enter a password without a lowercase', function(){ 33 | it('should show the lowercase required message',function(){ 34 | form.typeAndBlurPassword('AAAAAAAAAA'); 35 | browser.sleep(1000); 36 | expect(form.isShowingPasswordError('requireLowerCase')).to.eventually.equal(true); 37 | }); 38 | }); 39 | describe('if I enter a password without a uppercase', function(){ 40 | it('should show the uppercase required message',function(){ 41 | form.typeAndBlurPassword('aaaaaaaaaa'); 42 | browser.sleep(1000); 43 | expect(form.isShowingPasswordError('requireUpperCase')).to.eventually.equal(true); 44 | }); 45 | }); 46 | describe('if I enter a password without a number', function(){ 47 | it('should show the number required message',function(){ 48 | form.typeAndBlurPassword('aaaaaaaaaaAA'); 49 | browser.sleep(1000); 50 | expect(form.isShowingPasswordError('requireNumeric')).to.eventually.equal(true); 51 | }); 52 | }); 53 | 54 | }); 55 | }; -------------------------------------------------------------------------------- /app/styles/grid/grid.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid LESS 3 | * 4 | * Horizontal Grid 5 | * 6 | * Copyright 2009 Greg Thornton 7 | * License: http://creativecommons.org/licenses/by-sa/3.0/us/ 8 | */ 9 | 10 | @import url(reset.less); 11 | @import url(text.less); 12 | 13 | /** 14 | * Create a grid container. 15 | */ 16 | .grid-container(@columns:16, @width: 960px, @margin: 10px) { 17 | width: @width; 18 | margin-right: auto; 19 | margin-left: auto; 20 | 21 | /** 22 | * Variable passed to child grids. 23 | */ 24 | @column-width: @width / @columns; 25 | 26 | /** 27 | * Create a grid. 28 | */ 29 | .grid(@grid-columns: 0) { 30 | 31 | /** 32 | * Styles 33 | */ 34 | display: inline; 35 | float: left; 36 | position: relative; 37 | margin-left: @margin; 38 | margin-right: @margin; 39 | width: @grid-columns * @column-width - 2 * @margin; 40 | } 41 | 42 | /** 43 | * Add empty columns to the left. 44 | */ 45 | .prefix(@prefix: 0) { 46 | padding-left: @column-width * @prefix; 47 | } 48 | 49 | /** 50 | * Add empty columns to the right. 51 | */ 52 | .suffix(@suffix: 0) { 53 | padding-right: @column-width * @suffix; 54 | } 55 | 56 | /** 57 | * Pull grid to the left. 58 | */ 59 | .pull(@pull: 0) { 60 | left: 0 - @column-width * @pull; 61 | } 62 | 63 | /** 64 | * Push grid to the right. 65 | */ 66 | .push(@push: 0) { 67 | left: @column-width * @push; 68 | } 69 | } 70 | 71 | /** 72 | * The following 2 mixins would ideally be defined inside the .grid-container 73 | * mixin since it isn't relevant in any other scope. There is a bug in LESS 74 | * that makes it impossible to nest mixins that don't take arguments so they 75 | * are defined globally for now. 76 | */ 77 | 78 | /** 79 | * The first grid inside another grid. 80 | */ 81 | .alpha { 82 | margin-left: 0; 83 | } 84 | 85 | /** 86 | * The last grid inside another grid. 87 | */ 88 | .omega { 89 | margin-right: 0; 90 | } 91 | 92 | /** 93 | * Make an element clear all floats. 94 | */ 95 | .clear { 96 | clear: both; 97 | } -------------------------------------------------------------------------------- /app/scripts/directives/passwordpolicyvalidation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .directive('passwordPolicyValidation', function (Stormpath) { 5 | return { 6 | restrict: 'A', 7 | link: function postLink(scope) { 8 | scope.errors = { 9 | minLength: false, 10 | maxLength: false, 11 | requireLowerCase: false, 12 | requireUpperCase: false, 13 | requireNumeric: false, 14 | requireDiacritical: false 15 | }; 16 | scope.errorCount = function(){ 17 | return Object.keys(scope.errors).filter(function(k){ 18 | return scope.errors[k]; 19 | }).length; 20 | }; 21 | scope.validate = function(element){ 22 | scope.clearErrors(); 23 | var v = element.val(); 24 | 25 | if (!Stormpath.idSiteModel.passwordPolicy) { 26 | return; 27 | } 28 | 29 | var tests = [ 30 | ['minLength' , function(){return v.length < Stormpath.idSiteModel.passwordPolicy.minLength;}], 31 | ['maxLength' , function(){ return v.length > Stormpath.idSiteModel.passwordPolicy.maxLength;}], 32 | ['requireLowerCase' , function(){ return Stormpath.idSiteModel.passwordPolicy.requireLowerCase && !(/[a-z]/).test(v);}], 33 | ['requireUpperCase' , function(){ return Stormpath.idSiteModel.passwordPolicy.requireUpperCase && !(/[A-Z]/).test(v);}], 34 | ['requireNumeric' , function(){ return Stormpath.idSiteModel.passwordPolicy.requireNumeric && !(/[0-9]/).test(v);}], 35 | ['requireSymbol' , function(){ return Stormpath.idSiteModel.passwordPolicy.requireSymbol && !(/[!-\/:-@\[-`{-~]/).test(v);}], 36 | ['requireDiacritical' , function(){ return Stormpath.idSiteModel.passwordPolicy.requireDiacritical && !(/[\u00C0-\u017F]/).test(v);}] 37 | ]; 38 | 39 | for(var i=0;i0){ 42 | break; 43 | } 44 | } 45 | 46 | scope.validationError = scope.errorCount() > 0 ; 47 | return scope.validationError; 48 | }; 49 | } 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /app/styles/grid/text.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Grid LESS 3 | * 4 | * Vertical Grid and Text 5 | * 6 | * Copyright 2009 Greg Thornton 7 | * License: http://creativecommons.org/licenses/by-sa/3.0/us/ 8 | */ 9 | 10 | @font-size: 12px; 11 | @line-height: 18px; 12 | 13 | body { 14 | font-family: Helvetica, Arial, 'Liberation Sans', FreeSans, sans-serif; 15 | font-size: @font-size; 16 | line-height: @line-height; 17 | } 18 | 19 | a:focus { 20 | outline: 1px dotted; 21 | } 22 | 23 | hr { 24 | border: 0 #ccc solid; 25 | border-top-width: 1px; 26 | clear: both; 27 | height: 0; 28 | margin-top: -1px; 29 | } 30 | 31 | .gargantuan { 32 | font-weight: bold; 33 | font-size: 6em; 34 | line-height: 1em; 35 | } 36 | 37 | .enormous { 38 | font-weight: bold; 39 | font-size: 5em; 40 | line-height: 1.2em; 41 | } 42 | 43 | .giant { 44 | font-weight: bold; 45 | font-size: 4em; 46 | line-height: 1.125em; 47 | } 48 | 49 | .huge { 50 | font-weight: bold; 51 | font-size: 3em; 52 | line-height: 1.5em; 53 | } 54 | 55 | .h1 { 56 | font-weight: bold; 57 | font-size: 2.5em; 58 | line-height: 1.2em; 59 | } 60 | 61 | .h2 { 62 | font-weight: bold; 63 | font-size: 2em; 64 | line-height: 1.5em; 65 | } 66 | 67 | .h3 { 68 | font-weight: bold; 69 | font-size: 1.5em; 70 | line-height: @line-height; 71 | } 72 | 73 | .h4 { 74 | font-weight: bold; 75 | font-size: 1.25em; 76 | line-height: @line-height; 77 | } 78 | 79 | .h5 { 80 | font-weight: bold; 81 | font-size: 1em; 82 | line-height: @line-height; 83 | } 84 | 85 | .h6 { 86 | font-weight: normal; 87 | font-size: 1em; 88 | line-height: @line-height; 89 | } 90 | 91 | h1 { .h1; } 92 | h2 { .h2; } 93 | h3 { .h3; } 94 | h4 { .h4; } 95 | h5 { .h5; } 96 | h6 { .h6; } 97 | 98 | ol { 99 | list-style: decimal; 100 | } 101 | 102 | ul { 103 | list-style: none; 104 | } 105 | 106 | li { 107 | margin-left: 30px; 108 | } 109 | 110 | p, 111 | dl, 112 | hr, 113 | h1, 114 | h2, 115 | h3, 116 | h4, 117 | h5, 118 | h6, 119 | ol, 120 | ul, 121 | pre, 122 | table, 123 | address, 124 | fieldset { 125 | margin-bottom: @line-height; 126 | } -------------------------------------------------------------------------------- /test/protractor/forgot.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var IdSiteApp = require('./page-objects/idsite-app'); 10 | var ForgotPasswordForm = require('./page-objects/forgot-password-form'); 11 | 12 | describe('Forgot password form', function() { 13 | var form = new ForgotPasswordForm(); 14 | var app = new IdSiteApp(); 15 | beforeEach(function(done){ 16 | app.arriveWithJwt('/#/forgot',function(){ 17 | form.waitForForm().finally(done); 18 | }); 19 | }); 20 | 21 | describe('upon arrival', function() { 22 | it('should show me a link to return to login', function() { 23 | expect(form.isShowingBackToLogin()).to.eventually.equal(true); 24 | }); 25 | }); 26 | 27 | describe('if I enter an invalid email address', function() { 28 | it('should show me the invalid email error', function() { 29 | form.fillWithInvalidEmail(); 30 | form.submit(); 31 | expect(form.isShowingInvalidEmail()).to.eventually.equal(true); 32 | }); 33 | }); 34 | 35 | describe('if I submit the form with a valid email address', function() { 36 | it('should tell me to check my email for a link', function() { 37 | form.fillWithValidEmail(); 38 | form.submit(); 39 | form.waitForSubmitResult(); 40 | expect(form.isShowingSuccess()).to.eventually.equal(true); 41 | }); 42 | it('should hide the back-to-login link', function() { 43 | expect(form.isShowingBackToLogin()).to.eventually.equal(true); 44 | }); 45 | }); 46 | 47 | describe('if I enter an email with whitespace in the front', function() { 48 | it('should succeed', function() { 49 | form.fillWithEmailAndWhitespaceAtFront(); 50 | form.submit(); 51 | form.waitForSubmitResult(); 52 | expect(form.isShowingSuccess()).to.eventually.equal(true); 53 | }); 54 | }); 55 | 56 | describe('if I try to use the back button after a successful sent', function() { 57 | it('should keep me on this form', function() { 58 | form.fillWithValidEmail(); 59 | form.submit(); 60 | form.pressBackButton(); 61 | expect(browser.driver.getCurrentUrl()).to.eventually.contain('#/forgot'); 62 | }); 63 | }); 64 | 65 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stormpathidp", 3 | "version": "0.6.0", 4 | "repository": "https://github.com/stormpath/idsite-src", 5 | "dependencies": { 6 | "node-uuid": "^1.4.7" 7 | }, 8 | "devDependencies": { 9 | "async": "^2.1.2", 10 | "autoprefixer": "^6.5.1", 11 | "chai": "^3.5.0", 12 | "chai-as-promised": "^6.0.0", 13 | "colors": "^1.1.2", 14 | "express": "^4.14.0", 15 | "grunt": "^0.4.5", 16 | "grunt-bower-install": "^1.6.0", 17 | "grunt-concurrent": "^2.3.1", 18 | "grunt-contrib-clean": "^1.0.0", 19 | "grunt-contrib-concat": "^1.0.1", 20 | "grunt-contrib-connect": "^1.0.2", 21 | "grunt-contrib-copy": "^1.0.0", 22 | "grunt-contrib-cssmin": "^1.0.2", 23 | "grunt-contrib-htmlmin": "^2.0.0", 24 | "grunt-contrib-imagemin": "^1.0.1", 25 | "grunt-contrib-jshint": "^1.0.0", 26 | "grunt-contrib-less": "^1.4.0", 27 | "grunt-contrib-uglify": "^2.0.0", 28 | "grunt-contrib-watch": "^1.0.0", 29 | "grunt-google-cdn": "^0.4.3", 30 | "grunt-includes": "^0.5.0", 31 | "grunt-karma": "^0.8.3", 32 | "grunt-newer": "^1.2.0", 33 | "grunt-ng-annotate": "^2.0.2", 34 | "grunt-postcss": "^0.8.0", 35 | "grunt-protractor-runner": "^0.2.4", 36 | "grunt-rev": "~0.1.0", 37 | "grunt-svgmin": "^4.0.0", 38 | "grunt-usemin": "^3.1.1", 39 | "jshint-stylish": "^2.2.1", 40 | "karma": "^0.12.14", 41 | "karma-chai": "^0.1.0", 42 | "karma-chrome-launcher": "^0.1.3", 43 | "karma-coverage": "^0.2.1", 44 | "karma-mocha": "^0.1.3", 45 | "karma-ng-html2js-preprocessor": "^0.1.0", 46 | "karma-ng-scenario": "^0.1.0", 47 | "load-grunt-tasks": "^3.5.2", 48 | "localtunnel": "^1.7.0", 49 | "mocha": "^3.1.2", 50 | "ngrok": "^2.2.3", 51 | "protractor": "^4.0.9", 52 | "q": "^1.4.1", 53 | "request": "^2.75.0", 54 | "stormpath": "^0.18.5", 55 | "time-grunt": "^1.4.0", 56 | "untildify": "^3.0.2" 57 | }, 58 | "engines": { 59 | "node": ">=0.10.0" 60 | }, 61 | "license": "Apache-2.0", 62 | "homepage": "https://github.com/stormpath/idsite-src", 63 | "scripts": { 64 | "test": "grunt test", 65 | "install": "node ./node_modules/protractor/bin/webdriver-manager update", 66 | "protractor-sauce": "./node_modules/protractor/bin/protractor protractor.conf.sauce.js" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/protractor/page-objects/login-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LoginForm = function(){ 4 | this.typeInField = function(field,value){ 5 | return element(by.css('.login-form input[name='+field+']')).sendKeys(value); 6 | }; 7 | this.submit = function(){ 8 | return element(by.css('button')).submit(); 9 | }; 10 | this.isShowingInvalidLogin = function(){ 11 | return element(by.css('.bad-login')).isDisplayed(); 12 | }; 13 | this.isShowingNotFound = function(){ 14 | return element(by.css('.not-found')).isDisplayed(); 15 | }; 16 | this.isShowingRegistrationLink = function(){ 17 | return element(by.css('[wd-can-register]')).isDisplayed(); 18 | }; 19 | this.hasFacebookButton = function(){ 20 | return element(by.css('.btn-facebook')).isPresent(); 21 | }; 22 | this.hasGoogleButton = function(){ 23 | return element(by.css('.btn-google')).isPresent(); 24 | }; 25 | this.hasSamlButton = function(){ 26 | return element(by.css('.btn-saml')).isPresent(); 27 | }; 28 | this.isShowingProviderArea = function(){ 29 | return element(by.css('.provider-area')).isDisplayed(); 30 | }; 31 | this.isShowingUsernameField = function(){ 32 | return element(by.css('.login-form input[name=username]')).isDisplayed(); 33 | }; 34 | this.isPresent = function(){ 35 | return element(by.css('.login-view')).isDisplayed(); 36 | }; 37 | this.submitIsDisabled = function() { 38 | return element(by.css('.login-view button[type=submit]')).getAttribute('disabled'); 39 | }; 40 | this.waitForForm = function(){ 41 | /* 42 | TODO - implenet something in the stormpath.js client 43 | which keeps track of the "is waiting for a network request" 44 | state. then, we can get rid of these wait functions and have 45 | a common "wait for client" function. 46 | */ 47 | var self = this; 48 | return browser.wait(function(){ 49 | return self.isPresent(); 50 | },10000); 51 | }; 52 | this.waitForLoginAttempt = function(){ 53 | var self = this; 54 | browser.driver.wait(function(){ 55 | return self.submitIsDisabled().then(function(value) { 56 | return value === null; 57 | }); 58 | },10000); 59 | }; 60 | this.login = function(account){ 61 | this.typeInField('username',account.email); 62 | this.typeInField('password',account.password); 63 | this.submit(); 64 | }; 65 | }; 66 | 67 | module.exports = LoginForm; -------------------------------------------------------------------------------- /test/protractor/page-objects/forgot-password-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('../util'); 4 | 5 | var SubmitForm = require('./submit-form'); 6 | 7 | var ForgotPasswordForm = function(){ 8 | this.cssRoot = '.forgot-view '; 9 | SubmitForm.call(this); 10 | return this; 11 | }; 12 | 13 | 14 | ForgotPasswordForm.prototype.typeInField = function(field,value){ 15 | return element(by.css(this.cssRoot+'input[name='+field+']')).sendKeys(value); 16 | }; 17 | ForgotPasswordForm.prototype.clearField = function(field){ 18 | return element(by.css(this.cssRoot+'input[name='+field+']')).clear(); 19 | }; 20 | ForgotPasswordForm.prototype.isShowingSuccess = function(){ 21 | return element(by.css(this.cssRoot+'.wd-sent')).isDisplayed(); 22 | }; 23 | ForgotPasswordForm.prototype.isShowingBackToLogin = function(){ 24 | return element(by.css('[wd-back-to-login]')).isDisplayed(); 25 | }; 26 | ForgotPasswordForm.prototype.isShowingInvalidEmail = function(){ 27 | return element(by.css(this.cssRoot+'.wd-invalid-email')).isDisplayed(); 28 | }; 29 | ForgotPasswordForm.prototype.isShowingUserNotFound = function(){ 30 | return element(by.css(this.cssRoot+'.wd-not-found')).isDisplayed(); 31 | }; 32 | ForgotPasswordForm.prototype.fillWithInvalidEmail = function(){ 33 | this.typeInField('email','123'); 34 | }; 35 | ForgotPasswordForm.prototype.fillWithValidEmail = function(){ 36 | this.typeInField('email','robert@stormpath.com'); 37 | }; 38 | ForgotPasswordForm.prototype.fillWithEmailAndWhitespaceAtFront = function(){ 39 | this.typeInField('email', ' robert@stormpath.com'); 40 | }; 41 | ForgotPasswordForm.prototype.arrive = function(){ 42 | browser.get( 43 | browser.params.appUrl + '#/forgot' + util.fakeAuthParams('1') 44 | ); 45 | }; 46 | ForgotPasswordForm.prototype.pressBackButton = function pressBackButton(){ 47 | browser.navigate('/'); 48 | }; 49 | ForgotPasswordForm.prototype.isPresent = function(){ 50 | return element(by.css(this.cssRoot)).isDisplayed(); 51 | }; 52 | ForgotPasswordForm.prototype.waitForForm = function(){ 53 | /* 54 | TODO - implenet something in the stormpath.js client 55 | which keeps track of the "is waiting for a network request" 56 | state. then, we can get rid of these wait functions and have 57 | a common "wait for client" function. 58 | */ 59 | var self = this; 60 | return browser.driver.wait(function(){ 61 | return self.isPresent(); 62 | },10000); 63 | }; 64 | 65 | module.exports = ForgotPasswordForm; -------------------------------------------------------------------------------- /app/views/reset.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Password Reset 6 | Sorry! Something went wrong. 7 | You have successfully changed your password. 8 |
9 |
10 |

11 | You will need to make a new password reset request. 12 | Click here to try again 13 |

14 |

 

15 |
16 |
17 |

18 | You may now 19 | log in 20 |

21 |

 

22 |
23 |
24 |

The error was:

25 |

{{error}}

26 |
27 | 28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 | 41 | Password does not match 42 |
43 |
44 |
45 | Sorry, an error occured. Please return to the previous page and try again. 46 |
47 |
48 |

{{unknownError}}

49 |
50 | 51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /test/spec/controllers/verify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: VerifyCtrl', function () { 4 | 5 | var stormpathFixture1 = { 6 | init: { 7 | // dont callback on init, so that we can assert initial controller state 8 | then: function(){} 9 | } 10 | }; 11 | 12 | var stormpathFixture2 = { 13 | // verification success 14 | init: { 15 | then: function(cb){cb();} 16 | }, 17 | verifyEmailToken: function(cb){ 18 | cb(null); 19 | } 20 | }; 21 | 22 | var stormpathFixture3 = { 23 | // verification failure with generic error object 24 | init: { 25 | then: function(cb){cb();} 26 | }, 27 | verifyEmailToken: function(cb){ 28 | cb(new Error('an error')); 29 | } 30 | }; 31 | 32 | beforeEach(module('stormpathIdpApp')); 33 | 34 | describe('initial state',function(){ 35 | var VerifyCtrl, scope; 36 | beforeEach(inject(function ($controller, $rootScope) { 37 | scope = $rootScope.$new(); 38 | VerifyCtrl = $controller('VerifyCtrl', { 39 | $scope: scope, 40 | Stormpath: stormpathFixture1 41 | }); 42 | })); 43 | it('should begin with status loading', function () { 44 | expect(scope.status).to.equal('loading'); 45 | }); 46 | }); 47 | 48 | 49 | describe('with a valid token',function(){ 50 | var VerifyCtrl, scope; 51 | beforeEach(inject(function ($controller, $rootScope) { 52 | scope = $rootScope.$new(); 53 | VerifyCtrl = $controller('VerifyCtrl', { 54 | $scope: scope, 55 | Stormpath: stormpathFixture2 56 | }); 57 | })); 58 | it('should have status of verified', function () { 59 | expect(scope.status).to.equal('verified'); 60 | }); 61 | }); 62 | 63 | describe('with a invalid token',function(){ 64 | var VerifyCtrl, scope; 65 | beforeEach(inject(function ($controller, $rootScope) { 66 | scope = $rootScope.$new(); 67 | VerifyCtrl = $controller('VerifyCtrl', { 68 | $scope: scope, 69 | Stormpath: stormpathFixture3 70 | }); 71 | })); 72 | 73 | it('should have status failed if an error is returned', function () { 74 | expect(scope.status).to.equal('failed'); 75 | }); 76 | }); 77 | 78 | describe('with a generic error result',function(){ 79 | var VerifyCtrl, scope; 80 | beforeEach(inject(function ($controller, $rootScope) { 81 | scope = $rootScope.$new(); 82 | VerifyCtrl = $controller('VerifyCtrl', { 83 | $scope: scope, 84 | Stormpath: stormpathFixture3 85 | }); 86 | })); 87 | 88 | it('should put the error string onto scope.error', function () { 89 | expect(scope.error).to.equal('an error'); 90 | }); 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /app/views/forgot.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Password Reset 6 | Reset link has expired 7 | Please check your mail. 8 | 9 |
10 |
11 |

12 | We've sent a password reset link to 13 | {{fields.email.value}}. 14 |

15 |

 

16 |
17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | An organization name is required. 26 | 27 | 28 | Invalid Organization Name. 29 | 30 |
31 |
32 | 33 |
34 | 35 | 36 |
37 | 38 | Invalid email address 39 | Account does not exist. 40 |
41 |
42 |
43 | Sorry, an error occured. Please return to the previous page and try again. 44 |
45 |
46 |

{{unknownError}}

47 |
48 | 49 |
50 |
51 |
52 | 53 |
-------------------------------------------------------------------------------- /test/protractor/page-objects/registration-form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('../util'); 4 | var uuid = require('node-uuid'); 5 | 6 | var SubmitForm = require('./submit-form'); 7 | 8 | var RegistrationForm = function(){ 9 | var cssRoot = this.cssRoot = '.registration-form '; 10 | SubmitForm.call(this); 11 | this.typeInField = function(field,value){ 12 | return element(by.css(cssRoot+'input[name='+field+']')).sendKeys(value); 13 | }; 14 | this.clearField = function(field){ 15 | return element(by.css(cssRoot+'input[name='+field+']')).clear(); 16 | }; 17 | this.typeAndBlurPassword = function(v){ 18 | this.clearField('password'); 19 | this.typeInField('password',v); 20 | this.submit(); 21 | }; 22 | this.clearField = function(field){ 23 | return element(by.css(cssRoot+'input[name='+field+']')).clear(); 24 | }; 25 | this.isShowingInvalidEmail = function(){ 26 | return element(by.css(cssRoot+'.group-email .validation-error')).isDisplayed(); 27 | }; 28 | this.isShowingDuplicateUser = function(){ 29 | return element(by.css(cssRoot+'.duplicate-user')).isDisplayed(); 30 | }; 31 | this.isShowingPasswordError = function(error){ 32 | return element(by.css('[wd-'+error+']')).isDisplayed(); 33 | }; 34 | this.isPresent = function(){ 35 | return element(by.css('.registration-view')).isDisplayed(); 36 | }; 37 | this.waitForForm = function(){ 38 | /* 39 | TODO - implenet something in the stormpath.js client 40 | which keeps track of the "is waiting for a network request" 41 | state. then, we can get rid of these wait functions and have 42 | a common "wait for client" function. 43 | */ 44 | var self = this; 45 | return browser.driver.wait(function(){ 46 | return self.isPresent(); 47 | },10000); 48 | }; 49 | this.fillWithValidInformation = function(){ 50 | this.typeInField('givenName','test'); 51 | this.typeInField('surname','test'); 52 | this.typeInField('email','nobody+'+uuid()+'@stormpath.com'); 53 | this.typeInField('password','aaaaaaaaA1'); 54 | this.typeInField('passwordConfirm','aaaaaaaaA1'); 55 | }; 56 | this.fillWithDuplicateUser = function(){ 57 | this.fillWithValidInformation(); 58 | this.clearField('email'); 59 | this.typeInField('email',util.resources.loginAccount.email); 60 | }; 61 | this.arriveWithPasswordRequirements = 62 | function arriveWithPasswordRequirements(){ 63 | browser.get( 64 | browser.params.appUrl + '#register' + util.fakeAuthParams('1') 65 | ); 66 | }; 67 | this.arriveWithDiacriticRequirements = 68 | function arriveWithDiacriticRequirements(){ 69 | browser.get( 70 | browser.params.appUrl + '#register' + util.fakeAuthParams('2') 71 | ); 72 | }; 73 | }; 74 | 75 | module.exports = RegistrationForm; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.6.0 2 | 3 | Added Multi-Factor Authentication functionality. You can now require a user to enroll or verify a second factor on a per-session basis. Please see the new 4 | `require_mfa` option in this documentation: 5 | 6 | https://docs.stormpath.com/rest/product-guide/latest/idsite.html#idsite-auth-jwt 7 | 8 | ## 0.5.1 9 | 10 | * Fixed: the password input fields were not enforcing symbol requirements, if 11 | required by directory configuration. The result was an error from the REST API 12 | that was un-recoverable by the end-user. This has been fixed and the 13 | requirements are now enforced client-side, before making a call to the REST API. 14 | 15 | ## 0.5.0 16 | 17 | * Registration attempts now post to the Organization, if an Organization is 18 | specified when the user is redirected to ID Site. Previously they were posted 19 | to the Application's endpoint. 20 | 21 | **Known Bug**: In this version, the end user cannot recover from the duplicate 22 | account error in the organization context. This is an issue with the Stormpath 23 | REST API and a fix will be released soon. 24 | 25 | * This version also properly surfaces 403 errors from the Stormpath REST API. 26 | 27 | ## 0.4.2 28 | 29 | Fix "Back to login" link, on registration view, to include trailing slash. 30 | 31 | ## 0.4.1 32 | 33 | * Upgrade to `stormpath.js@0.6.2` for Enterprise/PD bug fix. 34 | 35 | * Upgrade to `stormpath@0.18.2` for local testing. 36 | 37 | ## 0.4.0 38 | 39 | * The initial JWT is now pulled from the URL, and persisted in a cookie. This 40 | allows the page to be refreshed, without breaking the authentication session. 41 | 42 | * If an Organization context is specified, the ID Site model is now requested 43 | from the Organization, rather than the parent application. 44 | 45 | To support these features, the [Stormpath.js][] dependency has been updated to 46 | 0.6.0. 47 | 48 | ## 0.3.0 49 | 50 | Adding SAML support. The ID Site model is used to provide the SAML providers and 51 | we render buttons for them in the right side bar, along the social providers. 52 | 53 | ## 0.2.4 54 | 55 | Password reset callback will redirect the user to the callback URL if there is 56 | an error during submission. 57 | 58 | ## 0.2.3 59 | 60 | An error message is now shown if email or password is omitted on login form. 61 | 62 | ## 0.2.2 63 | 64 | Upgrade to stormpath.js@0.4.0, better error handling and better error messages. 65 | 66 | With this release, ID Site timeout errors will redirect the user back to your 67 | callback URL, with an error JWT. 68 | 69 | ## 0.2.1 70 | 71 | Updating Stormpath.js to 0.3.1, to get organization support and base64 fixes. 72 | 73 | ## 0.2.0 74 | 75 | Adding support for Organizations, as configured by the ID Site Builder in the 76 | service provider. 77 | 78 | [Stormpath.js]: https://github.com/stormpath/stormpath.js 79 | -------------------------------------------------------------------------------- /app/views/registration.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | Create an Account 6 |
7 |
8 |
9 | Sorry, an error occured. The message was: 10 |
11 |
12 |

{{unknownError}}

13 |
14 |
15 |

{{knownError}}

16 |
17 |
18 | 19 |
20 | 21 | First name is required 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 | Last name is required 30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 | Email address is already registered 38 | Invalid email address 39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 | 54 | Password does not match 55 |
56 |
57 | 58 | 59 | 60 |
61 |
62 |
63 | 64 |
-------------------------------------------------------------------------------- /test/protractor/reset.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var util = require('./util'); 10 | 11 | var ResetPasswordPageObject = function(){ 12 | var cssRoot = '.reset-view '; 13 | this.typeInField = function(field,value){ 14 | return element(by.css(cssRoot+'input[name='+field+']')).sendKeys(value); 15 | }; 16 | this.typeAndBlurPassword = function(v){ 17 | this.clearField('password'); 18 | this.typeInField('password',v); 19 | this.submit(); 20 | }; 21 | this.clearField = function(field){ 22 | return element(by.css(cssRoot+'input[name='+field+']')).clear(); 23 | }; 24 | this.submit = function(){ 25 | return element(by.css(cssRoot+'button')).submit(); 26 | }; 27 | this.isShowingSuccess = function(){ 28 | return element(by.css(cssRoot+'[wd-success]')).isDisplayed(); 29 | }; 30 | this.formIsVisible = function(){ 31 | return element(by.css(cssRoot+'.reset-form')).isDisplayed(); 32 | }; 33 | this.isShowingMismatchedPasswords = function(){ 34 | return element(by.css(cssRoot+'[wd-pw-mismatch]')).isDisplayed(); 35 | }; 36 | this.isShowingUserNotFound = function(){ 37 | return element(by.css(cssRoot+'.wd-not-found')).isDisplayed(); 38 | }; 39 | this.isShowingPasswordError = function(error){ 40 | return element(by.css('[wd-'+error+']')).isDisplayed(); 41 | }; 42 | this.fillWithMismatchedPasswords = function(){ 43 | this.typeInField('password','123'); 44 | this.typeInField('passwordConfirm','1'); 45 | }; 46 | this.fillWithValidPasswords = function(){ 47 | this.typeInField('password','abC123'); 48 | this.typeInField('passwordConfirm','abC123'); 49 | }; 50 | this.clearForm = function(){ 51 | this.clearField('password'); 52 | this.clearField('passwordConfirm'); 53 | }; 54 | this.arriveWithValidToken = function arriveWithValidToken(){ 55 | browser.get( 56 | browser.params.appUrl + '#/reset' + util.fakeAuthParams('1',{sp_token:'avalidtoken'}) 57 | ); 58 | }; 59 | this.arriveWithInvalidToken = function arriveWithInvalidToken(){ 60 | browser.get( 61 | browser.params.appUrl + '#/reset' + util.fakeAuthParams('1',{sp_token:'invalid'}) 62 | ); 63 | }; 64 | }; 65 | 66 | 67 | describe.skip('Reset password view', function() { 68 | 69 | var pageObj = new ResetPasswordPageObject(); 70 | 71 | describe('with a valid token', function() { 72 | 73 | before(function(){ 74 | pageObj.arriveWithValidToken(); 75 | }); 76 | it('should show me the reset password form', function() { 77 | expect(pageObj.formIsVisible()).to.eventually.equal(true); 78 | }); 79 | 80 | }); 81 | 82 | require('./suite/password')(function(){ 83 | before(function(){ 84 | pageObj.arriveWithValidToken(); 85 | }); 86 | },ResetPasswordPageObject); 87 | 88 | describe('with an invalid token', function() { 89 | before(function(){ 90 | pageObj.arriveWithInvalidToken(); 91 | }); 92 | it('should send me to #/forgot/retry', function() { 93 | util.getCurrentUrl(function(url){ 94 | expect(url).to.have.string('#/forgot/retry'); 95 | }); 96 | }); 97 | }); 98 | 99 | }); -------------------------------------------------------------------------------- /app/views/mfa-verify.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | Verification Required 7 |
8 | 9 |
10 | Choose a method to verify access to your account: 11 |
12 | 13 |
14 |
15 |

16 |

{{factor.title}}

17 |

{{factor.subTitle}}

18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Verification Required 28 |
29 |
30 | We just sent a code to {{factor.phone.number}}. Enter the code you recieve here. 31 |
32 | 33 |
34 | Invalid code. Check that you've entered it correctly. 35 |
36 | 37 |
38 | Replacement code sent. 39 |
40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 | 54 |
55 |
56 |
57 |
58 |
59 | Verification Required 60 |
61 |
62 | 63 | Open your Google Authenticator app on your mobile device and enter the code shown for entry 64 | {{factor.issuer}} 65 | ({{factor.accountName}}). 66 | 67 |
68 | 69 |
70 | Invalid code. Check that you've entered the current code shown in Google Authenticator. 71 |
72 | 73 |
74 |
75 | 76 | 77 |
78 |
79 |
80 |
81 |
82 |
83 | -------------------------------------------------------------------------------- /test/protractor/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var util = require('./util'); 10 | 11 | var IdSiteApp = require('./page-objects/idsite-app'); 12 | var RegistrationForm = require('./page-objects/registration-form'); 13 | 14 | describe('Registration view', function() { 15 | var form = new RegistrationForm(); 16 | var app = new IdSiteApp(); 17 | var mapping; 18 | before(function(done){ 19 | util.mapDirectory(util.resources.application,util.resources.directory,true,function(asm){ 20 | mapping = asm; 21 | done(); 22 | }); 23 | }); 24 | beforeEach(function(done){ 25 | app.arriveWithJwt('/#/register',function(){ 26 | form.waitForForm().finally(done); 27 | }); 28 | }); 29 | 30 | after(function(done) { 31 | util.deleteResource(mapping,done); 32 | }); 33 | 34 | describe('if I enter an invalid email address', function() { 35 | it('should show me the invalid email error', function() { 36 | form.typeInField('email','123'); 37 | form.submit(); 38 | expect(form.isShowingInvalidEmail()).to.eventually.equal(true); 39 | }); 40 | }); 41 | 42 | describe('if I try to register a duplicate email address', function() { 43 | it('should show me the duplicate user error', function() { 44 | form.fillWithValidInformation(); 45 | form.fillWithDuplicateUser(); 46 | form.submit(); 47 | form.waitForSubmitResult(); 48 | expect(form.isShowingDuplicateUser()).to.eventually.equal(true); 49 | }); 50 | }); 51 | 52 | describe('if I give unique user information and my account is unverified', function() { 53 | describe.skip('and the directory requires verification',function() { 54 | // TODO - need to provision a directory which has email verification enabled 55 | it('should tell me to check my email for a verification link', function() { 56 | form.fillWithValidInformation(); 57 | form.submit(); 58 | browser.sleep(1000); 59 | form.waitForSubmitResult(); 60 | expect(browser.driver.getCurrentUrl()).to.eventually.contain('unverified'); 61 | }); 62 | }); 63 | describe('and the directory does not require verification',function() { 64 | it('should take me to the service provider after form submission', function() { 65 | form.fillWithValidInformation(); 66 | form.submit(); 67 | browser.sleep(5000); 68 | expect(browser.driver.getCurrentUrl()).to.eventually.contain(util.resources.appHost + '/stormpathCallback'); 69 | }); 70 | }); 71 | }); 72 | }); 73 | 74 | describe.skip('Registration view with password requirements', function() { 75 | var form = new RegistrationForm(); 76 | require('./suite/password')(function(){ 77 | form.arriveWithPasswordRequirements(); 78 | },RegistrationForm); 79 | }); 80 | 81 | describe.skip('Registration view with diacritical password requirement', function() { 82 | var form = new RegistrationForm(); 83 | beforeEach(function(){ 84 | form.arriveWithDiacriticRequirements(); 85 | }); 86 | 87 | describe('if I enter a password without a diacritical', function(){ 88 | it('should show the diacritical required message',function(){ 89 | form.typeAndBlurPassword('aaaaaaaaaaAA1'); 90 | expect(form.isShowingPasswordError('requireDiacritical')).to.eventually.equal(true); 91 | }); 92 | }); 93 | 94 | }); -------------------------------------------------------------------------------- /localtunnel.js: -------------------------------------------------------------------------------- 1 | var localtunnel = require('localtunnel'); 2 | var stormpath = require('stormpath'); 3 | 4 | var callbckUri = 'http://stormpath.localhost:8001/idSiteCallback'; 5 | var client = new stormpath.Client(); 6 | var doCleanup = false; 7 | var previousDomainName = null; 8 | var host = null; 9 | 10 | /*eslint no-console: 0*/ 11 | 12 | function prepeareIdSiteModel(client, idSiteApplicationHost, callbckUri, cb) { 13 | client.getCurrentTenant(function(err, tenant) { 14 | if (err) { 15 | throw err; 16 | } 17 | 18 | client.getResource(tenant.href + '/idSites', function(err, collection) { 19 | if (err) { 20 | throw err; 21 | } 22 | 23 | var idSiteModel = collection.items[0]; 24 | 25 | previousDomainName = idSiteModel.domainName; 26 | 27 | idSiteModel.domainName = idSiteApplicationHost.split('//')[1]; 28 | 29 | if (idSiteModel.authorizedOriginUris.indexOf(idSiteApplicationHost) === -1) { 30 | idSiteModel.authorizedOriginUris.push(idSiteApplicationHost); 31 | idSiteModel.authorizedOriginUris.push(idSiteApplicationHost.replace('https','http')); 32 | } 33 | 34 | if (idSiteModel.authorizedRedirectUris.indexOf(callbckUri) === -1) { 35 | idSiteModel.authorizedRedirectUris.push(callbckUri); 36 | } 37 | 38 | idSiteModel.save(cb); 39 | }); 40 | }); 41 | } 42 | 43 | function revertIdSiteModel(client, idSiteApplicationHost, callbckUri, cb) { 44 | client.getCurrentTenant(function(err, tenant) { 45 | if (err) { 46 | throw err; 47 | } 48 | 49 | client.getResource(tenant.href + '/idSites', function(err, collection) { 50 | if (err) { 51 | throw err; 52 | } 53 | 54 | var idSiteModel = collection.items[0]; 55 | 56 | idSiteModel.domainName = previousDomainName; 57 | 58 | idSiteModel.authorizedOriginUris = idSiteModel.authorizedOriginUris.filter(function(uri) { 59 | return !uri.match(idSiteApplicationHost); 60 | }); 61 | 62 | idSiteModel.authorizedOriginUris = idSiteModel.authorizedOriginUris.filter(function(uri) { 63 | return !uri.match(idSiteApplicationHost.replace('https','http')); 64 | }); 65 | 66 | idSiteModel.authorizedRedirectUris = idSiteModel.authorizedRedirectUris.filter(function(uri) { 67 | return !uri.match(callbckUri); 68 | }); 69 | 70 | idSiteModel.save(cb); 71 | }); 72 | }); 73 | } 74 | 75 | function cleanup(cb){ 76 | if(doCleanup){ 77 | revertIdSiteModel(client,host,callbckUri,function (err) { 78 | if (err) { 79 | throw err; 80 | } 81 | console.log('ID Site Model Restored'); 82 | cb(); 83 | }); 84 | } 85 | } 86 | 87 | console.log(process.env.PORT); 88 | 89 | var tunnel = localtunnel(process.env.PORT || 9000, function(err, tunnel) { 90 | if (err) { 91 | console.error(err); 92 | return process.exit(1); 93 | } 94 | host = tunnel.url; 95 | console.log(host); 96 | prepeareIdSiteModel(client,host,callbckUri,function(err){ 97 | if (err) { 98 | throw err; 99 | } 100 | console.log('ID Site Model Ready'); 101 | setInterval(function () { },1000); 102 | doCleanup = true; 103 | }); 104 | 105 | }); 106 | 107 | tunnel.on('error', function (err) { 108 | console.error(err); 109 | cleanup(); 110 | }); 111 | 112 | tunnel.on('close', cleanup); 113 | 114 | process.on('SIGTERM', function() { 115 | console.log('\nCaught termination signal'); 116 | cleanup(function (){ 117 | process.exit(); 118 | }); 119 | }); 120 | 121 | process.on('SIGINT', function() { 122 | console.log('\nCaught interrupt signal'); 123 | cleanup(function (){ 124 | process.exit(); 125 | }); 126 | }); -------------------------------------------------------------------------------- /app/views/login.html: -------------------------------------------------------------------------------- 1 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /app/scripts/controllers/mfa-setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('MfaSetupCtrl', function ($location, $scope, $routeParams, Stormpath) { 5 | 6 | $scope.account = Stormpath.getAccountFromSession(); 7 | $scope.factor = null; 8 | $scope.factors = []; 9 | $scope.challenge = null; 10 | 11 | $scope.googleAuthenticator = { 12 | state: null, 13 | base64QRImage: null, 14 | code: null, 15 | factor: null 16 | }; 17 | 18 | $scope.smsFactor = { 19 | state: null, 20 | phoneNumber: null 21 | }; 22 | 23 | var factorMetaData = { 24 | 'sms': { 25 | id: 'sms', 26 | title: 'SMS Text Messages', 27 | subTitle: 'Your carrier\'s standard charges may apply.' 28 | }, 29 | 'google-authenticator': { 30 | id: 'google-authenticator', 31 | title: 'Google Authenticator', 32 | subTitle: 'A free app from Google.' 33 | } 34 | }; 35 | 36 | $scope.selectFactor = function (factor) { 37 | $location.path('/mfa/setup/' + factor); 38 | }; 39 | 40 | $scope.verifyGoogleAuthenticatorCode = function () { 41 | var data = { 42 | code: $scope.googleAuthenticator.code 43 | }; 44 | 45 | Stormpath.createChallenge($scope.factor, data, function (err, result) { 46 | if (err) { 47 | $scope.status = 'failed'; 48 | $scope.error = String(err.userMessage || err.developerMessage || err.message || err); 49 | return; 50 | } 51 | 52 | if (result.status === 'FAILED') { 53 | $scope.googleAuthenticator.state = 'invalid_code'; 54 | return; 55 | } 56 | 57 | Stormpath.mfaRedirectFromCallbackUrl('setup', result.serviceProviderCallbackUrl); 58 | }); 59 | }; 60 | 61 | $scope.createSmsFactor = function () { 62 | var data = { 63 | type: 'sms', 64 | phone: { 65 | number: $scope.smsFactor.phoneNumber 66 | } 67 | }; 68 | 69 | Stormpath.createFactor($scope.account, data, function (err) { 70 | if (err) { 71 | if (err.code === 13105) { 72 | return $scope.smsFactor.state = 'duplicate_phone_number'; 73 | } 74 | 75 | return $scope.smsFactor.state = 'invalid_phone_number'; 76 | } 77 | 78 | $location.path('/mfa/verify/sms/true'); 79 | }); 80 | }; 81 | 82 | $scope.status = 'loading'; 83 | 84 | Stormpath.init.then(function initSuccess(){ 85 | Stormpath.client.requireMfa.forEach(function (factorId) { 86 | factorId = factorId.toLowerCase(); 87 | 88 | if (!(factorId in factorMetaData)) { 89 | return; 90 | } 91 | 92 | var newFactor = JSON.parse(JSON.stringify(factorMetaData[factorId])); 93 | 94 | if (factorId === $routeParams.factor) { 95 | $scope.factor = newFactor; 96 | } 97 | 98 | $scope.factors.push(newFactor); 99 | }); 100 | 101 | if ($scope.factor && $scope.factor.id === 'google-authenticator') { 102 | var data = { 103 | type: 'google-authenticator', 104 | issuer: Stormpath.makeId() 105 | }; 106 | 107 | return Stormpath.createFactor($scope.account, data, function (err, remoteFactor) { 108 | if (err) { 109 | $scope.status = 'failed'; 110 | $scope.error = String(err.userMessage || err.developerMessage || err.message || err); 111 | return; 112 | } 113 | 114 | for (var key in remoteFactor) { 115 | $scope.factor[key] = remoteFactor[key]; 116 | } 117 | 118 | $scope.googleAuthenticator.factor = remoteFactor; 119 | $scope.googleAuthenticator.base64QRImage = remoteFactor.base64QRImage; 120 | $scope.status = 'loaded'; 121 | }); 122 | } 123 | 124 | $scope.status = 'loaded'; 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /app/scripts/controllers/mfa-verify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('MfaVerifyCtrl', function ($location, $scope, $routeParams, Stormpath) { 5 | $scope.account = Stormpath.getAccountFromSession(); 6 | $scope.factor = null; 7 | $scope.factors = []; 8 | $scope.challenge = null; 9 | $scope.otherFactorsAvailable = false; 10 | 11 | $scope.factorId = $routeParams.factor; 12 | $scope.isFirstVerification = $routeParams.firstVerification === 'true'; 13 | 14 | $scope.verification = { 15 | code: null, 16 | state: null 17 | }; 18 | 19 | var factorMetaData = { 20 | 'sms': { 21 | id: 'sms', 22 | title: 'Send a Text Message', 23 | subTitle: 'We\'ll send a code to {{phoneNumber}}.' 24 | }, 25 | 'google-authenticator': { 26 | id: 'google-authenticator', 27 | title: 'Google Authenticator', 28 | subTitle: 'View a code in the mobile app.' 29 | } 30 | }; 31 | 32 | $scope.selectFactor = function (factor) { 33 | $location.path('/mfa/verify/' + factor); 34 | }; 35 | 36 | $scope.changeSmsPhoneNumber = function () { 37 | $location.path('/mfa/setup/sms'); 38 | } 39 | 40 | $scope.resendSmsCode = function () { 41 | Stormpath.createChallenge($scope.factor, null, function (err, challenge) { 42 | if (err) { 43 | $scope.status = 'failed'; 44 | $scope.error = String(err.userMessage || err.developerMessage || err.message || err); 45 | return; 46 | } 47 | 48 | $scope.verification.state = 'resent_code'; 49 | $scope.challenge = challenge; 50 | }); 51 | }; 52 | 53 | $scope.verifyCode = function () { 54 | var data = { 55 | code: $scope.verification.code 56 | }; 57 | 58 | Stormpath.updateChallenge($scope.challenge, data, function (err, result) { 59 | if (err) { 60 | $scope.status = 'failed'; 61 | $scope.error = String(err.userMessage || err.developerMessage || err.message || err); 62 | return; 63 | } 64 | 65 | if (result.status === 'FAILED') { 66 | $scope.verification.state = 'invalid_code'; 67 | return; 68 | } 69 | 70 | var source = $scope.isFirstVerification ? 'setup' : 'verification'; 71 | 72 | Stormpath.mfaRedirectFromCallbackUrl(source, result.serviceProviderCallbackUrl); 73 | }); 74 | }; 75 | 76 | $scope.status = 'loading'; 77 | 78 | Stormpath.init.then(function initSuccess(){ 79 | var allowFactorMap = {}; 80 | 81 | Stormpath.client.requireMfa.forEach(function (factorId) { 82 | allowFactorMap[factorId.toLowerCase()] = null; 83 | }); 84 | 85 | $scope.otherFactorsAvailable = Stormpath.client.requireMfa.length > 1; 86 | 87 | Stormpath.getFactors($scope.account, function (err, factors) { 88 | if (err) { 89 | $scope.status = 'failed'; 90 | $scope.error = String(err.userMessage || err.developerMessage || err.message || err); 91 | return; 92 | } 93 | 94 | factors.items.forEach(function (remoteFactor) { 95 | if (remoteFactor.status !== 'ENABLED' || (remoteFactor.verificationStatus !== 'VERIFIED' && !$scope.isFirstVerification)) { 96 | return; 97 | } 98 | 99 | var factorId = remoteFactor.type.toLowerCase(); 100 | 101 | if (!(factorId in allowFactorMap)) { 102 | return; 103 | } 104 | 105 | var factor = JSON.parse(JSON.stringify(factorMetaData[factorId])); 106 | 107 | for (var key in remoteFactor) { 108 | factor[key] = remoteFactor[key]; 109 | } 110 | 111 | // Inject the phone number into the sms factor sub title. 112 | if (factorId === 'sms') { 113 | factor.subTitle = factor.subTitle.replace('{{phoneNumber}}', factor.phone.number); 114 | } 115 | 116 | // If the route 'factor' parameter is the current factor, then set that in our scope. 117 | if ($scope.factorId === factor.id) { 118 | $scope.factor = factor; 119 | } 120 | 121 | $scope.factors.push(factor); 122 | }); 123 | 124 | // If we don't have any factors, then redirect to the setup step. 125 | if (!$scope.factors.length) { 126 | return $location.path('/mfa/setup/'); 127 | } 128 | 129 | // If there's a factor selected, then create a new challenge. 130 | if ($scope.factor) { 131 | Stormpath.client.createChallenge($scope.factor, function (err, challenge) { 132 | $scope.challenge = challenge; 133 | }); 134 | } 135 | 136 | $scope.status = 'loaded'; 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /app/views/mfa-setup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | Two-Factor Authentication Required 7 |
8 | 9 |
10 | To protect your account from unauthorized access, we require that you set up an additional factor to verify your identity when logging in. Please choose a method to set up now: 11 |
12 | 13 |
14 |
15 |

16 |

{{factor.title}}

17 |

{{factor.subTitle}}

18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Set Up SMS Authentication 28 |
29 | 30 |
31 | Enter a phone number that you can receive SMS text messages at. You'll need to have access to this phone when logging into your account. 32 |
33 | 34 |
35 | Invalid phone number. 36 |
37 | 38 |
39 | That phone number has already been setup for this account. 40 |
41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 |
50 | 53 |
54 |
55 |
56 |
57 |
58 | Set Up Google Authenticator 59 |
60 | 61 |
62 |
63 | 1 64 | Install and open Google Authenticator on your mobile device. 65 |
66 | 67 |
68 |

69 |

70 |
71 |

72 |

73 | Search the App Store or Play Store
for "Google Authenticator". 74 |

75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 | 2 83 | Add this account to Google Authenticator. 84 |
85 | 86 |
87 |

Tap the plus icon.

88 |
89 |

Scan this barcode:

90 |

91 |
92 |
93 |

Paste in this code:

94 |
95 |

96 | {{factor.secret}} 97 |

98 |
99 | 100 |
101 |
102 | 103 |
104 |
105 | 3 106 | Enter the code shown in Google Authenticator. 107 |
108 |
109 |
110 | Invalid code. Check that you've entered the current code shown in Google Authenticator. 111 |
112 |
113 |
114 | 115 | 116 |
117 |
118 |
119 |
120 |
121 |
122 | 125 |
126 |
127 | -------------------------------------------------------------------------------- /app/scripts/controllers/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('stormpathIdpApp') 4 | .controller('LoginCtrl', function ($scope,Stormpath,$window) { 5 | $scope.ready = false; 6 | $scope.canRegister = true; 7 | $scope.errors = { 8 | badLogin: false, 9 | notFound: false, 10 | userMessage: false, 11 | unknown: false, 12 | organizationNameKeyRequired: false, 13 | organizationNameKeyInvalid: false 14 | }; 15 | 16 | function initFB(){ 17 | $window.fbAsyncInit = function() { 18 | var FB = $window.FB; 19 | FB.init({ 20 | appId: Stormpath.getProvider('facebook').clientId, 21 | xfbml: true, 22 | status: true, 23 | version: 'v2.0' 24 | }); 25 | }; 26 | (function(d, s, id){ 27 | var js, fjs = d.getElementsByTagName(s)[0]; 28 | if (d.getElementById(id)) { 29 | return; 30 | } 31 | js = d.createElement(s); 32 | js.id = id; 33 | js.src = '//connect.facebook.net/es_LA/sdk.js'; 34 | fjs.parentNode.insertBefore(js, fjs); 35 | }($window.document, 'script', 'facebook-jssdk')); 36 | } 37 | 38 | Stormpath.init.then(function initSuccess(){ 39 | $scope.organizationNameKey = Stormpath.getOrganizationNameKey(); 40 | $scope.showOrganizationField = Stormpath.client.jwtPayload.sof; 41 | $scope.disableOrganizationField = $scope.organizationNameKey !== ''; 42 | $scope.canRegister = !!Stormpath.idSiteModel.passwordPolicy; 43 | $scope.providers = Stormpath.providers; 44 | $scope.ready = true; 45 | $scope.hasProviders = $scope.providers.length > 0; 46 | if(Stormpath.getProvider('facebook')){ 47 | initFB(); 48 | } 49 | }); 50 | 51 | var googleIsSignedIn = false; 52 | 53 | function clearErrors(){ 54 | Object.keys($scope.errors).map(function(k){$scope.errors[k]=false;}); 55 | } 56 | 57 | function showError(err){ 58 | if(err.status===400){ 59 | if(err.code && err.code===2014){ 60 | $scope.errors.organizationNameKeyInvalid = true; 61 | }else{ 62 | $scope.errors.badLogin = true; 63 | } 64 | } 65 | else if(err.status===404){ 66 | $scope.errors.notFound = true; 67 | } 68 | else if(err.userMessage || err.message){ 69 | $scope.errors.userMessage = err.userMessage || err.message; 70 | }else{ 71 | $scope.errors.unknown = true; 72 | } 73 | } 74 | 75 | function errHandler(err){ 76 | $scope.submitting = false; 77 | if(err){ 78 | showError(err); 79 | } 80 | } 81 | 82 | $scope.submit = function(){ 83 | clearErrors(); 84 | if($scope.showOrganizationField && !$scope.organizationNameKey){ 85 | $scope.errors.organizationNameKeyRequired = true; 86 | } 87 | else if($scope.username && $scope.password){ 88 | $scope.submitting = true; 89 | var data = { 90 | login: $scope.username.trim(), 91 | password: $scope.password.trim() 92 | }; 93 | if($scope.organizationNameKey){ 94 | data.accountStore = { 95 | nameKey: $scope.organizationNameKey 96 | }; 97 | } 98 | if(Stormpath.client.jwtPayload.ash){ 99 | data.accountStore = { 100 | href: Stormpath.client.jwtPayload.ash 101 | }; 102 | } 103 | Stormpath.login(data,errHandler); 104 | }else{ 105 | $scope.errors.emailPasswordRequired = true; 106 | } 107 | }; 108 | 109 | $scope.googleLogin = function(){ 110 | var gapi = $window.gapi; 111 | if(!gapi){ 112 | return; 113 | } 114 | clearErrors(); 115 | var params = { 116 | clientid: Stormpath.getProvider('google').clientId, 117 | scope: 'email', 118 | cookiepolicy: 'single_host_origin', 119 | callback: function(authResult){ 120 | if (!googleIsSignedIn && authResult.status.signed_in && authResult.status.method === 'PROMPT') { 121 | googleIsSignedIn = true; 122 | Stormpath.register({ 123 | providerData: { 124 | providerId: 'google', 125 | accessToken: authResult.access_token 126 | } 127 | },errHandler); 128 | } 129 | } 130 | }; 131 | 132 | gapi.auth.signIn(params); 133 | }; 134 | 135 | function fbRegister(response){ 136 | Stormpath.register({ 137 | providerData: { 138 | providerId: 'facebook', 139 | accessToken: response.authResponse.accessToken 140 | } 141 | },errHandler); 142 | } 143 | 144 | $scope.facebookLogin = function(){ 145 | var FB = $window.FB; 146 | 147 | FB.login(function(response) { 148 | if(response.status === 'connected'){ 149 | fbRegister(response); 150 | } 151 | },{scope: 'email'}); 152 | 153 | }; 154 | 155 | $scope.samlLogin = function(provider){ 156 | Stormpath.samlLogin(provider.accountStore,errHandler); 157 | }; 158 | 159 | $scope.providerLogin = function(provider){ 160 | var providerId = provider.providerId; 161 | var fn = $scope[providerId+'Login']; 162 | if(typeof fn!=='function'){ 163 | console.error('provider login function \'' + providerId + '\' is not implemented'); 164 | }else{ 165 | fn(provider); 166 | } 167 | }; 168 | 169 | return $scope; 170 | }); 171 | -------------------------------------------------------------------------------- /test/protractor/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | var expect = chai.expect; 8 | 9 | var util = require('./util'); 10 | 11 | var LoginForm = require('./page-objects/login-form'); 12 | 13 | var IdSiteApp = require('./page-objects/idsite-app'); 14 | 15 | describe('Login Flow', function() { 16 | var app = new IdSiteApp(); 17 | var form = new LoginForm(); 18 | 19 | beforeEach(function(done){ 20 | app.arriveWithJwt('',function(){ 21 | form.waitForForm().finally(done); 22 | }); 23 | }); 24 | 25 | describe('If a password-based directory is mapped', function() { 26 | 27 | describe('without the default account store flag',function(){ 28 | 29 | var mapping; 30 | 31 | before(function(done) { 32 | util.mapDirectory(util.resources.application,util.resources.directory,false,function(asm){ 33 | mapping = asm; 34 | done(); 35 | }); 36 | }); 37 | 38 | after(function(done) { 39 | util.deleteResource(mapping,done); 40 | }); 41 | 42 | it('should have the correct page title', function() { 43 | expect(app.pageTitle()).to.eventually.equal('Login'); 44 | }); 45 | 46 | it('should show the logo image', function() { 47 | expect(app.logoImageUrl()).to.eventually.equal(browser.params.logoUrl); 48 | }); 49 | 50 | it('should show the login form', function() { 51 | expect(form.isPresent()).to.eventually.equal(true); 52 | }); 53 | 54 | it('should not show the social login area',function(){ 55 | expect(form.isShowingProviderArea()).to.eventually.equal(false); 56 | }); 57 | 58 | it('should not show the registration link',function() { 59 | expect(form.isShowingRegistrationLink()).to.eventually.equal(false); 60 | }); 61 | 62 | it('should allow me to submit the form', function(){ 63 | form.login(util.getLoginAccount()); 64 | browser.sleep(5000); 65 | expect(browser.driver.getCurrentUrl()).to.eventually.contain(util.resources.appHost + '/stormpathCallback'); 66 | }); 67 | 68 | it('should show me an error if I enter invalid credentials',function(){ 69 | form.login({email:'me@stormpath.com',password:'b'}); 70 | form.waitForLoginAttempt(); 71 | expect(form.isShowingInvalidLogin()).to.eventually.equal(true); 72 | }); 73 | }); 74 | 75 | describe('with the default account store flag', function() { 76 | 77 | var mapping; 78 | 79 | before(function(done) { 80 | util.mapDirectory(util.resources.application,util.resources.directory,true,function(asm){ 81 | mapping = asm; 82 | done(); 83 | }); 84 | }); 85 | 86 | after(function(done) { 87 | util.deleteResource(mapping,done); 88 | }); 89 | 90 | 91 | it('should show the registration link',function() { 92 | expect(form.isShowingRegistrationLink()).to.eventually.equal(true); 93 | }); 94 | 95 | }); 96 | }); 97 | 98 | 99 | 100 | describe.skip('if an organization name is required',function(){ 101 | it('should show the organization name field',function() { 102 | 103 | }); 104 | it('should show error when an invalid organization name is given',function() { 105 | 106 | }); 107 | it('should log me in if a valid organization and username and password is given',function() { 108 | 109 | }); 110 | }); 111 | 112 | describe('If a google directory is mapped to the application',function() { 113 | var mapping; 114 | before(function(done) { 115 | util.mapDirectory(util.resources.application,util.resources.googleDirectory,false,function(asm){ 116 | mapping = asm; 117 | done(); 118 | }); 119 | }); 120 | after(function(done) { 121 | util.deleteResource(mapping,done); 122 | }); 123 | it('should show the google login button',function() { 124 | expect(form.hasGoogleButton()).to.eventually.equal(true); 125 | 126 | }); 127 | }); 128 | 129 | describe('If a Facebook directory is mapped to the application',function() { 130 | var mapping; 131 | before(function(done) { 132 | util.mapDirectory(util.resources.application,util.resources.facebookDirectory,false,function(asm){ 133 | mapping = asm; 134 | done(); 135 | }); 136 | }); 137 | after(function(done) { 138 | util.deleteResource(mapping,done); 139 | }); 140 | it('should show the facebook login button',function() { 141 | expect(form.hasFacebookButton()).to.eventually.equal(true); 142 | }); 143 | }); 144 | 145 | describe('If a SAML directory is mapped to the application',function() { 146 | var mapping; 147 | before(function(done) { 148 | util.mapDirectory(util.resources.application,util.resources.samlDirectory,false,function(asm){ 149 | mapping = asm; 150 | done(); 151 | }); 152 | }); 153 | after(function(done) { 154 | util.deleteResource(mapping,done); 155 | }); 156 | it('should show the SAML login button',function() { 157 | expect(form.hasSamlButton()).to.eventually.equal(true); 158 | }); 159 | }); 160 | 161 | describe('If only social providers are mapped to the application',function() { 162 | 163 | var mapping; 164 | before(function(done) { 165 | util.mapDirectory(util.resources.application,util.resources.facebookDirectory,false,function(asm){ 166 | mapping = asm; 167 | done(); 168 | }); 169 | }); 170 | beforeEach(function() { 171 | form.waitForForm(); 172 | }); 173 | after(function(done) { 174 | util.deleteResource(mapping,done); 175 | }); 176 | it('should not show the registration link',function() { 177 | expect(form.isShowingRegistrationLink()).to.eventually.equal(false); 178 | }); 179 | 180 | // TODO ! 181 | it('should not show the login form',function() { 182 | this.skip(); 183 | expect(form.hasFacebookButton()).to.eventually.equal(true); 184 | }); 185 | }); 186 | 187 | }); -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Login 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 37 | 40 | 43 | 46 | 49 | 52 | 55 | 58 | 61 | 64 | 67 | 68 |
69 |
70 | 71 |
72 |
73 |
74 | 75 |
76 |
77 | 78 | 79 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Stormpath is Joining Okta 2 | We are incredibly excited to announce that [Stormpath is joining forces with Okta](https://stormpath.com/blog/stormpaths-new-path?utm_source=github&utm_medium=readme&utm-campaign=okta-announcement). Please visit [the Migration FAQs](https://stormpath.com/oktaplusstormpath?utm_source=github&utm_medium=readme&utm-campaign=okta-announcement) for a detailed look at what this means for Stormpath users. 3 | 4 | We're available to answer all questions at [support@stormpath.com](mailto:support@stormpath.com). 5 | 6 | # Stormpath ID Site Source # 7 | 8 | [Stormpath](http://stormpath.com/) is a User Management API that reduces 9 | development time with instant-on, scalable user infrastructure. Stormpath's 10 | intuitive API and expert support make it easy for developers to authenticate, 11 | manage, and secure users and roles in any application. 12 | 13 | This is the development environment for the Stormpath hosted ID Site. You can 14 | use this repository to build the same single page application (SPA) that 15 | Stormpath provides by default. The SPA uses Angular and Browserify and it is 16 | built using Grunt and Yeoman. 17 | 18 | If you want to build your own ID Site and are comfortable with Angular, you should 19 | use this repository. 20 | 21 | If you are not using Angular, you can build your own ID Site from scratch 22 | but you will need to use [Stormpath.js][] in order to communicate with our API 23 | from your custom ID Site application. 24 | 25 | ## Browser Support 26 | 27 | ID Site will work in the following web browser environments: 28 | 29 | * Chrome (all versions) 30 | * Internet Explorer 10+ 31 | * Firefox 23+ 32 | * Safari 8+ 33 | * Android Browser, if Android version is 4.1 (Jellybean) or greater 34 | 35 | ## Installation 36 | 37 | It is assumed that you have the following tools installed on your computer 38 | 39 | * [Bower][] 40 | * [Grunt][] 41 | * [Node.JS][] 42 | * [Localtunnel.me][] 43 | 44 | You should clone this repository and these tasks within the repository: 45 | 46 | ```sh 47 | npm install 48 | bower install 49 | ``` 50 | 51 | ## Setup an HTTPS proxy 52 | 53 | Because ID Site only works with HTTPS, you will need to setup a local tunnel 54 | which will serve your your local ID Site from a secure connection. With the 55 | local tunnel tool you must do this: 56 | 57 | > lt --port 9000 58 | 59 | It will fetch a URL and tell you something like this: 60 | 61 | > your url is: https://wqdkeiseuj.localtunnel.me 62 | 63 | You must take that URL and configure your ID Site accordingly. Please login 64 | to the [Stormpath Admin Console][] and set these options on your ID Site 65 | Configuration: 66 | 67 | | Configuration Option | Should be set to | 68 | |----------------------------------------|-------------------------------------------------------------------------------------| 69 | | **Domain Name** | your local tunnel URL | 70 | | **Authorized Javascript Origin URLs** | your local tunnel URL should be in this list | 71 | | **Authorized Redirect URLs** | the endpoint on your server application which will receive the user after ID site (read below) | 72 | 73 | ## Your Service Provider (required) 74 | 75 | The application (typically, your server) that sends the user to ID Site is known 76 | as the Service Provider (SP). You send the user to ID Site by constructing a 77 | redirect URL with one of our SDKs. For example, [createIdSiteUrl()][] in our 78 | Node.js SDK. 79 | 80 | After the user authenticates at ID Site, the user is redirected back to your 81 | application. Your application must have a callback URL which receives the user 82 | and validates the `jwtResponse` parameter in the URL (our SDK does this work 83 | for you). 84 | 85 | If you haven't built your service provider we have a simple service provider 86 | application which you can use for testing purposes, see: [Fake SP][] 87 | 88 | 89 | ## Startup 90 | 91 | Once you have setup the environment (the steps above) you are ready to start 92 | the development tasks. Run the following command to start the server: 93 | 94 | > grunt serve 95 | 96 | This will open the application your browser. Because the application does not 97 | have a JWT request, you will see the JWT error. At this point you should use 98 | your service provider to redirect the user to your ID Site. 99 | 100 | 101 | ## Development Process - Stormpath Tenants 102 | 103 | Do you use Stormpath? Are you forking this repository so that you can build a 104 | modified version of our default application? This section is for you. 105 | 106 | After you have started your environment (all the steps above) and made your 107 | customizations to your fork of this repository, there are two ways you can 108 | deploy your changes: 109 | 110 | * **Directly form your fork, unminified**. If you've forked this library and your changes 111 | exist in your fork, you can simply point your ID Site Configuration to the URL 112 | of your forked github repository. Nothing else is required, however you will 113 | be serving un-minified assets, which may be slower. 114 | 115 | * **With Minification**. If you want to serve minified assets for performance, 116 | you need to run `grunt build` to build the application into it's minified form. 117 | This will be placed in the `dist/` folder, which is not tracked by git. You 118 | will need to commit this output to another github repository, and then point 119 | your ID Site configuration at that git repository. 120 | 121 | ## Development Process - Stormpath Engineers 122 | 123 | Are you a Stormpath Engineer who is working on this module? This section is 124 | for you. 125 | 126 | You need to create a build of this application, after you have merged all of 127 | your changes into master. You create the build with the `grunt build` task. 128 | After the build is created, you need to copy the output from the `dist/` folder 129 | in this repo (which is not tracked by git) and commit it to the [ID Site Repository][]. 130 | 131 | The purpose of the [ID Site Repository][] is to hold the minified output of this 132 | development environment. When a tenant is using the default ID Site configuration, 133 | they are receiving the files that exist in the [ID Site Repository][]. 134 | 135 | When you push a commit to master on the [ID Site Repository][], the files 136 | are consumed and pushed to our CDN for serving to any tenant which is using the 137 | default ID Site. 138 | 139 | Are you working on a feature that is not yet ready for master, but you'd like a 140 | tenant to try it out? No problem! Just create a named topic branch in the 141 | [ID Site Repository][] and place the build output there. The tenant can then 142 | point their ID Site Configuration at the branch for testing purposes. When you 143 | merge the feature into master and deploy a master release, you should delete 144 | the branch. 145 | 146 | In this situation, you'll likely want to use `grunt build:debug` to create a 147 | non-obfuscated build output (which will make in-browser debugging easier). 148 | 149 | 150 | ### Testing 151 | 152 | To run the Selenium tests, you need to install Protractor: 153 | 154 | ``` 155 | npm install -g protractor 156 | ``` 157 | 158 | Start the development server by running `grunt serve`, 159 | then run Protractor with the config file in this repo: 160 | 161 | ``` 162 | protractor protractor.conf.js 163 | ``` 164 | 165 | **WARNING**: This will modify the ID Site Configuration of the Stormpath Tenant 166 | that is defined by these environment variables: 167 | 168 | ``` 169 | STORMPATH_CLIENT_APIKEY_ID 170 | STORMPATH_CLIENT_APIKEY_SECRET 171 | ``` 172 | Alas, you must ensure that you are using a tenant that is not used by your 173 | production application! 174 | 175 | ## Copyright 176 | 177 | Copyright © 2014 Stormpath, Inc. and contributors. 178 | 179 | This project is open-source via the [Apache 2.0 180 | License](http://www.apache.org/licenses/LICENSE-2.0). 181 | 182 | [Bower]: http://bower.io 183 | [createIdSiteUrl()]: https://docs.stormpath.com/nodejs/api/application#createIdSiteUrl 184 | [Fake SP]: https://github.com/robertjd/fakesp 185 | [Grunt]: http://gruntjs.com 186 | [ID Site Repository]: https://github.com/stormpath/idsite 187 | [Localtunnel.me]: http://localtunnel.me/ 188 | [Node.JS]: http://nodejs.org 189 | [Stormpath Admin Console]: https://api.stormpath.com 190 | [Stormpath.js]: https://github.com/stormpath/stormpath.js 191 | -------------------------------------------------------------------------------- /app/scripts/services/stormpath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | angular.module('stormpathIdpApp') 5 | .service('Stormpath', function Stormpath($window,$routeParams,$location,$rootScope,$q) { 6 | var self = this; 7 | var init = $q.defer(); 8 | var params = $location.search(); 9 | var stormpath = $window.Stormpath; 10 | var ieMatch = $window.navigator.userAgent.match(/MSIE ([0-9.]+)/); 11 | 12 | var client = self.client = null; 13 | self.init = init.promise; 14 | self.errors = []; 15 | self.jwt = params.jwt; 16 | self.isRegistered = null; 17 | self.providers = []; 18 | self.registeredAccount = null; 19 | self.isVerified = null; 20 | 21 | function showError(error){ 22 | var msg = error.userMessage || error.developerMessage || error.message || 'Unknown'; 23 | if(self.errors.indexOf(msg)===-1){ 24 | self.errors.push(msg); 25 | } 26 | } 27 | 28 | function getJwtFromCallbackUri(uri) { 29 | return uri.split('jwtResponse=')[1]; 30 | } 31 | 32 | function serviceProviderRedirect(serviceProviderCallbackUrl) { 33 | $window.location = serviceProviderCallbackUrl; 34 | } 35 | 36 | function initialize(){ 37 | if(ieMatch && ieMatch[1]){ 38 | if(parseInt(ieMatch[1],10)<10){ 39 | showError(new Error('Internet Explorer ' + ieMatch[1] + ' is not supported. Please try again with a newer browser.')); 40 | return; 41 | } 42 | } 43 | 44 | client = self.client = new stormpath.Client(function(err,idSiteModel){ 45 | $rootScope.$apply(function(){ 46 | if(err){ 47 | showError(err); 48 | init.reject(err); 49 | }else{ 50 | var m = idSiteModel; 51 | self.idSiteModel = m; 52 | self.providers = self.providers.concat(m.providers); 53 | $rootScope.logoUrl = m.logoUrl; 54 | init.resolve(); 55 | // If the initial JWT has account scope, it means that the user 56 | // is already logged in, but must complete MFA verification 57 | if (self.getAccountFromSession()) { 58 | $location.path('/mfa/verify') 59 | } 60 | } 61 | }); 62 | }); 63 | } 64 | 65 | this.ssoEndpointRedirect = function ssoEndpointRedirect(jwt) { 66 | $window.location = client.baseurl + 'sso/?jwtResponse=' + jwt; 67 | }; 68 | 69 | this.ssoEndpointRedirectFromUrl = function ssoEndpointRedirectFromUrl(url) { 70 | var jwt = getJwtFromCallbackUri(url); 71 | self.ssoEndpointRedirect(jwt); 72 | }; 73 | 74 | this.mfaRedirectFromCallbackUrl = function mfaRedirectFromCallbackUrl(source, url) { 75 | var jwt = getJwtFromCallbackUri(url); 76 | $location.path('/mfa/redirect/' + encodeURIComponent(source) + '/' + encodeURIComponent(jwt)); 77 | }; 78 | 79 | this.samlLogin = function samlLogin(accountStore, cb){ 80 | var xhrRequest = { 81 | method: 'GET', 82 | url: self.client.appHref + '/saml/sso/idpRedirect?accountStore.href=' + accountStore.href 83 | }; 84 | 85 | self.client.requestExecutor.execute(xhrRequest, function(err,response) { 86 | if(err){ 87 | if(err.serviceProviderCallbackUrl){ 88 | serviceProviderRedirect(err.serviceProviderCallbackUrl); 89 | }else{ 90 | cb(err); 91 | } 92 | }else{ 93 | $window.location = response.serviceProviderCallbackUrl; 94 | } 95 | }); 96 | }; 97 | 98 | this.getAccountFromSession = function getAccountFromSession() { 99 | var sessionJwt = client.getSessionJwt(); 100 | 101 | var accountScope = sessionJwt.body.scope.account; 102 | 103 | if (!accountScope) { 104 | return null; 105 | } 106 | 107 | var accountId = Object.keys(accountScope)[0]; 108 | var accountHref = client.baseurl + '/v1/accounts/' + accountId; 109 | 110 | return { 111 | href: accountHref 112 | }; 113 | }; 114 | 115 | this.login = function login(data,cb){ 116 | var options = {}; 117 | 118 | if (client.requireMfa) { 119 | options.redirect = false; 120 | } 121 | 122 | client.login(data, options, function (err, response) { 123 | $rootScope.$apply(function(){ 124 | if(err){ 125 | if(err.serviceProviderCallbackUrl){ 126 | serviceProviderRedirect(err.serviceProviderCallbackUrl); 127 | }else{ 128 | cb(err); 129 | } 130 | return; 131 | } 132 | 133 | if (response && response.serviceProviderCallbackUrl) { 134 | var callbackJwt = getJwtFromCallbackUri(response.serviceProviderCallbackUrl); 135 | return self.ssoEndpointRedirect(callbackJwt); 136 | } 137 | 138 | var sessionJwt = client.getSessionJwt(); 139 | 140 | if (!sessionJwt) { 141 | return cb(new Error('Login failed. Did not receive a session JWT.')); 142 | } 143 | 144 | if (client.requireMfa) { 145 | var action = 'setup'; 146 | 147 | // If we have factors, then redirect to verification. 148 | if (Object.keys(sessionJwt.body.scope.factor || {}).length > 0) { 149 | action = 'verify'; 150 | } 151 | 152 | return $location.path('/mfa/' + action); 153 | } 154 | 155 | self.ssoEndpointRedirect(sessionJwt.toString()); 156 | }); 157 | }); 158 | }; 159 | 160 | this.register = function register(data,cb){ 161 | client.register(data,function(err,response){ 162 | $rootScope.$apply(function(){ 163 | if(err){ 164 | if(err.serviceProviderCallbackUrl){ 165 | serviceProviderRedirect(err.serviceProviderCallbackUrl); 166 | }else{ 167 | cb(err); 168 | } 169 | return; 170 | } 171 | 172 | var sessionJwt = client.getSessionJwt(); 173 | 174 | if (sessionJwt && sessionJwt.body.require_mfa) { 175 | return $location.path('/mfa/setup/'); 176 | } 177 | 178 | if(response && response.serviceProviderCallbackUrl){ 179 | var jwt = getJwtFromCallbackUri(response.serviceProviderCallbackUrl); 180 | return self.ssoEndpointRedirect(jwt); 181 | } 182 | 183 | self.isRegistered = true; 184 | 185 | $location.path('/unverified'); 186 | }); 187 | }); 188 | }; 189 | 190 | this.verifyEmailToken = function verifyEmailToken(cb){ 191 | client.verifyEmailToken(function(err){ 192 | $rootScope.$apply(function(){ 193 | self.isVerified = err ? false : true; 194 | cb(err); 195 | }); 196 | }); 197 | }; 198 | 199 | this.verifyPasswordToken = function verifyPasswordToken(cb){ 200 | client.verifyPasswordResetToken(function(err, resp) { 201 | $rootScope.$apply(function(){ 202 | cb(err,resp); 203 | }); 204 | }); 205 | }; 206 | 207 | this.sendPasswordResetEmail = function sendPasswordResetEmail(email,cb){ 208 | client.sendPasswordResetEmail(email,function(err) { 209 | $rootScope.$apply(function(){ 210 | if(err){ 211 | if(err.serviceProviderCallbackUrl){ 212 | serviceProviderRedirect(err.serviceProviderCallbackUrl); 213 | }else{ 214 | cb(err); 215 | } 216 | }else{ 217 | cb(); 218 | } 219 | }); 220 | }); 221 | }; 222 | 223 | this.setNewPassword = function setNewPassword(pwTokenVerification,newPassword,cb){ 224 | client.setAccountPassword(pwTokenVerification,newPassword,function(err, resp) { 225 | $rootScope.$apply(function(){ 226 | cb(err,resp); 227 | }); 228 | }); 229 | }; 230 | 231 | this.getFactors = function getFactors(account, callback){ 232 | client.getFactors(account, function (err, response) { 233 | $rootScope.$apply(function () { 234 | callback(err, response); 235 | }); 236 | }); 237 | }; 238 | 239 | this.challengeFactor = function challengeFactor(factor, challenge, callback){ 240 | client.challengeFactor(factor, challenge, function (err, response) { 241 | $rootScope.$apply(function () { 242 | callback(err, response); 243 | }); 244 | }); 245 | }; 246 | 247 | this.createFactor = function createFactor(account, data, callback){ 248 | client.createFactor(account, data, function (err, response) { 249 | $rootScope.$apply(function () { 250 | callback(err, response); 251 | }); 252 | }); 253 | }; 254 | 255 | this.createChallenge = function createFactor(factor, data, callback){ 256 | client.createChallenge(factor, data, function (err, response) { 257 | $rootScope.$apply(function () { 258 | callback(err, response); 259 | }); 260 | }); 261 | }; 262 | 263 | this.updateChallenge = function createFactor(challenge, data, callback){ 264 | client.updateChallenge(challenge, data, function (err, response) { 265 | $rootScope.$apply(function () { 266 | callback(err, response); 267 | }); 268 | }); 269 | }; 270 | 271 | this.getOrganizationNameKey = function getOrganizationNameKey(){ 272 | return client.jwtPayload.asnk || ''; 273 | }; 274 | 275 | this.getProvider = function getProvider(providerId){ 276 | var r = self.providers.filter(function(p){ 277 | return p.providerId === providerId; 278 | }); 279 | return r.length === 1 ? r[0]:null; 280 | }; 281 | 282 | this.makeId = function makeId(length) { 283 | length = length || 5; 284 | var text = ''; 285 | var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 286 | 287 | for (var i = 0; i < length; i++) { 288 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 289 | } 290 | return text; 291 | }; 292 | 293 | initialize(); 294 | 295 | return this; 296 | }); 297 | -------------------------------------------------------------------------------- /test/protractor/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var express = require('express'); 5 | var ngrok = require('ngrok'); 6 | var path = require('path'); 7 | var request = require('request'); 8 | var stormpath = require('stormpath'); 9 | var uuid = require('node-uuid'); 10 | 11 | require('colors'); 12 | 13 | var client = new stormpath.Client({ 14 | apiKey: new stormpath.ApiKey( 15 | process.env.STORMPATH_CLIENT_APIKEY_ID, 16 | process.env.STORMPATH_CLIENT_APIKEY_SECRET 17 | ) 18 | }); 19 | 20 | var ready = false; 21 | 22 | var resources = { 23 | application: null, 24 | directory: null, 25 | loginAccount: null, 26 | googleDirectory: null, 27 | facebookDirectory: null 28 | }; 29 | 30 | function createAccount(directory,cb){ 31 | console.log('Create Account'.blue); 32 | var newAccount = { 33 | email: 'nobody+'+uuid()+'@stormpath.com', 34 | givenName: uuid(), 35 | surname: uuid(), 36 | password: uuid() + uuid().toUpperCase() 37 | }; 38 | directory.createAccount(newAccount,function(err,account){ 39 | if(err){ 40 | throw err; 41 | }else{ 42 | account.password = newAccount.password; 43 | cb(null,account); 44 | } 45 | }); 46 | } 47 | 48 | function createDirectory(client,directoryConfig,cb){ 49 | console.log(('Create Directory ' + directoryConfig.name).blue); 50 | 51 | client.createDirectory(directoryConfig,function(err,directory){ 52 | if(err){ 53 | throw err; 54 | }else{ 55 | cb(null,directory); 56 | } 57 | }); 58 | } 59 | 60 | function createGoogleDirectory(client,cb) { 61 | var directoryConfig = { 62 | name:'protractor-test-id-site-'+uuid(), 63 | provider: { 64 | providerId: 'google', 65 | clientId: uuid(), 66 | clientSecret: uuid(), 67 | redirectUri: uuid() 68 | } 69 | }; 70 | createDirectory(client,directoryConfig,cb); 71 | } 72 | 73 | function createFacebookDirectory(client,cb) { 74 | var directoryConfig = { 75 | name:'protractor-test-id-site-'+uuid(), 76 | provider: { 77 | providerId: 'facebook', 78 | clientId: uuid(), 79 | clientSecret: uuid(), 80 | redirectUri: uuid() 81 | } 82 | }; 83 | createDirectory(client,directoryConfig,cb); 84 | } 85 | 86 | function createSamlDirectory(client, cb) { 87 | var directoryData = { 88 | name:'protractor-test-id-site-'+uuid(), 89 | provider: { 90 | providerId: 'saml', 91 | ssoLogoutUrl: 'https://stormpathsaml-dev-ded.my.salesforce.com/idp/endpoint/HttpRedirect', 92 | ssoLoginUrl: 'https://stormpathsaml-dev-ded.my.salesforce.com/idp/endpoint/HttpRedirect', 93 | encodedX509SigningCert: '-----BEGIN CERTIFICATE-----\nMIIErDCCA5SgAwIBAgIOAVGSh6YMAAAAAHJWVEswDQYJKoZIhvcNAQELBQAwgZAx\nKDAmBgNVBAMMH1NlbGZTaWduZWRDZXJ0XzExRGVjMjAxNV8xOTMyMjExGDAWBgNV\nBAsMDzAwRDE1MDAwMDAwR1lzVTEXMBUGA1UECgwOU2FsZXNmb3JjZS5jb20xFjAU\nBgNVBAcMDVNhbiBGcmFuY2lzY28xCzAJBgNVBAgMAkNBMQwwCgYDVQQGEwNVU0Ew\nHhcNMTUxMjExMTkzMjIyWhcNMTcxMjExMTIwMDAwWjCBkDEoMCYGA1UEAwwfU2Vs\nZlNpZ25lZENlcnRfMTFEZWMyMDE1XzE5MzIyMTEYMBYGA1UECwwPMDBEMTUwMDAw\nMDBHWXNVMRcwFQYDVQQKDA5TYWxlc2ZvcmNlLmNvbTEWMBQGA1UEBwwNU2FuIEZy\nYW5jaXNjbzELMAkGA1UECAwCQ0ExDDAKBgNVBAYTA1VTQTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAJ5wNURCyRpekSFKMVZ95hDvLxOdxvb+YlP6b+Xj\nY5TdRVGlJ7JL5vMtOKfcJmP9uDEwN//Iv/CC2+LDwNrwmoPxvcL7IZHRAYkLLKZu\nRtv54DzIQkJ26sQ4hHvXDF5Rb+eus+JhEebN+gbwmfcSlbXOQ5/se1xeTZFkYGDj\n9Myk8im3FQISbU1viuXqC199RsnQbPcOiLcauPvPBlm/WX335ful25Ebtj+4eecV\nFslLXXPtPkXgNlteeS/Ez70viHYhK8TE6dQOFm6Xk6m5mogX8d1h3IXe3NB81NVI\nGhdQog7pSBBlfYbST8PEX0+KsWXV/HZTU+2Jg+tbTRhMBRcCAwEAAaOCAQAwgf0w\nHQYDVR0OBBYEFESEZhu31Qufyz+0oCDTBCZIRi/UMA8GA1UdEwEB/wQFMAMBAf8w\ngcoGA1UdIwSBwjCBv4AURIRmG7fVC5/LP7SgINMEJkhGL9ShgZakgZMwgZAxKDAm\nBgNVBAMMH1NlbGZTaWduZWRDZXJ0XzExRGVjMjAxNV8xOTMyMjExGDAWBgNVBAsM\nDzAwRDE1MDAwMDAwR1lzVTEXMBUGA1UECgwOU2FsZXNmb3JjZS5jb20xFjAUBgNV\nBAcMDVNhbiBGcmFuY2lzY28xCzAJBgNVBAgMAkNBMQwwCgYDVQQGEwNVU0GCDgFR\nkoemDAAAAAByVlRLMA0GCSqGSIb3DQEBCwUAA4IBAQAWgBu9o6lXwr82waBMI8AA\ne+75j+gLXiOdKQK9KXw4XVCSZQsJeaiMapCDIPCCORXfsnQjQUxdN1vKwHM/rV6x\n5GFrWrVfabM5n/p/qTWz3qlawTxPZv1WyTF6UeLCAmsdfBZE29E5GgFZ9LYHW1qh\n09yT+vksFYM4caQPT12eXiEC6uKH9un3D3qhos3LxczER64OkBV1D/IP2H20n9nn\nsuXL6rHgRs3Aii4xmjc6CU9YIRkQ7gE0oYsOujPAHslD/ej6mjJn2XXnjgDB/T5d\nlM/sEIxEPbc4J1Hca8bDNoY+siunoYgqMye+xYobMJCfXACIvzOBx4kjvohVM43U\n-----END CERTIFICATE-----', 94 | requestSignatureAlgorithm: 'RSA-SHA256' 95 | } 96 | } 97 | createDirectory(client,directoryData,cb); 98 | } 99 | 100 | function createApplication(client,cb){ 101 | console.log('Create Application'.blue); 102 | client.createApplication( 103 | {name:'protractor-test-id-site-'+uuid()}, 104 | function(err,application){ 105 | if(err){ 106 | throw err; 107 | }else{ 108 | cb(null,application); 109 | } 110 | } 111 | ); 112 | } 113 | 114 | 115 | function prepeareIdSiteModel(client,currentHost,callbackUri,cb){ 116 | console.log('WARNING! I am modifying your ID Site Configuration'.yellow); 117 | console.log('\tI am adding ', (resources.appHost+'').green , ' to your ', 'authorizedOriginUris'.yellow); 118 | console.log('\tI am adding ', (callbackUri).green , ' to your ', 'authorizedRedirectUris'.yellow); 119 | console.log('\tI am adding ', (browser.params.logoUrl+'').green , ' to your ', 'logoUrl'.yellow); 120 | console.log('You want to remove these values from your ID Site Configuration before you launch a production application'.yellow); 121 | console.log('If you need a sandbox environment for testing ID Site, please contact support'.yellow); 122 | client.getCurrentTenant(function(err,tenant){ 123 | if(err){ 124 | throw err; 125 | }else{ 126 | client.getResource(tenant.href + '/idSites',function(err,collection){ 127 | if(err){ 128 | throw err; 129 | }else{ 130 | var idSiteModel = collection.items[0]; 131 | if(idSiteModel.authorizedOriginUris.indexOf(currentHost) === -1){ 132 | idSiteModel.authorizedOriginUris.push(currentHost); 133 | } 134 | if(idSiteModel.authorizedRedirectUris.indexOf(callbackUri) === -1){ 135 | idSiteModel.authorizedRedirectUris.push(callbackUri); 136 | } 137 | 138 | idSiteModel.logoUrl = browser.params.logoUrl; 139 | idSiteModel.save(function(err){ 140 | if(err){ 141 | throw err; 142 | }else{ 143 | cb(); 144 | } 145 | }); 146 | } 147 | }); 148 | } 149 | }); 150 | } 151 | 152 | function getJwtUrl(path,cb){ 153 | var uri = resources.appHost + '/stormpathCallback'; 154 | 155 | var url = resources.application.createIdSiteUrl({ 156 | callbackUri: uri, 157 | path: path 158 | }); 159 | 160 | request(url,{ 161 | followRedirect: false 162 | },function(err,res,body){ 163 | if(err){ 164 | throw err; 165 | }else if(res.statusCode!==302){ 166 | throw new Error(body&&body.message || JSON.stringify(body||'Unknown error')); 167 | }else{ 168 | var fragment = res.headers.location.split('/#')[1]; 169 | var url = resources.appHost + '#' + fragment; 170 | cb(url); 171 | } 172 | }); 173 | 174 | } 175 | function deleteResource(resource,cb){ 176 | console.log(('Delete').blue,resource.href); 177 | resource.delete(cb); 178 | } 179 | 180 | function cleanup(cb){ 181 | console.log('Begin Cleanup'.blue); 182 | 183 | var toDelete = Object.keys(resources) 184 | .filter(function(key){ 185 | return !!resources[key].href; // only pull our resources that we created 186 | }) 187 | .map(function(key){ 188 | return resources[key]; 189 | }); 190 | 191 | // Sort the application resource to the end, as it should be deleted last 192 | toDelete.sort(function(a,b){ 193 | var _a = a.href.match(/applications/); 194 | var _b = b.href.match(/applications/); 195 | return ( _a === _b ) ? 0 : ( _a ? 1 : -1); 196 | }); 197 | 198 | async.eachSeries(toDelete,deleteResource,function(err){ 199 | if(err){ 200 | throw err; 201 | }else{ 202 | console.log('Cleanup Complete'.blue); 203 | cb(); 204 | } 205 | }); 206 | } 207 | 208 | function mapDirectory(application,directory,isDefaultAccountStore,cb) { 209 | var mapping = { 210 | application: { 211 | href: application.href 212 | }, 213 | accountStore: { 214 | href: directory.href 215 | }, 216 | isDefaultAccountStore: isDefaultAccountStore 217 | }; 218 | 219 | application.createAccountStoreMapping(mapping, function(err, mapping){ 220 | if(err){ 221 | throw err; 222 | }else{ 223 | cb(mapping); 224 | } 225 | }); 226 | } 227 | 228 | function startAppServer(done){ 229 | var app = express(); 230 | app.use(express.static(path.join(__dirname, '..','..','dist'))); 231 | app.get('/stormpathCallback', function(req,res){ 232 | res.json(req.query || {}); 233 | }); 234 | console.log('Starting Asset Server'); 235 | var server = app.listen(0,function(){ 236 | console.log('Listening on port ' + server.address().port); 237 | 238 | ngrok.connect(server.address().port, function(err, url) { 239 | console.log(url) 240 | resources.appHost = url; 241 | done(); 242 | }); 243 | 244 | }); 245 | } 246 | 247 | async.parallel({ 248 | app: startAppServer, 249 | googleDirectory: createGoogleDirectory.bind(null,client), 250 | facebookDirectory: createFacebookDirectory.bind(null,client), 251 | samlDirectory: createSamlDirectory.bind(null,client), 252 | application: createApplication.bind(null,client), 253 | directory: createDirectory.bind(null,client,{name:'protractor-test-id-site-'+uuid()}) 254 | },function(err,results) { 255 | if(err){ 256 | throw err; 257 | }else{ 258 | resources.googleDirectory = results.googleDirectory; 259 | resources.facebookDirectory = results.facebookDirectory; 260 | resources.application = results.application; 261 | resources.directory = results.directory; 262 | resources.samlDirectory = results.samlDirectory; 263 | 264 | async.parallel({ 265 | idSiteModel: prepeareIdSiteModel.bind(null,client,resources.appHost,resources.appHost + '/stormpathCallback'), 266 | account: createAccount.bind(null,resources.directory) 267 | },function(err,results) { 268 | if(err){ 269 | throw err; 270 | }else{ 271 | resources.loginAccount = results.account; 272 | ready = true; 273 | } 274 | }); 275 | 276 | } 277 | }); 278 | 279 | module.exports = { 280 | createApplication: createApplication, 281 | createAccount: createAccount, 282 | deleteResource: deleteResource, 283 | ready: function(){ 284 | return ready; 285 | }, 286 | resources: resources, 287 | getJwtUrl: getJwtUrl, 288 | cleanup: cleanup, 289 | getDirectory: function(){ 290 | return resources.directory; 291 | }, 292 | getLoginAccount: function(){ 293 | return resources.loginAccount; 294 | }, 295 | mapDirectory: mapDirectory 296 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-05-01 using generator-angular 0.8.0 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | // 10 | var untildify = require('untildify'); 11 | 12 | module.exports = function (grunt) { 13 | 14 | // Load grunt tasks automatically 15 | require('load-grunt-tasks')(grunt); 16 | 17 | // Time how long tasks take. Can help when optimizing build times 18 | require('time-grunt')(grunt); 19 | 20 | // Define the configuration for all the tasks 21 | grunt.initConfig({ 22 | pkg: grunt.file.readJSON('package.json'), 23 | hostname: process.env.SSO_DEV_HOST || 'localhost', 24 | port: process.env.SSO_DEV_PORT || 9000, 25 | year: new Date().getFullYear(), 26 | // Project settings 27 | yeoman: { 28 | // configurable paths 29 | app: require('./bower.json').appPath || 'app', 30 | dist: 'dist' 31 | }, 32 | 33 | // Watches files for changes and runs tasks based on the changed files 34 | watch: { 35 | bower: { 36 | files: ['bower.json'], 37 | tasks: ['bowerInstall'] 38 | }, 39 | js: { 40 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 41 | tasks: ['newer:jshint:all','karma:liveunit:run'], 42 | options: { 43 | livereload: true 44 | } 45 | }, 46 | jsTest: { 47 | files: ['test/spec/{,*/}*.js'], 48 | tasks: ['newer:jshint:test', 'karma:liveunit:run'] 49 | }, 50 | styles: { 51 | files: ['<%= yeoman.app %>/styles/{,*/}*.{less,css}'], 52 | tasks: ['less','postcss'] 53 | }, 54 | gruntfile: { 55 | files: ['Gruntfile.js'] 56 | }, 57 | html: { 58 | files: ['<%= yeoman.app %>/{,*/}*.html'], 59 | tasks: ['includes:html'] 60 | }, 61 | livereload: { 62 | options: { 63 | livereload: '<%= connect.options.livereload %>' 64 | }, 65 | files: [ 66 | '<%= yeoman.app %>/{,*/}*.html', 67 | '.tmp/styles/{,*/}*.css', 68 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 69 | ] 70 | } 71 | }, 72 | 73 | // The actual grunt server settings 74 | connect: { 75 | options: { 76 | port: '<%=port %>', 77 | // Change this to '0.0.0.0' to access the server from outside. 78 | hostname: '0.0.0.0', 79 | livereload: 35729 80 | }, 81 | livereload: { 82 | options: { 83 | open: 'http://<%=hostname %>:<%=port %>', 84 | base: [ 85 | '.tmp', 86 | '<%= yeoman.app %>' 87 | ] 88 | } 89 | }, 90 | test: { 91 | options: { 92 | port: 9001, 93 | base: [ 94 | '.tmp', 95 | 'test', 96 | '<%= yeoman.app %>' 97 | ] 98 | } 99 | }, 100 | dist: { 101 | options: { 102 | base: '<%= yeoman.dist %>' 103 | } 104 | } 105 | }, 106 | 107 | less: { 108 | development:{ 109 | files: { 110 | '.tmp/styles/main.css':'<%= yeoman.app %>/styles/main.less' 111 | } 112 | } 113 | }, 114 | 115 | // Make sure code styles are up to par and there are no obvious mistakes 116 | jshint: { 117 | options: { 118 | jshintrc: '.jshintrc', 119 | reporter: require('jshint-stylish') 120 | }, 121 | all: [ 122 | 'Gruntfile.js', 123 | '<%= yeoman.app %>/scripts/{,*/}*.js' 124 | ], 125 | test: { 126 | options: { 127 | jshintrc: 'test/.jshintrc' 128 | }, 129 | src: ['test/spec/{,*/}*.js'] 130 | } 131 | }, 132 | 133 | // Empties folders to start fresh 134 | clean: { 135 | dist: { 136 | files: [{ 137 | dot: true, 138 | src: [ 139 | '.tmp', 140 | '<%= yeoman.dist %>/*', 141 | '!<%= yeoman.dist %>/.git*' 142 | ] 143 | }] 144 | }, 145 | server: '.tmp' 146 | }, 147 | 148 | // Add vendor prefixed styles 149 | postcss: { 150 | options: { 151 | processors: [ 152 | require('autoprefixer')({browsers: ['last 3 versions']}) 153 | ] 154 | }, 155 | dist: { 156 | files: [{ 157 | expand: true, 158 | cwd: '.tmp/styles/', 159 | src: '{,*/}*.css', 160 | dest: '.tmp/styles/' 161 | }] 162 | } 163 | }, 164 | 165 | // Automatically inject Bower components into the app 166 | bowerInstall: { 167 | app: { 168 | src: ['<%= yeoman.app %>/index.html'], 169 | ignorePath: '<%= yeoman.app %>/' 170 | } 171 | }, 172 | 173 | // Renames files for browser caching purposes 174 | rev: { 175 | dist: { 176 | files: { 177 | src: [ 178 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 179 | '<%= yeoman.dist %>/styles/{,*/}*.css', 180 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 181 | '<%= yeoman.dist %>/styles/fonts/*' 182 | ] 183 | } 184 | } 185 | }, 186 | 187 | // Reads HTML for usemin blocks to enable smart builds that automatically 188 | // concat, minify and revision files. Creates configurations in memory so 189 | // additional tasks can operate on them 190 | useminPrepare: { 191 | dist:{ 192 | src: '<%= yeoman.app %>/index.html', 193 | options: { 194 | dest: '<%= yeoman.dist %>', 195 | flow: { 196 | html: { 197 | steps: { 198 | js: ['concat', 'uglifyjs'], 199 | css: ['cssmin'] 200 | }, 201 | post: {} 202 | } 203 | } 204 | } 205 | }, 206 | debug:{ 207 | src: '<%= yeoman.app %>/index.html', 208 | options: { 209 | dest: '<%= yeoman.dist %>', 210 | flow: { 211 | html: { 212 | steps: { 213 | js: ['concat'], 214 | css: ['cssmin'] 215 | }, 216 | post: {} 217 | } 218 | } 219 | } 220 | } 221 | }, 222 | 223 | // Performs rewrites based on rev and the useminPrepare configuration 224 | usemin: { 225 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 226 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 227 | options: { 228 | assetsDirs: ['<%= yeoman.dist %>'] 229 | } 230 | }, 231 | 232 | // The following *-min tasks produce minified files in the dist folder 233 | cssmin: { 234 | options: { 235 | root: '<%= yeoman.app %>' 236 | } 237 | }, 238 | 239 | imagemin: { 240 | dist: { 241 | files: [{ 242 | expand: true, 243 | cwd: '<%= yeoman.app %>/images', 244 | src: '{,*/}*.{png,jpg,jpeg,gif}', 245 | dest: '<%= yeoman.dist %>/images' 246 | }] 247 | } 248 | }, 249 | 250 | svgmin: { 251 | dist: { 252 | files: [{ 253 | expand: true, 254 | cwd: '<%= yeoman.app %>/images', 255 | src: '{,*/}*.svg', 256 | dest: '<%= yeoman.dist %>/images' 257 | }] 258 | } 259 | }, 260 | 261 | htmlmin: { 262 | dist: { 263 | options: { 264 | collapseWhitespace: true, 265 | collapseBooleanAttributes: true, 266 | removeCommentsFromCDATA: true, 267 | removeOptionalTags: true 268 | }, 269 | files: [{ 270 | expand: true, 271 | cwd: '<%= yeoman.dist %>', 272 | src: ['*.html', 'views/{,*/}*.html'], 273 | dest: '<%= yeoman.dist %>' 274 | }] 275 | } 276 | }, 277 | 278 | // ngmin tries to make the code safe for minification automatically by 279 | // using the Angular long form for dependency injection. It doesn't work on 280 | // things like resolve or inject so those have to be done manually. 281 | ngAnnotate: { 282 | dist: { 283 | files: [{ 284 | expand: true, 285 | cwd: '.tmp/concat/scripts', 286 | src: '*.js', 287 | dest: '.tmp/concat/scripts' 288 | }] 289 | } 290 | }, 291 | 292 | // Replace Google CDN references 293 | cdnify: { 294 | dist: { 295 | html: ['<%= yeoman.dist %>/*.html'] 296 | } 297 | }, 298 | 299 | // Copies remaining files to places other tasks can use 300 | copy: { 301 | dist: { 302 | files: [{ 303 | expand: true, 304 | dot: true, 305 | cwd: '<%= yeoman.app %>', 306 | dest: '<%= yeoman.dist %>', 307 | src: [ 308 | '*.{ico,png,txt}', 309 | 'images/{,*/}*.{webp}', 310 | 'fonts/*' 311 | ] 312 | }, { 313 | expand: true, 314 | cwd: '.tmp/images', 315 | dest: '<%= yeoman.dist %>/images', 316 | src: ['generated/*'] 317 | },{ 318 | expand: true, 319 | cwd: '.tmp', 320 | dest: '<%= yeoman.dist %>', 321 | src: ['*.html'] 322 | },{ 323 | expand: true, 324 | cwd: '<%= yeoman.app %>', 325 | dest: '<%= yeoman.dist %>', 326 | src: ['error.html'] 327 | }] 328 | }, 329 | less: { 330 | expand: true, 331 | cwd: '<%= yeoman.app %>/styles', 332 | dest: '.tmp/styles/', 333 | src: '{,*/}*.{less}' 334 | }, 335 | debug: { 336 | expand: true, 337 | cwd: '.tmp/concat/scripts', 338 | dest: '<%= yeoman.dist %>/scripts', 339 | src: '{,*/}*.js' 340 | }, 341 | dest:{ 342 | cwd: '<%= yeoman.dist %>', 343 | src: '**/*', 344 | dest: grunt.option('dest') ? untildify(grunt.option('dest')):'', 345 | expand: true 346 | }, 347 | }, 348 | includes: { 349 | html:{ 350 | 351 | src: ['<%= yeoman.app %>/index.html'], // Source files 352 | dest: '.tmp/index.html', // Destination directory 353 | options: { 354 | // flatten: true, 355 | silent: false, 356 | banner: '' 357 | } 358 | } 359 | }, 360 | 361 | // Run some tasks in parallel to speed up the build process 362 | concurrent: { 363 | server: [ 364 | 'copy:less', 365 | 'less', 366 | 'includes:html' 367 | ], 368 | test: [ 369 | 'copy:less' 370 | ], 371 | dist: [ 372 | 'copy:less', 373 | 'less', 374 | 'includes:html', 375 | 'imagemin', 376 | 'svgmin' 377 | ] 378 | }, 379 | 380 | protractor: { 381 | options: { 382 | configFile: 'protractor.conf.js', // Default config file 383 | keepAlive: false, // If false, the grunt process stops when the test fails. 384 | noColor: false, // If true, protractor will not use colors in its output. 385 | }, 386 | dev: { 387 | options: { 388 | args: { 389 | baseUrl: 'http://<%=hostname %>:<%=port %>', 390 | params: { 391 | appUrl: '/', 392 | apiUrl: 'http://fakeapi.stormpath.com:1337' 393 | } 394 | } 395 | } 396 | }, 397 | sauce: { 398 | options: { 399 | configFile: 'protractor.conf.sauce.js', 400 | args: { 401 | sauceUser: process.env.SAUCE_USER, 402 | sauceKey: process.env.SAUCE_API_KEY, 403 | baseUrl: 'http://<%=hostname %>:<%=port %>', 404 | params: { 405 | appUrl: '/', 406 | apiUrl: 'http://fakeapi.stormpath.com:1337' 407 | } 408 | } 409 | } 410 | } 411 | }, 412 | 413 | // By default, your `index.html`'s will take care of 414 | // minification. These next options are pre-configured if you do not wish 415 | // to use the Usemin blocks. 416 | // cssmin: { 417 | // dist: { 418 | // files: { 419 | // '<%= yeoman.dist %>/styles/main.css': [ 420 | // '.tmp/styles/{,*/}*.css', 421 | // '<%= yeoman.app %>/styles/{,*/}*.css' 422 | // ] 423 | // } 424 | // } 425 | // }, 426 | // uglify: { 427 | // dist: { 428 | // files: { 429 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 430 | // '<%= yeoman.dist %>/scripts/scripts.js' 431 | // ] 432 | // } 433 | // } 434 | // }, 435 | concat: { 436 | dist: { 437 | options: { 438 | banner: '/*\n' + 439 | ' Stormpath ID Site v<%= pkg.version %>\n' + 440 | ' (c) 2014-<%= year %> Stormpath, Inc. http://stormpath.com\n'+ 441 | ' License: Apache 2.0\n' + 442 | '*/\n', 443 | }, 444 | files: { 445 | 'dist/scripts/app.js': ['dist/scripts/app.js'], 446 | 'dist/scripts/vendor.js': ['dist/scripts/vendor.js'] 447 | } 448 | } 449 | }, 450 | 451 | // Test settings 452 | 453 | karma: { 454 | unit: { 455 | configFile: 'karma.conf.js', 456 | singleRun: true 457 | }, 458 | liveunit: { // starts a karma server in the background 459 | configFile: 'karma.conf.js', 460 | background: true 461 | } 462 | }, 463 | }); 464 | 465 | 466 | grunt.registerTask('serve', function (target) { 467 | if (target === 'dist') { 468 | return grunt.task.run(['build', 'connect:dist:keepalive']); 469 | } 470 | 471 | grunt.task.run([ 472 | 'clean:server', 473 | 'bowerInstall', 474 | 'concurrent:server', 475 | 'postcss', 476 | 'connect:livereload', 477 | // 'karma:liveunit', // disabled until karama test runner is fixed 478 | 'watch' 479 | ]); 480 | }); 481 | 482 | grunt.registerTask('server', function (target) { 483 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 484 | grunt.task.run(['serve:' + target]); 485 | }); 486 | 487 | grunt.registerTask('test', [ 488 | 'clean:server', 489 | 'concurrent:test', 490 | 'postcss', 491 | 'connect:test', 492 | 'karma' 493 | ]); 494 | 495 | grunt.registerTask('build', 'Build the project, assets are placed in dist/', 496 | [ 497 | 'clean:dist', 498 | 'bowerInstall', 499 | 'useminPrepare:dist', 500 | 'concurrent:dist', 501 | 'postcss', 502 | 'concat', 503 | 'ngAnnotate', 504 | 'copy:dist', 505 | 'cdnify', 506 | 'cssmin', 507 | 'uglify', 508 | // 'rev', 509 | 'usemin', 510 | // 'htmlmin' 511 | 'concat:dist' 512 | ] 513 | ); 514 | 515 | grunt.registerTask('build:debug', [ 516 | 'clean:dist', 517 | 'bowerInstall', 518 | 'useminPrepare:debug', 519 | 'concurrent:dist', 520 | 'postcss', 521 | 'concat', 522 | 'ngAnnotate', 523 | 'copy:dist', 524 | 'cdnify', 525 | 'cssmin', 526 | 'usemin', 527 | 'copy:debug', 528 | 'concat:dist' 529 | ]); 530 | 531 | grunt.registerTask('default', [ 532 | 'newer:jshint', 533 | 'test', 534 | 'build' 535 | ]); 536 | 537 | grunt.registerTask('dist', 538 | 'Use this task to build a distro and copy it to the directoroy sepcified by --dist on the command line', 539 | function(){ 540 | if(grunt.option('dest')){ 541 | return grunt.task.run(['build','copy:dest']); 542 | }else{ 543 | throw new Error('Must specify destination with --dest option'); 544 | } 545 | } 546 | ); 547 | }; 548 | -------------------------------------------------------------------------------- /app/scripts/app-mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | 5 | 6 | function MockStormpath(){ 7 | function uuid(){ 8 | return Math.floor(Math.random()*10000); 9 | } 10 | function CsrfResponse(appHref){ 11 | return { 12 | appHref: appHref, 13 | csrfToken: uuid(), 14 | hpValue: uuid(), 15 | expires: new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 16 | }; 17 | } 18 | 19 | var ssoConfigs = { 20 | 'https://api.stormpath.com/v1/applications/1234': { 21 | href: 'https://api.stormpath.com/v1/applications/1234/idSiteModel', 22 | socialProviders: { 23 | googleClientId: '279686489820-bm1m1kd1dbdojvhmh4phhr6aofj95933.apps.googleusercontent.com', 24 | facebookAppId: '711511582223538' 25 | }, 26 | passwordPolicy: { 27 | minLength: 8, 28 | maxLength: 100, 29 | requireLowerCase: true, 30 | requireUpperCase: true, 31 | requireNumeric: true, 32 | requireDiacritical: false, 33 | special: false 34 | }, 35 | logoUrl: 'images/logo.png' 36 | }, 37 | 'https://api.stormpath.com/v1/applications/2': { 38 | href: 'https://api.stormpath.com/v1/applications/2/idSiteModel', 39 | socialProviders: { 40 | googleClientId: null, 41 | facebookAppId: null, 42 | }, 43 | passwordPolicy: { 44 | minLength: 8, 45 | maxLength: 20, 46 | requireLowerCase: true, 47 | requireUpperCase: true, 48 | requireNumeric: true, 49 | requireDiacritical: true, 50 | special: false 51 | }, 52 | logoUrl: null 53 | }, 54 | 'https://api.stormpath.com/v1/applications/3': { 55 | href: 'https://api.stormpath.com/v1/applications/3/idSiteModel', 56 | socialProviders: { 57 | googleClientId: null, 58 | facebookAppId: '711511582223538', 59 | }, 60 | passwordPolicy: { 61 | minLength: 0, 62 | maxLength: 255, 63 | requireLowerCase: true, 64 | requireUpperCase: true, 65 | requireNumeric: true, 66 | requireDiacritical: false, 67 | special: false 68 | }, 69 | logoUrl: 'images/logo.png' 70 | }, 71 | 'https://api.stormpath.com/v1/applications/4': { 72 | href: 'https://api.stormpath.com/v1/applications/4/idSiteModel', 73 | socialProviders: { 74 | googleClientId: '279686489820-bm1m1kd1dbdojvhmh4phhr6aofj95933.apps.googleusercontent.com', 75 | facebookAppId: null, 76 | }, 77 | passwordPolicy: { 78 | minLength: 0, 79 | maxLength: 255, 80 | requireLowerCase: true, 81 | requireUpperCase: true, 82 | requireNumeric: true, 83 | requireDiacritical: false 84 | }, 85 | logoUrl: 'images/logo.png' 86 | } 87 | }; 88 | 89 | function respondWithApplication(appHref,xhr){ 90 | 91 | var response = { 92 | 'href' : appHref, 93 | 'loginAttempts': { 94 | href: appHref + '/loginAttempts' 95 | }, 96 | 'accounts': { 97 | href: appHref + '/accounts' 98 | }, 99 | 'passwordResetTokens': { 100 | href: appHref + '/passwordResetTokens' 101 | }, 102 | idSiteModel: ssoConfigs[appHref] 103 | }; 104 | xhr.respond( 105 | 200, 106 | {'Content-Type': 'application/json'}, 107 | JSON.stringify(response) 108 | ); 109 | } 110 | 111 | function respondWithLoginSuccess(accountHref,xhr){ 112 | var response = { 113 | 'account': { 114 | 'href' : accountHref 115 | }, 116 | 'redirectTo': 'https://stormpath.com', 117 | 'csrfToken': uuid(), 118 | 'hpValue': uuid(), 119 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 120 | }; 121 | xhr.respond( 122 | 200, 123 | { 124 | 'Content-Type': 'application/json', 125 | 'Stormpath-SSO-Redirect-Location': 'https://stormpath.com' 126 | }, 127 | JSON.stringify(response) 128 | ); 129 | } 130 | 131 | function respondWithLoginFailure(xhr){ 132 | var response = { 133 | 'status': 400, 134 | 'code': 400, 135 | 'message': 'Invalid username or password.', 136 | 'developerMessage': 'Invalid username or password.', 137 | 'moreInfo': 'mailto:support@stormpath.com', 138 | 'csrfToken': uuid(), 139 | 'hpValue': uuid(), 140 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 141 | }; 142 | xhr.respond( 143 | 400, 144 | {'Content-Type': 'application/json'}, 145 | JSON.stringify(response) 146 | ); 147 | } 148 | 149 | 150 | function respondWithNotFound(xhr){ 151 | var response = { 152 | status: 404, 153 | code: 404, 154 | message: 'The requested resource does not exist.', 155 | developerMessage: 'The requested resource does not exist.', 156 | moreInfo: 'mailto:support@stormpath.com', 157 | 'csrfToken': uuid(), 158 | 'hpValue': uuid(), 159 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 160 | }; 161 | xhr.respond( 162 | 404, 163 | {'Content-Type': 'application/json'}, 164 | JSON.stringify(response) 165 | ); 166 | } 167 | 168 | function respondWithNewAccount(account,verified,xhr){ 169 | var id = uuid(); 170 | var response = { 171 | 'href' : 'https://api.stormpath.com/v1/accounts/' + id, 172 | 'username' : account.username, 173 | 'email' : account.email || account.username, 174 | 'fullName' : account.givenName + ' ' + account.surname, 175 | 'givenName' : account.givenName, 176 | 'middleName' : '', 177 | 'surname' : account.surname, 178 | 'status' : verified? 'VERIFIED':'UNVERIFIED', 179 | 'customData': { 180 | 'href': 'https://api.stormpath.com/v1/accounts/'+id+'/customData' 181 | }, 182 | 'groups' : { 183 | 'href' : 'https://api.stormpath.com/v1/accounts/'+id+'/groups' 184 | }, 185 | 'groupMemberships' : { 186 | 'href' : 'https://api.stormpath.com/v1/accounts/'+id+'/groupMemberships' 187 | }, 188 | 'directory' : { 189 | 'href' : 'https://api.stormpath.com/v1/directories/' + uuid() 190 | }, 191 | 'tenant' : { 192 | 'href' : 'https://api.stormpath.com/v1/tenants/' + uuid() 193 | }, 194 | 'emailVerificationToken' : { 195 | 'href' : 'https://api.stormpath.com/v1/accounts/emailVerificationTokens/' + uuid() 196 | }, 197 | 'csrfToken': uuid(), 198 | 'hpValue': uuid(), 199 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 200 | }; 201 | 202 | var headers = { 203 | 'Content-Type': 'application/json' 204 | }; 205 | 206 | if(verified){ 207 | headers['Stormpath-SSO-Redirect-Location'] = 'https://stormpath.com'; 208 | } 209 | 210 | xhr.respond(201,headers,JSON.stringify(response)); 211 | } 212 | 213 | function respondWithDuplicateUser(xhr){ 214 | 215 | var response = { 216 | status: 409, 217 | code: 2001, 218 | userMessage: 'Account with that username already exists. Please choose another username.', 219 | developerMessage: 'Account with that username already exists. Please choose another username.', 220 | moreInfo: 'http://docs.stormpath.com/errors/2001', 221 | message: 'HTTP 409, Stormpath 2001 (http://docs.stormpath.com/errors/2001): Account with that username already exists. Please choose another username.', 222 | 'csrfToken': uuid(), 223 | 'hpValue': uuid(), 224 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 225 | }; 226 | xhr.respond( 227 | 409, 228 | {'Content-Type': 'application/json'}, 229 | JSON.stringify(response) 230 | ); 231 | } 232 | 233 | function respondWithOtherError(xhr){ 234 | var response = { 235 | status: 499, 236 | code: 499, 237 | userMessage: 'Something bad happened, and I\'m telling you about it', 238 | 'csrfToken': uuid(), 239 | 'hpValue': uuid(), 240 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 241 | }; 242 | xhr.respond( 243 | 499, 244 | {'Content-Type': 'application/json'}, 245 | JSON.stringify(response) 246 | ); 247 | } 248 | 249 | function respondWithPasswordTooShortError(xhr){ 250 | var response = { 251 | status: 400, 252 | code: 2007, 253 | userMessage: 'Account password minimum length not satisfied.', 254 | developerMessage: 'Account password minimum length not satisfied.', 255 | moreInfo: 'http://docs.stormpath.com/errors/2007', 256 | message: 'HTTP 400, Stormpath 2007 (http://docs.stormpath.com/errors/2007): Account password minimum length not satisfied.', 257 | 'csrfToken': uuid(), 258 | 'hpValue': uuid(), 259 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 260 | }; 261 | xhr.respond( 262 | response.status, 263 | {'Content-Type': 'application/json'}, 264 | JSON.stringify(response) 265 | ); 266 | } 267 | 268 | function respondWithPasswordRequiresUppercaseError(xhr){ 269 | var response = { 270 | status: 400, 271 | code: 400, 272 | userMessage: 'Password requires an uppercase character!', 273 | developerMessage: 'Password requires an uppercase character!', 274 | moreInfo: 'mailto:support@stormpath.com', 275 | message: 'HTTP 400, Stormpath 400 (mailto:support@stormpath.com): Password requires an uppercase character!', 276 | 'csrfToken': uuid(), 277 | 'hpValue': uuid(), 278 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 279 | }; 280 | xhr.respond( 281 | response.status, 282 | {'Content-Type': 'application/json'}, 283 | JSON.stringify(response) 284 | ); 285 | } 286 | 287 | function respondWithPasswordRequiresNumberError(xhr){ 288 | var response = { 289 | status: 400, 290 | code: 400, 291 | userMessage: 'Password requires a numeric character!', 292 | developerMessage: 'Password requires a numeric character!', 293 | moreInfo: 'mailto:support@stormpath.com', 294 | message: 'HTTP 400, Stormpath 400 (mailto:support@stormpath.com): Password requires a numeric character!', 295 | 'csrfToken': uuid(), 296 | 'hpValue': uuid(), 297 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 298 | }; 299 | xhr.respond( 300 | response.status, 301 | {'Content-Type': 'application/json'}, 302 | JSON.stringify(response) 303 | ); 304 | } 305 | 306 | function respondWithPasswordResetToken(email,xhr){ 307 | var response = { 308 | status: 200, 309 | code: 200, 310 | href: 'https://api.stormpath.com/v1/applications/1h72PFWoGxHKhysKjYIkir/passwordResetTokens/QnKDpz3jxWYrnX9UzOStjgR5S6XBLyhEHRaUgpnKUUrb8GqWWGxcYC8CjcBCchKO3n0quuAZe9', 311 | email: 'robert@robertjd.com', 312 | account: { href: 'https://api.stormpath.com/v1/accounts/' + uuid() }, 313 | 'csrfToken': uuid(), 314 | 'hpValue': uuid(), 315 | 'expires': new Date().getTime() + (1000 * 60 * 5 ) //5 minutes 316 | }; 317 | xhr.respond( 318 | response.status, 319 | {'Content-Type': 'application/json'}, 320 | JSON.stringify(response) 321 | ); 322 | } 323 | 324 | var sinon = window.sinon; 325 | 326 | 327 | var server = sinon.fakeServer.create(); 328 | 329 | 330 | 331 | server.respondWith('POST', 'https://api.stormpath.com/v1/applications/1234/csrfToken', 332 | 333 | function(xhr){ 334 | xhr.respond( 335 | 200, 336 | { 'Content-Type': 'application/json' }, 337 | JSON.stringify( new CsrfResponse('/v1/applications/1234/')) 338 | ); 339 | } 340 | ); 341 | 342 | server.respondWith( 343 | 'POST', 344 | 'https://api.stormpath.com/v1/applications/1234/loginAttempts?expand=account', 345 | function(xhr){ 346 | var validLogin = '{"type":"basic","value":"cm9iZXJ0QHN0b3JtcGF0aC5jb206cm9iZXJ0QHN0b3JtcGF0aC5jb20="}'; // robert@stormpath.com , robert@stormpath.com 347 | var badLogin = '{"type":"basic","value":"cm9iZXJ0QHN0b3JtcGF0aC5jb206MQ=="}'; // robert@stormpath.com , 1 348 | // use '3' for account not found 349 | var niceError = '{"type":"basic","value":"NDk5OjQ5OQ=="}'; // 499 , 499 350 | 351 | if(xhr.requestBody===validLogin){ 352 | respondWithLoginSuccess('https://api.stormpath.com/v1/applications/1234',xhr); 353 | }else if(xhr.requestBody===badLogin){ 354 | respondWithLoginFailure(xhr); 355 | }else if(xhr.requestBody===niceError){ 356 | respondWithOtherError(xhr); 357 | }else{ 358 | respondWithNotFound(xhr); 359 | } 360 | 361 | } 362 | 363 | ); 364 | 365 | 366 | server.respondWith( 367 | 'POST', 368 | 'https://api.stormpath.com/v1/applications/1234/accounts', 369 | function(xhr){ 370 | var data = JSON.parse(xhr.requestBody); 371 | var p = data.password; 372 | if(xhr.requestBody.match(/stormpath/)){ 373 | respondWithDuplicateUser(xhr); 374 | }else if(p && p.length===1){ 375 | respondWithPasswordTooShortError(xhr); 376 | }else if(p && !p.match(/[A-Z]+/)){ 377 | respondWithPasswordRequiresUppercaseError(xhr); 378 | }else if(p && !p.match(/[0-9]+/)){ 379 | respondWithPasswordRequiresNumberError(xhr); 380 | }else if(xhr.requestBody.match(/499/)){ 381 | respondWithOtherError(xhr); 382 | }else{ 383 | var verified = xhr.requestBody.match(/verified|google|facebook/) !== null; 384 | respondWithNewAccount(JSON.parse(xhr.requestBody),verified,xhr); 385 | } 386 | } 387 | ); 388 | 389 | server.respondWith( 390 | 'GET', 391 | new RegExp('https://api.stormpath.com/v1/applications/*'), 392 | function(xhr){ 393 | respondWithApplication(xhr.url,xhr); 394 | } 395 | 396 | ); 397 | 398 | server.respondWith( 399 | 'GET', 400 | 'https://api.stormpath.com/v1/tenants/current', 401 | function(xhr){ 402 | var tenantId = Math.floor(Math.random()*100000); 403 | xhr.respond( 404 | 200, 405 | {'Content-Type': 'application/json'}, 406 | JSON.stringify({ 407 | href: 'https://api.stormpath.com/v1/tenants/'+tenantId 408 | }) 409 | ); 410 | } 411 | 412 | ); 413 | 414 | server.respondWith( 415 | 'POST', 416 | 'https://api.stormpath.com/v1/accounts/emailVerificationTokens/1', 417 | function(xhr){ 418 | respondWithNewAccount({ 419 | email: 'joe@somebody.com' 420 | },true,xhr); 421 | } 422 | ); 423 | 424 | server.respondWith( 425 | 'POST', 426 | 'https://api.stormpath.com/v1/accounts/emailVerificationTokens/2', 427 | function(xhr){ 428 | respondWithNotFound(xhr); 429 | } 430 | ); 431 | 432 | server.respondWith( 433 | 'POST', 434 | 'https://api.stormpath.com/v1/accounts/emailVerificationTokens/3', 435 | function(xhr){ 436 | respondWithOtherError(xhr); 437 | } 438 | ); 439 | 440 | server.respondWith( 441 | 'GET', 442 | 'https://api.stormpath.com/v1/applications/1234/passwordResetTokens/1', 443 | function(xhr){ 444 | respondWithNewAccount({ 445 | email: 'joe@somebody.com' 446 | },true,xhr); 447 | } 448 | ); 449 | 450 | server.respondWith( 451 | 'GET', 452 | 'https://api.stormpath.com/v1/applications/1234/passwordResetTokens/2', 453 | function(xhr){ 454 | respondWithNotFound(xhr); 455 | } 456 | ); 457 | 458 | server.respondWith( 459 | 'GET', 460 | 'https://api.stormpath.com/v1/applications/1234/passwordResetTokens/3', 461 | function(xhr){ 462 | respondWithOtherError(xhr); 463 | } 464 | ); 465 | 466 | server.respondWith( 467 | 'POST', 468 | 'https://api.stormpath.com/v1/applications/1234/passwordResetTokens', 469 | function(xhr){ 470 | var data = JSON.parse(xhr.requestBody); 471 | if(data.email.match(/robert@stormpath.com/)){ 472 | respondWithPasswordResetToken(data.email,xhr); 473 | }else if(data.email.match(/499/)){ 474 | respondWithOtherError(xhr); 475 | }else{ 476 | respondWithNotFound(xhr); 477 | } 478 | } 479 | ); 480 | 481 | server.respondWith( 482 | 'POST', 483 | new RegExp('https://api.stormpath.com/v1/accounts/[0-9]+$'), 484 | function(xhr){ 485 | var data = JSON.parse(xhr.requestBody); 486 | if(data.password.length===1){ 487 | respondWithPasswordTooShortError(xhr); 488 | }else if(!data.password.match(/[A-Z]+/)){ 489 | respondWithPasswordRequiresUppercaseError(xhr); 490 | }else if(!data.password.match(/[0-9]+/)){ 491 | respondWithPasswordRequiresNumberError(xhr); 492 | }else if(xhr.requestBody.match(/499/)){ 493 | respondWithOtherError(xhr); 494 | }else{ 495 | respondWithNewAccount(JSON.parse(xhr.requestBody),true,xhr); 496 | } 497 | } 498 | ); 499 | 500 | sinon.FakeXMLHttpRequest.useFilters=true; 501 | sinon.FakeXMLHttpRequest.addFilter(function(method, url){ 502 | // this could be a way of passing through to a "real" server instance 503 | // that is running on our lan 504 | var t = (/fakeapi.stormpath.com/).test(url); 505 | return t; 506 | }); 507 | 508 | server.autoRespond = true; 509 | server.autoRespondAfter = 200; 510 | return server; 511 | 512 | } 513 | 514 | window.fakeserver = new MockStormpath(); --------------------------------------------------------------------------------