├── .bowerrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── Procfile ├── README.md ├── bower.json ├── client ├── css │ ├── .gitignore │ └── app.css ├── img │ └── .gitignore ├── js │ ├── app.js │ ├── controllers.js │ ├── directives.js │ ├── filters.js │ ├── routingConfig.js │ └── services.js ├── tests │ └── unit │ │ ├── directivesSpec.js │ │ └── servicesSpec.js └── views │ ├── index.jade │ └── partials │ ├── .gitignore │ ├── 404.jade │ ├── admin.jade │ ├── home.jade │ ├── login.jade │ ├── private │ ├── home.jade │ ├── layout.jade │ ├── nested.jade │ └── nestedAdmin.jade │ └── register.jade ├── karma.conf.js ├── package.json ├── run_server_tests.sh ├── server.js └── server ├── controllers ├── auth.js └── user.js ├── models └── User.js ├── routes.js └── tests ├── integration └── index.spec.js └── unit └── controllers └── auth.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "client/components" 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | .idea/ 19 | node_modules/ 20 | 21 | # External tool builders 22 | .externalToolBuilders/ 23 | 24 | # Locally stored "Eclipse launch configurations" 25 | *.launch 26 | 27 | # CDT-specific 28 | .cproject 29 | 30 | # PDT-specific 31 | .buildpath 32 | 33 | 34 | ################# 35 | ## Visual Studio 36 | ################# 37 | 38 | ## Ignore Visual Studio temporary files, build results, and 39 | ## files generated by popular Visual Studio add-ons. 40 | 41 | # User-specific files 42 | *.suo 43 | *.user 44 | *.sln.docstates 45 | 46 | # Build results 47 | [Dd]ebug/ 48 | [Rr]elease/ 49 | *_i.c 50 | *_p.c 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.vspscc 65 | .builds 66 | *.dotCover 67 | 68 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 69 | #packages/ 70 | 71 | # Visual C++ cache files 72 | ipch/ 73 | *.aps 74 | *.ncb 75 | *.opensdf 76 | *.sdf 77 | 78 | # Visual Studio profiler 79 | *.psess 80 | *.vsp 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper* 84 | 85 | # Installshield output folder 86 | [Ee]xpress 87 | 88 | # DocProject is a documentation generator add-in 89 | DocProject/buildhelp/ 90 | DocProject/Help/*.HxT 91 | DocProject/Help/*.HxC 92 | DocProject/Help/*.hhc 93 | DocProject/Help/*.hhk 94 | DocProject/Help/*.hhp 95 | DocProject/Help/Html2 96 | DocProject/Help/html 97 | 98 | # Click-Once directory 99 | publish 100 | 101 | # Others 102 | [Bb]in 103 | [Oo]bj 104 | sql 105 | TestResults 106 | *.Cache 107 | ClientBin 108 | stylecop.* 109 | ~$* 110 | *.dbmdl 111 | Generated_Code #added for RIA/Silverlight projects 112 | 113 | # Backup & report files from converting an old project file to a newer 114 | # Visual Studio version. Backup files are not needed, because we have git ;-) 115 | _UpgradeReport_Files/ 116 | Backup*/ 117 | UpgradeLog*.XML 118 | 119 | 120 | 121 | ############ 122 | ## Windows 123 | ############ 124 | 125 | # Windows image file caches 126 | Thumbs.db 127 | 128 | # Folder config file 129 | Desktop.ini 130 | 131 | 132 | ############# 133 | ## Python 134 | ############# 135 | 136 | *.py[co] 137 | 138 | # Packages 139 | *.egg 140 | *.egg-info 141 | dist 142 | build 143 | eggs 144 | parts 145 | bin 146 | var 147 | sdist 148 | develop-eggs 149 | .installed.cfg 150 | 151 | # Installer logs 152 | pip-log.txt 153 | 154 | # Unit test / coverage reports 155 | .coverage 156 | .tox 157 | 158 | #Translations 159 | *.mo 160 | 161 | #Mr Developer 162 | .mr.developer.cfg 163 | 164 | # Mac crap 165 | .DS_Store 166 | 167 | ########### 168 | # Misc. 169 | ########### 170 | *.env 171 | client/components -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.11' 4 | - '0.10' 5 | env: 6 | global: 7 | - secure: ED0+LU7O2Bk8oOEllanXBpcRpAuBs0uGoDKTMjMjrhZG5NLhMOnQyQivhJb2YME1LPrDUXmq0fwvzTyuBg8FzKKXlgkd7AxkrLVp+e7kdiJ6beUBHQPgDu39+DKr+HKDtmEUGAaA9UXjGrZDD7AlhrZ9hNNRcc1s0oKm5dxq/GU= 8 | - secure: UqgUXa5J8uVMclZhfVoileYalPo6fWGBTQ1kjt9GAI4WfALvlznk9/biZvDSIy1roSVU6WPt+DIprOrte3jxjV8FRWyu4dFEyECQFzFATVFODXFKl6LMXo/kQPSfAmBJNpiyOxRiT9t8DsR4eW0sHXEcy6jSNodiVkj5Gz4EDkY= 9 | - secure: Gg9YDP0yZJ/0vHxCS5JWY+yNrr8bCT9LXHaF/MG2Gz+FvsUh5IVN16WG2Zg/PZS3b2nYnOsgIjmqg9O3yqe0YrX+0QjLLlsZXDccej+YGkgosk3wyhiR3rB/lA8j9hBFfLekyV9RYxndstdSthfMV2phxzuS5nul17si5A7M9IA= 10 | - secure: MPgASha8a2ex7RYQPBMZT99uswR446MY/rcl16eO/YI8vo2XH8nKP03kH0ugNe/TsCZ1gPRJnAS+MGfg+SeZDOol1Zgty8yd1sEEDkOy7Rdaz10fZcybgzotzAXxtlWefzWPIngwFIT9rUnfiYgtUNDmwSJ0qi+axkehhWOH+X8= 11 | - secure: RhDrpOWHREqyY7BLCOe4VodAxyfkANDPTX5O1SA1DUtgUG/5PMvUcZXUGCL+o6vbbpWm29NQI6ttiyulVEnzU87I9vNmTWZHau4ha6IhBqZmPIc3/hXnxuEvegGMr7Rgw3KLwvEFyCPMdS2LTcmGHUkpC4zJIP+4tzEWr9GpDY4= 12 | - secure: PXqfazKG1nSZ5+DxSLmxaIMPgUP0EK+IeHd6lOc6vKKn7Ax5yeH7f8FdyU9ZufVrmV1Fnk+yiTqdzRwK8yjF08cPOlNdZLVW1816chKS/Da52KsttoKLQnukCraAxkhf81+FSi1w3pkdMiAQbF40YsDBi3dc/AnrJrC6d9YIDAY= 13 | - secure: V4WZW2w+tH++UclrvdQKncDyPRS6Lkl1WO3tLoIe31B4K5exSHvFxZ2VtQalLMR6aZOvKvfOYeR921mjrYcNWD99UjLa0zsPnT2jq25l4xSzHmWkwxSZLovHYdfVGUMRfGwJ4wfQokoyi7kxoC1vkHvRnYVK9B4xvhkFbApKGWc= 14 | - secure: IAJEt6xH/OEzZ9SbEOhYZlDHWgLc2RT4WL2HwVazGfF2jJ/d/Z8hq+tfwPqqGzusUZH9rtVdZwsggQm1vro87j2juvvQpk8JqejNrsHOCHJtg6g06yArgFlJYl1c4ApIZDH8WQoslomYrJT5QJ3bFTeSzsNWPfWvPYjqN6GFTyY= 15 | - secure: coTXjqC53pSpjysC/48B5TDzl6bjnVpCqiUQOnrhRQfXhDeIUkQKOvF4y94x2NEG1XG5Yo3xVn69arIHZ57lHxPEdRQbm/vCMxOGYxjd/BTivSk47X4E4uTptRZuywd9RyqGmlm4lKysN3OVN2rpcqnSyypGiMsNI54fVFvP7/c= 16 | - secure: bcCO0Omod0F1VxzGOmXIsFH1TtVpdhG4DolmOsAlzL+GT9n0L3Pn0NRpB3meavqo/BYTA/GyIAAWZy8SYRKNMSLbF+omZu/pEvxeasHV2TxdSRuSkownZzbIXWpupzv8TLsEqHzCEAbH/mYJu0YyXbu+XkurCMhlVX4MmqR+QJk= 17 | - secure: dRpiJaOJswRKUYyQJ7jAqBWutqbZA6qNCLz7srRkzJQunb50zl/BuuOKX0Oi7gbW7nv4iY7Yjt2o+giopSqv8WElxprLCnqKQ1S8UBAwIkm/BMfcc+zQkGQWW6aprlr07rJQ8ol8j0uYgd3oFbzFdgeSM85C06D7gEpIayNYHRA= 18 | - secure: NT0DavZOlgfNOOSImTPPHFIfYHtqUkCNsBC6btfJ2gGduQbnfx2G1OSgAImTMXGU8UATiYHWVhBylpUm7hmRnAHYQT9CeAJtT59IAEpQI9BMnnkatnHSEX/rPFP5iydcRcNxTQBOGJDY/Sv8U8KxzOtiN0eE7n5U0Gx1ntS8Tls= 19 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Load tasks 4 | grunt.loadNpmTasks('grunt-mocha-test'); 5 | grunt.loadNpmTasks('grunt-env'); 6 | grunt.loadNpmTasks('grunt-nodemon'); 7 | grunt.loadNpmTasks('grunt-contrib-clean'); 8 | 9 | grunt.initConfig({ 10 | 11 | mochaTest: { 12 | test: { 13 | options: { 14 | reporter: 'spec' 15 | }, 16 | src: ['server/tests/**/*.js'] 17 | } 18 | }, 19 | 20 | env : { 21 | options : { 22 | //Shared Options Hash 23 | }, 24 | dev : { 25 | NODE_ENV : 'development' 26 | }, 27 | test : { 28 | NODE_ENV : 'test' 29 | } 30 | }, 31 | 32 | nodemon: { 33 | dev: { 34 | script: 'server.js' 35 | } 36 | }, 37 | 38 | clean: ["node_modules", "client/components"] 39 | 40 | }); 41 | 42 | grunt.registerTask('serverTests', ['env:test', 'mochaTest']); 43 | grunt.registerTask('test', ['env:test', 'serverTests']); 44 | grunt.registerTask('dev', ['env:dev', 'nodemon']); 45 | 46 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Frederik Nakstad 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-client-side-auth 2 | ======================== 3 | 4 | [![Build Status](https://travis-ci.org/fnakstad/angular-client-side-auth.png?branch=master)](https://travis-ci.org/fnakstad/angular-client-side-auth) 5 | 6 | One way to implement authentication/authorization in Angular applications. 7 | 8 | **This repo now uses [UI-router](https://github.com/angular-ui/ui-router) rather than [ngRoute](http://docs.angularjs.org/api/ngRoute). For details on how this works [please read this post](http://www.frederiknakstad.com/2014/02/09/ui-router-in-angular-client-side-auth/).** 9 | 10 | * Blogposts: 11 | * [Original post discussing Angular.js client-side solution](http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/) 12 | * [Follow-up post discussing Node.js server-side solution](http://www.frederiknakstad.com/blog/2013/08/04/authentication-in-single-page-applications-with-angular.js-part-2/) 13 | * [UI-router and angular-client-side-auth](http://www.frederiknakstad.com/2014/02/09/ui-router-in-angular-client-side-auth/) 14 | * [Live version](http://angular-client-side-auth.herokuapp.com/) 15 | 16 | To run the server locally, open a terminal, and navigate to the directory you cloned the project to. Make sure you have Node/NPM and Bower installed! Then run the following commands: 17 | 18 | ``` 19 | npm install 20 | npm start 21 | ``` 22 | 23 | Twitter/Facebook/Google auth is enabled by default, but you can easily turn it off by commenting out the `passport.use()` statements in the [server.js](server.js) file. 24 | If you want to enable any of the social logins make sure to set the appropriate environment variables: 25 | 26 | | Provider | Key | Default value | 27 | | ---------| ----| --------------| 28 | | Twitter | TWITTER_CONSUMER_KEY | - | 29 | | Twitter | TWITTER_CONSUMER_SECRET | - | 30 | | Twitter | TWITTER_CALLBACK_URL | http://localhost:8000/auth/twitter/callback | 31 | | Facebook | FACEBOOK_APP_ID | - | 32 | | Facebook | FACEBOOK_APP_SECRET | - | 33 | | Facebook | FACEBOOK_CALLBACK_URL | http://localhost:8000/auth/facebook/callback | 34 | | Google | GOOGLE_REALM | http://localhost:8000 | 35 | | Google | GOOGLE_RETURN_URL | http://localhost:8000/auth/google/return | 36 | | LinkedIn | LINKED_IN_KEY | - | 37 | | LinkedIn | LINKED_IN_SECRET | - | 38 | | LinkedIn |LINKED_IN_CALLBACK_URL | http://localhost:8000/auth/linkedin/callback | 39 | 40 | ## Tests 41 | To run automated server tests: 42 | ``` 43 | npm test 44 | ``` 45 | 46 | ## License 47 | ``` 48 | The MIT License (MIT) 49 | 50 | Copyright (c) 2013 Frederik Nakstad 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a copy of 53 | this software and associated documentation files (the "Software"), to deal in 54 | the Software without restriction, including without limitation the rights to 55 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 56 | the Software, and to permit persons to whom the Software is furnished to do so, 57 | subject to the following conditions: 58 | 59 | The above copyright notice and this permission notice shall be included in all 60 | copies or substantial portions of the Software. 61 | 62 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 63 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 64 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 65 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 66 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 67 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 68 | ``` 69 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-client-side-auth", 3 | "dependencies": { 4 | "angular": "~1.3.3", 5 | "angular-ui-router": "~0.2.13", 6 | "bootstrap": "~3.1.0", 7 | "font-awesome": "~4.0.3", 8 | "angular-cookies": "~1.3.3" 9 | }, 10 | "devDependencies": { 11 | "angular-mocks": "~1.3.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/css/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fnakstad/angular-client-side-auth/9a6cdb8c42a0c1f5ae133f106d494e1bbbbb4217/client/css/.gitignore -------------------------------------------------------------------------------- /client/css/app.css: -------------------------------------------------------------------------------- 1 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 2 | display: none; 3 | } 4 | 5 | body { 6 | font-size: 1em; 7 | } 8 | 9 | #cover { 10 | position:absolute; 11 | height:100%; 12 | width:100%; 13 | background:white; 14 | } 15 | 16 | #userInfo { 17 | float:right;padding:8px; 18 | } 19 | 20 | #alertBox { 21 | position: fixed; 22 | min-width: 75%; 23 | left: 50%; 24 | margin: 0 0 0 -37.5%; 25 | top: 5px; 26 | } 27 | 28 | .fa.fa-twitter { 29 | color:#00A7E7; 30 | } 31 | 32 | .fa.fa-facebook-square { 33 | color:#3662A0; 34 | } 35 | 36 | .fa.fa-google-plus-square { 37 | color: #D74634; 38 | } 39 | 40 | .fade-hide-setup, .fade-show-setup { 41 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 42 | -moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 43 | -o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 44 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; 45 | } 46 | 47 | .fade-hide-setup { 48 | opacity:1; 49 | } 50 | .fade-hide-setup.fade-hide-start { 51 | opacity:0; 52 | } 53 | 54 | .fade-show-setup { 55 | opacity:0; 56 | } 57 | .fade-show-setup.fade-show-start { 58 | opacity:1; 59 | } -------------------------------------------------------------------------------- /client/img/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fnakstad/angular-client-side-auth/9a6cdb8c42a0c1f5ae133f106d494e1bbbbb4217/client/img/.gitignore -------------------------------------------------------------------------------- /client/js/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('angular-client-side-auth', ['ngCookies', 'ui.router']) 4 | 5 | .config(['$stateProvider', '$urlRouterProvider', '$locationProvider', '$httpProvider', function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) { 6 | 7 | var access = routingConfig.accessLevels; 8 | 9 | // Public routes 10 | $stateProvider 11 | .state('public', { 12 | abstract: true, 13 | template: "", 14 | data: { 15 | access: access.public 16 | } 17 | }) 18 | .state('public.404', { 19 | url: '/404/', 20 | templateUrl: '404' 21 | }); 22 | 23 | // Anonymous routes 24 | $stateProvider 25 | .state('anon', { 26 | abstract: true, 27 | template: "", 28 | data: { 29 | access: access.anon 30 | } 31 | }) 32 | .state('anon.login', { 33 | url: '/login/', 34 | templateUrl: 'login', 35 | controller: 'LoginCtrl' 36 | }) 37 | .state('anon.register', { 38 | url: '/register/', 39 | templateUrl: 'register', 40 | controller: 'RegisterCtrl' 41 | }); 42 | 43 | // Regular user routes 44 | $stateProvider 45 | .state('user', { 46 | abstract: true, 47 | template: "", 48 | data: { 49 | access: access.user 50 | } 51 | }) 52 | .state('user.home', { 53 | url: '/', 54 | templateUrl: 'home' 55 | }) 56 | .state('user.private', { 57 | abstract: true, 58 | url: '/private/', 59 | templateUrl: 'private/layout' 60 | }) 61 | .state('user.private.home', { 62 | url: '', 63 | templateUrl: 'private/home' 64 | }) 65 | .state('user.private.nested', { 66 | url: 'nested/', 67 | templateUrl: 'private/nested' 68 | }) 69 | .state('user.private.admin', { 70 | url: 'admin/', 71 | templateUrl: 'private/nestedAdmin', 72 | data: { 73 | access: access.admin 74 | } 75 | }); 76 | 77 | // Admin routes 78 | $stateProvider 79 | .state('admin', { 80 | abstract: true, 81 | template: "", 82 | data: { 83 | access: access.admin 84 | } 85 | }) 86 | .state('admin.admin', { 87 | url: '/admin/', 88 | templateUrl: 'admin', 89 | controller: 'AdminCtrl' 90 | }); 91 | 92 | 93 | $urlRouterProvider.otherwise('/404'); 94 | 95 | // FIX for trailing slashes. Gracefully "borrowed" from https://github.com/angular-ui/ui-router/issues/50 96 | $urlRouterProvider.rule(function($injector, $location) { 97 | if($location.protocol() === 'file') 98 | return; 99 | 100 | var path = $location.path() 101 | // Note: misnomer. This returns a query object, not a search string 102 | , search = $location.search() 103 | , params 104 | ; 105 | 106 | // check to see if the path already ends in '/' 107 | if (path[path.length - 1] === '/') { 108 | return; 109 | } 110 | 111 | // If there was no search string / query params, return with a `/` 112 | if (Object.keys(search).length === 0) { 113 | return path + '/'; 114 | } 115 | 116 | // Otherwise build the search string and return a `/?` prefix 117 | params = []; 118 | angular.forEach(search, function(v, k){ 119 | params.push(k + '=' + v); 120 | }); 121 | return path + '/?' + params.join('&'); 122 | }); 123 | 124 | $locationProvider.html5Mode(true); 125 | 126 | $httpProvider.interceptors.push(function($q, $location) { 127 | return { 128 | 'responseError': function(response) { 129 | if(response.status === 401 || response.status === 403) { 130 | $location.path('/login'); 131 | } 132 | return $q.reject(response); 133 | } 134 | }; 135 | }); 136 | 137 | }]) 138 | 139 | .run(['$rootScope', '$state', 'Auth', function ($rootScope, $state, Auth) { 140 | 141 | $rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) { 142 | 143 | if(!('data' in toState) || !('access' in toState.data)){ 144 | $rootScope.error = "Access undefined for this state"; 145 | event.preventDefault(); 146 | } 147 | else if (!Auth.authorize(toState.data.access)) { 148 | $rootScope.error = "Seems like you tried accessing a route you don't have access to..."; 149 | event.preventDefault(); 150 | 151 | if(fromState.url === '^') { 152 | if(Auth.isLoggedIn()) { 153 | $state.go('user.home'); 154 | } else { 155 | $rootScope.error = null; 156 | $state.go('anon.login'); 157 | } 158 | } 159 | } 160 | }); 161 | 162 | }]); 163 | -------------------------------------------------------------------------------- /client/js/controllers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Controllers */ 4 | 5 | angular.module('angular-client-side-auth') 6 | .controller('NavCtrl', ['$rootScope', '$scope', '$location', 'Auth', function($rootScope, $scope, $location, Auth) { 7 | $scope.user = Auth.user; 8 | $scope.userRoles = Auth.userRoles; 9 | $scope.accessLevels = Auth.accessLevels; 10 | 11 | $scope.logout = function() { 12 | Auth.logout(function() { 13 | $location.path('/login'); 14 | }, function() { 15 | $rootScope.error = "Failed to logout"; 16 | }); 17 | }; 18 | }]); 19 | 20 | angular.module('angular-client-side-auth') 21 | .controller('LoginCtrl', 22 | ['$rootScope', '$scope', '$location', '$window', 'Auth', function($rootScope, $scope, $location, $window, Auth) { 23 | 24 | $scope.rememberme = true; 25 | $scope.login = function() { 26 | Auth.login({ 27 | username: $scope.username, 28 | password: $scope.password, 29 | rememberme: $scope.rememberme 30 | }, 31 | function(res) { 32 | $location.path('/'); 33 | }, 34 | function(err) { 35 | $rootScope.error = "Failed to login"; 36 | }); 37 | }; 38 | 39 | $scope.loginOauth = function(provider) { 40 | $window.location.href = '/auth/' + provider; 41 | }; 42 | }]); 43 | 44 | angular.module('angular-client-side-auth') 45 | .controller('RegisterCtrl', 46 | ['$rootScope', '$scope', '$location', 'Auth', function($rootScope, $scope, $location, Auth) { 47 | $scope.role = Auth.userRoles.user; 48 | $scope.userRoles = Auth.userRoles; 49 | 50 | $scope.register = function() { 51 | Auth.register({ 52 | username: $scope.username, 53 | password: $scope.password, 54 | role: $scope.role 55 | }, 56 | function() { 57 | $location.path('/'); 58 | }, 59 | function(err) { 60 | $rootScope.error = err; 61 | }); 62 | }; 63 | }]); 64 | 65 | angular.module('angular-client-side-auth') 66 | .controller('AdminCtrl', 67 | ['$rootScope', '$scope', 'Users', 'Auth', function($rootScope, $scope, Users, Auth) { 68 | $scope.loading = true; 69 | $scope.userRoles = Auth.userRoles; 70 | 71 | Users.getAll(function(res) { 72 | $scope.users = res; 73 | $scope.loading = false; 74 | }, function(err) { 75 | $rootScope.error = "Failed to fetch users."; 76 | $scope.loading = false; 77 | }); 78 | 79 | }]); 80 | 81 | -------------------------------------------------------------------------------- /client/js/directives.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('angular-client-side-auth') 4 | .directive('accessLevel', ['Auth', function(Auth) { 5 | return { 6 | restrict: 'A', 7 | link: function($scope, element, attrs) { 8 | var prevDisp = element.css('display') 9 | , userRole 10 | , accessLevel; 11 | 12 | $scope.user = Auth.user; 13 | $scope.$watch('user', function(user) { 14 | if(user.role) 15 | userRole = user.role; 16 | updateCSS(); 17 | }, true); 18 | 19 | attrs.$observe('accessLevel', function(al) { 20 | if(al) accessLevel = $scope.$eval(al); 21 | updateCSS(); 22 | }); 23 | 24 | function updateCSS() { 25 | if(userRole && accessLevel) { 26 | if(!Auth.authorize(accessLevel, userRole)) 27 | element.css('display', 'none'); 28 | else 29 | element.css('display', prevDisp); 30 | } 31 | } 32 | } 33 | }; 34 | }]); 35 | 36 | angular.module('angular-client-side-auth').directive('activeNav', ['$location', function($location) { 37 | return { 38 | restrict: 'A', 39 | link: function(scope, element, attrs) { 40 | var anchor = element[0]; 41 | if(element[0].tagName.toUpperCase() != 'A') 42 | anchor = element.find('a')[0]; 43 | var path = anchor.href; 44 | 45 | scope.location = $location; 46 | scope.$watch('location.absUrl()', function(newPath) { 47 | path = normalizeUrl(path); 48 | newPath = normalizeUrl(newPath); 49 | 50 | if(path === newPath || 51 | (attrs.activeNav === 'nestedTop' && newPath.indexOf(path) === 0)) { 52 | element.addClass('active'); 53 | } else { 54 | element.removeClass('active'); 55 | } 56 | }); 57 | } 58 | 59 | }; 60 | 61 | function normalizeUrl(url) { 62 | if(url[url.length - 1] !== '/') 63 | url = url + '/'; 64 | return url; 65 | } 66 | 67 | }]); -------------------------------------------------------------------------------- /client/js/filters.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fnakstad/angular-client-side-auth/9a6cdb8c42a0c1f5ae133f106d494e1bbbbb4217/client/js/filters.js -------------------------------------------------------------------------------- /client/js/routingConfig.js: -------------------------------------------------------------------------------- 1 | (function(exports){ 2 | 3 | var config = { 4 | 5 | /* List all the roles you wish to use in the app 6 | * You have a max of 31 before the bit shift pushes the accompanying integer out of 7 | * the memory footprint for an integer 8 | */ 9 | roles :[ 10 | 'public', 11 | 'user', 12 | 'admin'], 13 | 14 | /* 15 | Build out all the access levels you want referencing the roles listed above 16 | You can use the "*" symbol to represent access to all roles. 17 | 18 | The left-hand side specifies the name of the access level, and the right-hand side 19 | specifies what user roles have access to that access level. E.g. users with user role 20 | 'user' and 'admin' have access to the access level 'user'. 21 | */ 22 | accessLevels : { 23 | 'public' : "*", 24 | 'anon': ['public'], 25 | 'user' : ['user', 'admin'], 26 | 'admin': ['admin'] 27 | } 28 | 29 | } 30 | 31 | exports.userRoles = buildRoles(config.roles); 32 | exports.accessLevels = buildAccessLevels(config.accessLevels, exports.userRoles); 33 | 34 | /* 35 | Method to build a distinct bit mask for each role 36 | It starts off with "1" and shifts the bit to the left for each element in the 37 | roles array parameter 38 | */ 39 | 40 | function buildRoles(roles){ 41 | 42 | var bitMask = "01"; 43 | var userRoles = {}; 44 | 45 | for(var role in roles){ 46 | var intCode = parseInt(bitMask, 2); 47 | userRoles[roles[role]] = { 48 | bitMask: intCode, 49 | title: roles[role] 50 | }; 51 | bitMask = (intCode << 1 ).toString(2) 52 | } 53 | 54 | return userRoles; 55 | } 56 | 57 | /* 58 | This method builds access level bit masks based on the accessLevelDeclaration parameter which must 59 | contain an array for each access level containing the allowed user roles. 60 | */ 61 | function buildAccessLevels(accessLevelDeclarations, userRoles){ 62 | 63 | var accessLevels = {}; 64 | for(var level in accessLevelDeclarations){ 65 | 66 | if(typeof accessLevelDeclarations[level] == 'string'){ 67 | if(accessLevelDeclarations[level] == '*'){ 68 | 69 | var resultBitMask = ''; 70 | 71 | for( var role in userRoles){ 72 | resultBitMask += "1" 73 | } 74 | //accessLevels[level] = parseInt(resultBitMask, 2); 75 | accessLevels[level] = { 76 | bitMask: parseInt(resultBitMask, 2) 77 | }; 78 | } 79 | else console.log("Access Control Error: Could not parse '" + accessLevelDeclarations[level] + "' as access definition for level '" + level + "'") 80 | 81 | } 82 | else { 83 | 84 | var resultBitMask = 0; 85 | for(var role in accessLevelDeclarations[level]){ 86 | if(userRoles.hasOwnProperty(accessLevelDeclarations[level][role])) 87 | resultBitMask = resultBitMask | userRoles[accessLevelDeclarations[level][role]].bitMask 88 | else console.log("Access Control Error: Could not find role '" + accessLevelDeclarations[level][role] + "' in registered roles while building access for '" + level + "'") 89 | } 90 | accessLevels[level] = { 91 | bitMask: resultBitMask 92 | }; 93 | } 94 | } 95 | 96 | return accessLevels; 97 | } 98 | 99 | })(typeof exports === 'undefined' ? this['routingConfig'] = {} : exports); -------------------------------------------------------------------------------- /client/js/services.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('angular-client-side-auth') 4 | .factory('Auth', function($http, $cookieStore){ 5 | 6 | var accessLevels = routingConfig.accessLevels 7 | , userRoles = routingConfig.userRoles 8 | , currentUser = $cookieStore.get('user') || { username: '', role: userRoles.public }; 9 | 10 | $cookieStore.remove('user'); 11 | 12 | function changeUser(user) { 13 | angular.extend(currentUser, user); 14 | } 15 | 16 | return { 17 | authorize: function(accessLevel, role) { 18 | if(role === undefined) { 19 | role = currentUser.role; 20 | } 21 | 22 | return accessLevel.bitMask & role.bitMask; 23 | }, 24 | isLoggedIn: function(user) { 25 | if(user === undefined) { 26 | user = currentUser; 27 | } 28 | return user.role.title === userRoles.user.title || user.role.title === userRoles.admin.title; 29 | }, 30 | register: function(user, success, error) { 31 | $http.post('/register', user).success(function(res) { 32 | changeUser(res); 33 | success(); 34 | }).error(error); 35 | }, 36 | login: function(user, success, error) { 37 | $http.post('/login', user).success(function(user){ 38 | changeUser(user); 39 | success(user); 40 | }).error(error); 41 | }, 42 | logout: function(success, error) { 43 | $http.post('/logout').success(function(){ 44 | changeUser({ 45 | username: '', 46 | role: userRoles.public 47 | }); 48 | success(); 49 | }).error(error); 50 | }, 51 | accessLevels: accessLevels, 52 | userRoles: userRoles, 53 | user: currentUser 54 | }; 55 | }); 56 | 57 | angular.module('angular-client-side-auth') 58 | .factory('Users', function($http) { 59 | return { 60 | getAll: function(success, error) { 61 | $http.get('/users').success(success).error(error); 62 | } 63 | }; 64 | }); 65 | -------------------------------------------------------------------------------- /client/tests/unit/directivesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for services go here */ 4 | 5 | 6 | describe('directives', function() { 7 | var scope, elem, $httpBackend, Auth; 8 | 9 | beforeEach( 10 | module('angular-client-side-auth') 11 | ); 12 | 13 | beforeEach(inject(function($injector) { 14 | $httpBackend = $injector.get('$httpBackend'); 15 | Auth = $injector.get('Auth'); 16 | })); 17 | 18 | // On module load there will always be a stateChange event to the login state 19 | beforeEach(function() { 20 | $httpBackend.expectGET('login').respond(); 21 | $httpBackend.flush(); 22 | }); 23 | 24 | afterEach(function() { 25 | $httpBackend.verifyNoOutstandingExpectation(); 26 | $httpBackend.verifyNoOutstandingRequest(); 27 | }); 28 | 29 | describe('accessLevel', function() { 30 | 31 | it('when user is public and access is public - the menu must be visible',inject(function($compile, $rootScope){ 32 | 33 | 34 | scope = $rootScope.$new(); 35 | scope.accessLevels = routingConfig.accessLevels; 36 | 37 | var elem = $compile("
  • some text here
  • ")(scope); 38 | 39 | //fire watch 40 | scope.$apply(); 41 | 42 | expect(elem.css('display')).to.equal(''); 43 | })); 44 | 45 | 46 | it('when user is public and access is user - the menu must be hidden',inject(function($compile, $rootScope){ 47 | 48 | scope = $rootScope.$new(); 49 | scope.accessLevels = routingConfig.accessLevels; 50 | 51 | var elem = $compile("
  • some text here
  • ")(scope); 52 | 53 | //fire watch 54 | scope.$apply(); 55 | 56 | expect(elem.css('display')).to.equal('none'); 57 | })) 58 | }); 59 | 60 | describe('activeNav', function() { 61 | var location, compile; 62 | 63 | beforeEach(inject(function($compile, $rootScope, $location) { 64 | scope = $rootScope.$new(); 65 | location = $location 66 | compile = $compile; 67 | })); 68 | 69 | it('when location is same as "href" of link - the link must be decorated with "active" class',function(){ 70 | location.path('register'); 71 | $httpBackend.expectGET('register').respond(); 72 | $httpBackend.flush(); 73 | 74 | var elem = compile("
  • Register
  • ")(scope); 75 | 76 | //fire watch 77 | scope.$apply(); 78 | expect(elem.hasClass('active')).to.equal(true); 79 | }); 80 | 81 | it('when location is different from "href" of link - the "active" class must be removed',function(){ 82 | location.path('register'); 83 | $httpBackend.expectGET('register').respond(); 84 | $httpBackend.flush(); 85 | 86 | //initially decorated with 'active' 87 | var elem = compile("
  • somelink
  • ")(scope); 88 | 89 | //fire watch 90 | scope.$apply(); 91 | expect(elem.hasClass('active')).to.equal(false); 92 | }) 93 | }) 94 | 95 | 96 | }); 97 | -------------------------------------------------------------------------------- /client/tests/unit/servicesSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jasmine specs for services go here */ 4 | 5 | describe('services', function() { 6 | var Auth; 7 | var $httpBackend; 8 | 9 | // you need to indicate your module in a test 10 | beforeEach(function() { 11 | module('angular-client-side-auth'); 12 | }); 13 | 14 | beforeEach(inject(function($injector) { 15 | $httpBackend = $injector.get('$httpBackend'); 16 | Auth = $injector.get('Auth'); 17 | })); 18 | 19 | // On module load there will always be a stateChange event to the login state 20 | beforeEach(function() { 21 | $httpBackend.expectGET('login').respond(); 22 | $httpBackend.flush(); 23 | }); 24 | 25 | afterEach(function() { 26 | $httpBackend.verifyNoOutstandingExpectation(); 27 | $httpBackend.verifyNoOutstandingRequest(); 28 | }); 29 | 30 | describe('Auth', function() { 31 | describe('instantiate', function() { 32 | it('should have isLoggedIn function', function() { 33 | expect(Auth.isLoggedIn).to.not.be.undefined; 34 | expect(angular.isFunction(Auth.isLoggedIn)).to.equal(true); 35 | }); 36 | 37 | it('should have authorize function', function() { 38 | expect(Auth.authorize).to.not.be.undefined; 39 | expect(angular.isFunction(Auth.authorize)).to.equal(true); 40 | }); 41 | 42 | it('should have login function', function() { 43 | expect(Auth.login).to.not.be.undefined; 44 | expect(angular.isFunction(Auth.login)).to.equal(true); 45 | }); 46 | 47 | it('should have logout function', function() { 48 | expect(Auth.logout).to.not.be.undefined; 49 | expect(angular.isFunction(Auth.logout)).to.equal(true); 50 | }); 51 | 52 | it('should have register function', function() { 53 | expect(Auth.register).to.not.be.undefined; 54 | expect(angular.isFunction(Auth.register)).to.equal(true); 55 | }); 56 | 57 | it('should have the user object', function() { 58 | expect(Auth.user).to.not.be.undefined; 59 | expect(angular.isObject(Auth.user)).to.equal(true); 60 | }); 61 | 62 | it('should have the userRoles object', function() { 63 | expect(Auth.userRoles).to.not.be.undefined; 64 | expect(angular.isObject(Auth.userRoles)).to.equal(true); 65 | }); 66 | 67 | it('should have the accessLevels object', function() { 68 | expect(Auth.accessLevels).to.not.be.undefined; 69 | expect(angular.isObject(Auth.accessLevels)).to.equal(true); 70 | }); 71 | 72 | it('should set the user object with no name and public role', function() { 73 | expect(Auth.user).to.deep.equal({ username: '', role: Auth.userRoles.public }); 74 | }); 75 | }); 76 | 77 | describe('authorize', function() { 78 | it('should return 0 when role not recognized', function() { 79 | expect(Auth.authorize('foo')).to.equal(0); 80 | }); 81 | 82 | it('should return 1 when role is recognized', function() { 83 | var accessLevels = { bitMask: 1 }; 84 | var role = { bitMask: 1 }; 85 | expect(Auth.authorize(accessLevels, role)).to.equal(1); 86 | }); 87 | 88 | it('should return 0 when role is omitted and not equal', function() { 89 | var accessLevels = { bitMask: 0 }; 90 | expect(Auth.user.role.bitMask).to.equal(1); 91 | expect(Auth.authorize(accessLevels)).to.equal(0); 92 | }); 93 | 94 | it('should return 1 when role is omitted but equal', function() { 95 | var accessLevels = { bitMask: 1 }; 96 | expect(Auth.user.role.bitMask).to.equal(1); 97 | expect(Auth.authorize(accessLevels)).to.equal(1); 98 | }); 99 | }); 100 | 101 | describe('isLoggedIn', function() { 102 | it('should use the currentUser when use omitted', function() { 103 | // current user has role public 104 | expect(Auth.isLoggedIn()).to.equal(false); 105 | }); 106 | 107 | it('should return false when user has role public', function() { 108 | var user = { role: { title: 'public' } }; 109 | expect(Auth.isLoggedIn(user)).to.equal(false); 110 | }); 111 | 112 | it('should return true when user has role user', function() { 113 | var user = { role: { title: 'user' } }; 114 | expect(Auth.isLoggedIn(user)).to.equal(true); 115 | }); 116 | 117 | it('should return true when user has role admin', function() { 118 | var user = { role: { title: 'admin' } }; 119 | expect(Auth.isLoggedIn(user)).to.equal(true); 120 | }); 121 | }); 122 | 123 | describe('register', function() { 124 | it('should make a request and invoke callback', function() { 125 | var invoked = false; 126 | var success = function() { 127 | invoked = true; 128 | }; 129 | var error = function() {}; 130 | $httpBackend.expectPOST('/register').respond(); 131 | Auth.register({}, success, error); 132 | $httpBackend.flush(); 133 | expect(invoked).to.equal(true); 134 | }); 135 | 136 | it('should append the user', function() { 137 | var success = function() {}; 138 | var error = function() {}; 139 | $httpBackend.expectPOST('/register').respond({ 'user': 'foo' }); 140 | Auth.register({}, success, error); 141 | $httpBackend.flush(); 142 | expect(Auth.user).to.deep.equal({ username : '', role : { bitMask : 1, title : 'public' }, user : 'foo' }); 143 | }); 144 | }); 145 | 146 | describe('login', function() { 147 | it('should make a request and invoke callback', function() { 148 | var invoked = false; 149 | var success = function() { 150 | invoked = true; 151 | }; 152 | var error = function() {}; 153 | $httpBackend.expectPOST('/login').respond(); 154 | Auth.login({}, success, error); 155 | $httpBackend.flush(); 156 | expect(invoked).to.equal(true); 157 | }); 158 | 159 | it('should append the user', function() { 160 | var success = function() {}; 161 | var error = function() {}; 162 | $httpBackend.expectPOST('/login').respond({ 'user': 'bar' }); 163 | Auth.login({}, success, error); 164 | $httpBackend.flush(); 165 | expect(Auth.user).to.deep.equal({ username : '', role : { bitMask : 1, title : 'public' }, user : 'bar' }); 166 | }); 167 | }); 168 | 169 | describe('logout', function() { 170 | it('should make a request and invoke callback', function() { 171 | var invoked = false; 172 | var success = function() { 173 | invoked = true; 174 | }; 175 | var error = function() {}; 176 | $httpBackend.expectPOST('/logout').respond(); 177 | Auth.logout(success, error); 178 | $httpBackend.flush(); 179 | expect(invoked).to.equal(true); 180 | }); 181 | }); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /client/views/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en', data-ng-app='angular-client-side-auth') 3 | head 4 | meta(charset='utf-8') 5 | base(href='/') 6 | title Angular Auth Example 7 | link(rel='stylesheet', href='/css/app.css') 8 | link(href="/components/bootstrap/dist/css/bootstrap.min.css", rel="stylesheet") 9 | link(href="/components/font-awesome/css/font-awesome.min.css", rel="stylesheet") 10 | 11 | // This is needed because Facebook login redirects add #_=_ at the end of the URL 12 | script(type="text/javascript"). 13 | if (window.location.href.indexOf('#_=_') > 0) { 14 | window.location = window.location.href.replace(/#.*/, ''); 15 | } 16 | body(data-ng-cloak) 17 | 18 | .navbar(data-ng-controller="NavCtrl") 19 | .navbar-inner 20 | .container-fluid 21 | ul.nav.nav-tabs 22 | li(data-access-level='accessLevels.anon', active-nav) 23 | a(href='/login') Log in 24 | li(data-access-level='accessLevels.anon', active-nav) 25 | a(href='/register') Register 26 | li(data-access-level='accessLevels.user', active-nav) 27 | a(href='/') Home 28 | li(data-access-level='accessLevels.user', active-nav='nestedTop') 29 | a(href='/private') Private 30 | li(data-access-level='accessLevels.admin', active-nav) 31 | a(href='/admin') Admin 32 | li(data-access-level='accessLevels.user') 33 | a(href="", data-ng-click="logout()") 34 | | Log out 35 | div#userInfo.pull-right(data-access-level='accessLevels.user') 36 | | Welcome  37 | strong {{ user.username }}  38 | span.label(data-ng-class='{"label-info": user.role.title == userRoles.user.title, "label-success": user.role.title == userRoles.admin.title}') {{ user.role.title }} 39 | 40 | .container(data-ui-view) 41 | #alertBox.alert.alert-danger(data-ng-show="error") 42 | button(type="button", class="close", data-ng-click="error = null;") × 43 | strong Oh no!  44 | span(data-ng-bind="error") 45 | 46 | script(src='/components/angular/angular.min.js') 47 | script(src='/components/angular-cookies/angular-cookies.min.js') 48 | script(src='/components/angular-ui-router/release/angular-ui-router.min.js') 49 | script(src='/js/routingConfig.js') 50 | script(src='/js/app.js') 51 | script(src='/js/services.js') 52 | script(src='/js/controllers.js') 53 | script(src='/js/filters.js') 54 | script(src='/js/directives.js') 55 | 56 | // Partial views... Load up front to make transitions smoother 57 | script(type="text/ng-template", id="404") 58 | include partials/404 59 | script(type="text/ng-template", id="admin") 60 | include partials/admin 61 | script(type="text/ng-template", id="home") 62 | include partials/home 63 | script(type="text/ng-template", id="login") 64 | include partials/login 65 | script(type="text/ng-template", id="private/layout") 66 | include partials/private/layout 67 | script(type="text/ng-template", id="private/home") 68 | include partials/private/home 69 | script(type="text/ng-template", id="private/nested") 70 | include partials/private/nested 71 | script(type="text/ng-template", id="private/nestedAdmin") 72 | include partials/private/nestedAdmin 73 | script(type="text/ng-template", id="register") 74 | include partials/register 75 | -------------------------------------------------------------------------------- /client/views/partials/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fnakstad/angular-client-side-auth/9a6cdb8c42a0c1f5ae133f106d494e1bbbbb4217/client/views/partials/.gitignore -------------------------------------------------------------------------------- /client/views/partials/404.jade: -------------------------------------------------------------------------------- 1 | h1 404 2 | p Ain't nothing here -------------------------------------------------------------------------------- /client/views/partials/admin.jade: -------------------------------------------------------------------------------- 1 | h1 Admin 2 | p This view is visible to users with the administrator role. 3 | 4 | table.table.table-striped(data-ng-hide="loading") 5 | thead 6 | tr 7 | th # 8 | th Username 9 | th Role 10 | tbody 11 | tr(data-ng-repeat="user in users") 12 | td {{ user.id }} 13 | td 14 | i.fa.fa-twitter(data-ng-show="user.provider == 'twitter'") 15 | i.fa.fa-facebook-square(data-ng-show="user.provider == 'facebook'") 16 | i.fa.fa-google-plus-square(data-ng-show="user.provider == 'google'") 17 | i.fa.fa-linkedin(data-ng-show="user.provider == 'linkedin'") 18 | | {{ user.username }} 19 | td 20 | span.label(data-ng-class='{"label-info": user.role.title == userRoles.user.title, "label-success": user.role.title == userRoles.admin.title}') {{ user.role.title }} -------------------------------------------------------------------------------- /client/views/partials/home.jade: -------------------------------------------------------------------------------- 1 | h1 Hello 2 | p This view is visible to logged in users. -------------------------------------------------------------------------------- /client/views/partials/login.jade: -------------------------------------------------------------------------------- 1 | h1 Log in 2 | p This site is an example of how one can implement role based authentication in Angular applications as outlined in 3 | a(href="http://www.frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/") this blogpost 4 | | . All the code can be found in 5 | a(href="https://github.com/fnakstad/angular-client-side-auth") this GitHub repository 6 | | . You can either register a new user, log in with one of the two predefined users... 7 | ul 8 | li admin/123 9 | li user/123 10 | 11 | hr 12 | form.form-horizontal(ng-submit="login()", name="loginForm", role="form") 13 | .form-group 14 | label.control-label.col-sm-2(for="username") Username 15 | .col-sm-10 16 | input.form-control(type="text", data-ng-model="username", placeholder="Username", name="username", required, autofocus) 17 | .form-group 18 | label.control-label.col-sm-2(for="password") Password 19 | .col-sm-10 20 | input.form-control(type="password", data-ng-model="password", placeholder="Password", name="password", required) 21 | .form-group 22 | .col-sm-offset-2.col-sm-10 23 | .checkbox 24 | label(for="rememberme").checkbox 25 | input(type="checkbox", data-ng-model="rememberme", name="rememberme") 26 | | Remember me 27 | .form-group 28 | .col-sm-offset-2.col-sm-10 29 | button.btn.btn-default(type="submit", data-ng-disabled="loginForm.$invalid") Log in 30 | hr 31 | p ... or use one of them fancy social logins: 32 | .btn-group 33 | a.btn.btn-default(href="", data-ng-click="loginOauth('facebook')") 34 | i.fa.fa-facebook-square 35 | | Facebook 36 | a.btn.btn-default(href="", data-ng-click="loginOauth('twitter')") 37 | i.fa.fa-twitter 38 | | Twitter 39 | a.btn.btn-default(href="", data-ng-click="loginOauth('google')") 40 | i.fa.fa-google-plus-square 41 | | Google 42 | a.btn.btn-default(href="", data-ng-click="loginOauth('linkedin')") 43 | i.fa.fa-linkedin 44 | | LinkedIn -------------------------------------------------------------------------------- /client/views/partials/private/home.jade: -------------------------------------------------------------------------------- 1 | h1 Private view 2 | p This nested view is visible to logged in users -------------------------------------------------------------------------------- /client/views/partials/private/layout.jade: -------------------------------------------------------------------------------- 1 | .container 2 | .row 3 | .col-md-4.col-md-push-6 4 | div.list-group(data-ng-controller='NavCtrl') 5 | .list-group-item 6 | strong Nested menu 7 | a.list-group-item(href='/private', active-nav) Nested view 8 | a.list-group-item(href='/private/nested', active-nav) Another nested view 9 | a.list-group-item(href='/private/admin', active-nav, data-access-level='accessLevels.admin') A nested admin view 10 | .col-md-6.col-md-pull-4(data-ui-view) -------------------------------------------------------------------------------- /client/views/partials/private/nested.jade: -------------------------------------------------------------------------------- 1 | h1 Private view 2 | p This other nested view is also visible to logged in users -------------------------------------------------------------------------------- /client/views/partials/private/nestedAdmin.jade: -------------------------------------------------------------------------------- 1 | h1 Private view 2 | p 3 | | This nested view is only available to  4 | span.label.label-success admin 5 | |  users -------------------------------------------------------------------------------- /client/views/partials/register.jade: -------------------------------------------------------------------------------- 1 | h1 Register 2 | form.form-horizontal(ng-submit="register()", name="registerForm", role="form") 3 | .form-group 4 | label.control-label.col-sm-2(for="username") Username 5 | .col-sm-10 6 | input.form-control(type="text", data-ng-model="username", placeholder="Username", name="username", required, data-ng-minlength="1", data-ng-maxlength="20", autofocus) 7 | .form-group 8 | label.control-label.col-sm-2(for="password") Password 9 | .col-sm-10 10 | input.form-control(type="password", data-ng-model="password", placeholder="Password", name="password", required, data-ng-minlength="5", data-ng-maxlength="60") 11 | .form-group 12 | .radio.radio-inline 13 | label 14 | input(type="radio", name="role", data-ng-model="role", id="adminRole", data-ng-value="userRoles.admin") 15 | | Administrator 16 | .radio.radio-inline 17 | label 18 | input(type="radio", name="role", data-ng-model="role", id="adminRole", data-ng-value="userRoles.user") 19 | | Normal user 20 | .form-group 21 | .controls 22 | button.btn(type="submit", data-ng-disabled="registerForm.$invalid") Submit 23 | 24 | .alert.alert-danger(ng-show="registerForm.$invalid && registerForm.$dirty") 25 | strong Please correct the following errors: 26 | ul 27 | li(ng-show="registerForm.username.$error.required") Username is required 28 | li(ng-show="registerForm.username.$error.minlength") Username has to be at least 1 character long 29 | li(ng-show="registerForm.username.$error.maxlength") Username has to be at most 20 character long 30 | li(ng-show="registerForm.password.$error.required") Password is required 31 | li(ng-show="registerForm.password.$error.minlength") Password must be at least 5 characters long 32 | li(ng-show="registerForm.password.$error.maxlength") Password must be at most 60 characters long -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config){ 2 | config.set({ 3 | basePath : 'client', 4 | 5 | files : [ 6 | 'components/angular/angular.js', 7 | 'components/angular-cookies/angular-cookies.js', 8 | 'components/angular-mocks/angular-mocks.js', 9 | 'components/angular-ui-router/release/angular-ui-router.js', 10 | 'js/**/*.js', 11 | 'tests/unit/**/*.js' 12 | ], 13 | 14 | /*exclude : [ 15 | 'app/lib/angular/angular-loader.js', 16 | 'app/lib/angular/*.min.js', 17 | 'app/lib/angular/angular-scenario.js' 18 | ],*/ 19 | 20 | autoWatch : true, 21 | 22 | frameworks: ['mocha', 'chai'], 23 | 24 | browsers : ['Chrome'], 25 | 26 | plugins : [ 27 | 'karma-junit-reporter', 28 | 'karma-chrome-launcher', 29 | 'karma-firefox-launcher', 30 | 'karma-mocha', 31 | 'karma-chai' 32 | ], 33 | 34 | junitReporter : { 35 | outputFile: 'test_out/unit.xml', 36 | suite: 'unit' 37 | } 38 | 39 | }) 40 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-client-side-auth", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "body-parser": "~1.4.3", 6 | "bower": "~1.3.5", 7 | "cookie-parser": "~1.3.1", 8 | "cookie-session": "~1.0.2", 9 | "csurf": "~1.2.2", 10 | "express": "~4.4.4", 11 | "express-session": "~1.5.0", 12 | "jade": "~1.3.1", 13 | "method-override": "~2.0.2", 14 | "morgan": "~1.1.1", 15 | "passport": "~0.2.0", 16 | "passport-facebook": "~1.0.3", 17 | "passport-google": "~0.3.0", 18 | "passport-linkedin": "~0.1.3", 19 | "passport-local": "~1.0.0", 20 | "passport-twitter": "~1.0.2", 21 | "underscore": "~1.6.0", 22 | "validator": "~1.1.1" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/fnakstad/angular-client-side-auth" 27 | }, 28 | "subdomain": "angular-client-side-auth", 29 | "scripts": { 30 | "start": "grunt dev", 31 | "test": "grunt test", 32 | "postinstall": "bower install" 33 | }, 34 | "engines": { 35 | "node": "0.8.x" 36 | }, 37 | "devDependencies": { 38 | "chai": "~1.9.1", 39 | "grunt": "~0.4.5", 40 | "grunt-cli": "~0.1.13", 41 | "grunt-contrib-clean": "~0.5.0", 42 | "grunt-env": "~0.4.1", 43 | "grunt-mocha-test": "~0.11.0", 44 | "grunt-nodemon": "~0.2.1", 45 | "karma": "~0.12.16", 46 | "karma-chai": "0.1.0", 47 | "karma-chrome-launcher": "~0.1.4", 48 | "karma-coffee-preprocessor": "~0.2.1", 49 | "karma-firefox-launcher": "~0.1.3", 50 | "karma-html2js-preprocessor": "~0.1.0", 51 | "karma-junit-reporter": "~0.2.2", 52 | "karma-mocha": "~0.1.4", 53 | "karma-phantomjs-launcher": "~0.1.4", 54 | "karma-requirejs": "~0.2.2", 55 | "karma-script-launcher": "~0.1.0", 56 | "mocha": "~1.20.1", 57 | "nodemon": "~1.2.0", 58 | "passport-stub": "~1.0.0", 59 | "requirejs": "~2.1.14", 60 | "sinon": "~1.10.2", 61 | "supertest": "~0.13.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /run_server_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export NODE_ENV=test mocha 4 | mocha --recursive -R list server/tests/ -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | , http = require('http') 3 | , passport = require('passport') 4 | , path = require('path') 5 | , morgan = require('morgan') 6 | , bodyParser = require('body-parser') 7 | , methodOverride = require('method-override') 8 | , cookieParser = require('cookie-parser') 9 | , cookieSession = require('cookie-session') 10 | , session = require('express-session') 11 | , csrf = require('csurf') 12 | , User = require('./server/models/User.js'); 13 | 14 | var app = module.exports = express(); 15 | 16 | app.set('views', __dirname + '/client/views'); 17 | app.set('view engine', 'jade'); 18 | app.use(morgan('dev')); 19 | app.use(bodyParser.urlencoded({extended: true})); 20 | app.use(bodyParser.json()); 21 | app.use(methodOverride()); 22 | app.use(express.static(path.join(__dirname, 'client'))); 23 | app.use(cookieParser()); 24 | app.use(session( 25 | { 26 | secret: process.env.COOKIE_SECRET || "Superdupersecret" 27 | })); 28 | 29 | var env = process.env.NODE_ENV || 'development'; 30 | if ('development' === env || 'production' === env) { 31 | app.use(csrf()); 32 | app.use(function(req, res, next) { 33 | res.cookie('XSRF-TOKEN', req.csrfToken()); 34 | next(); 35 | }); 36 | } 37 | 38 | app.use(passport.initialize()); 39 | app.use(passport.session()); 40 | 41 | passport.use(User.localStrategy); 42 | //passport.use(User.twitterStrategy()); Uncomment this line if you don't want to enable login via Twitter 43 | //passport.use(User.facebookStrategy()); Uncomment this line if you don't want to enable login via Facebook 44 | //passport.use(User.googleStrategy()); Uncomment this line if you don't want to enable login via Google 45 | //passport.use(User.linkedInStrategy()); Uncomment this line if you don't want to enable login via LinkedIn 46 | 47 | passport.serializeUser(User.serializeUser); 48 | passport.deserializeUser(User.deserializeUser); 49 | 50 | require('./server/routes.js')(app); 51 | 52 | app.set('port', process.env.PORT || 8000); 53 | http.createServer(app).listen(app.get('port'), function(){ 54 | console.log("Express server listening on port " + app.get('port')); 55 | }); 56 | -------------------------------------------------------------------------------- /server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport') 2 | , User = require('../models/User.js'); 3 | 4 | module.exports = { 5 | register: function(req, res, next) { 6 | try { 7 | User.validate(req.body); 8 | } 9 | catch(err) { 10 | return res.send(400, err.message); 11 | } 12 | 13 | User.addUser(req.body.username, req.body.password, req.body.role, function(err, user) { 14 | if(err === 'UserAlreadyExists') return res.send(403, "User already exists"); 15 | else if(err) return res.send(500); 16 | 17 | req.logIn(user, function(err) { 18 | if(err) { next(err); } 19 | else { res.json(200, { "role": user.role, "username": user.username }); } 20 | }); 21 | }); 22 | }, 23 | 24 | login: function(req, res, next) { 25 | passport.authenticate('local', function(err, user) { 26 | 27 | if(err) { return next(err); } 28 | if(!user) { return res.send(400); } 29 | 30 | 31 | req.logIn(user, function(err) { 32 | if(err) { 33 | return next(err); 34 | } 35 | 36 | if(req.body.rememberme) req.session.cookie.maxAge = 1000 * 60 * 60 * 24 * 7; 37 | res.json(200, { "role": user.role, "username": user.username }); 38 | }); 39 | })(req, res, next); 40 | }, 41 | 42 | logout: function(req, res) { 43 | req.logout(); 44 | res.send(200); 45 | } 46 | }; -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore') 2 | , User = require('../models/User.js') 3 | , userRoles = require('../../client/js/routingConfig').userRoles; 4 | 5 | module.exports = { 6 | index: function(req, res) { 7 | var users = User.findAll(); 8 | _.each(users, function(user) { 9 | delete user.password; 10 | delete user.twitter; 11 | delete user.facebook; 12 | delete user.google; 13 | delete user.linkedin; 14 | }); 15 | res.json(users); 16 | } 17 | }; -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | var User 2 | , _ = require('underscore') 3 | , passport = require('passport') 4 | , LocalStrategy = require('passport-local').Strategy 5 | , TwitterStrategy = require('passport-twitter').Strategy 6 | , FacebookStrategy = require('passport-facebook').Strategy 7 | , GoogleStrategy = require('passport-google').Strategy 8 | , LinkedInStrategy = require('passport-linkedin').Strategy 9 | , check = require('validator').check 10 | , userRoles = require('../../client/js/routingConfig').userRoles; 11 | 12 | var users = [ 13 | { 14 | id: 1, 15 | username: "user", 16 | password: "123", 17 | role: userRoles.user 18 | }, 19 | { 20 | id: 2, 21 | username: "admin", 22 | password: "123", 23 | role: userRoles.admin 24 | } 25 | ]; 26 | 27 | module.exports = { 28 | addUser: function(username, password, role, callback) { 29 | if(this.findByUsername(username) !== undefined) return callback("UserAlreadyExists"); 30 | 31 | // Clean up when 500 users reached 32 | if(users.length > 500) { 33 | users = users.slice(0, 2); 34 | } 35 | 36 | var user = { 37 | id: _.max(users, function(user) { return user.id; }).id + 1, 38 | username: username, 39 | password: password, 40 | role: role 41 | }; 42 | users.push(user); 43 | callback(null, user); 44 | }, 45 | 46 | findOrCreateOauthUser: function(provider, providerId) { 47 | var user = module.exports.findByProviderId(provider, providerId); 48 | if(!user) { 49 | user = { 50 | id: _.max(users, function(user) { return user.id; }).id + 1, 51 | username: provider + '_user', // Should keep Oauth users anonymous on demo site 52 | role: userRoles.user, 53 | provider: provider 54 | }; 55 | user[provider] = providerId; 56 | users.push(user); 57 | } 58 | 59 | return user; 60 | }, 61 | 62 | findAll: function() { 63 | return _.map(users, function(user) { return _.clone(user); }); 64 | }, 65 | 66 | findById: function(id) { 67 | return _.clone(_.find(users, function(user) { return user.id === id })); 68 | }, 69 | 70 | findByUsername: function(username) { 71 | return _.clone(_.find(users, function(user) { return user.username === username; })); 72 | }, 73 | 74 | findByProviderId: function(provider, id) { 75 | return _.find(users, function(user) { return user[provider] === id; }); 76 | }, 77 | 78 | validate: function(user) { 79 | check(user.username, 'Username must be 1-20 characters long').len(1, 20); 80 | check(user.password, 'Password must be 5-60 characters long').len(5, 60); 81 | check(user.username, 'Invalid username').not(/((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/); 82 | 83 | // TODO: Seems node-validator's isIn function doesn't handle Number arrays very well... 84 | // Till this is rectified Number arrays must be converted to string arrays 85 | // https://github.com/chriso/node-validator/issues/185 86 | var stringArr = _.map(_.values(userRoles), function(val) { return val.toString() }); 87 | check(user.role, 'Invalid user role given').isIn(stringArr); 88 | }, 89 | 90 | localStrategy: new LocalStrategy( 91 | function(username, password, done) { 92 | 93 | var user = module.exports.findByUsername(username); 94 | 95 | if(!user) { 96 | done(null, false, { message: 'Incorrect username.' }); 97 | } 98 | else if(user.password != password) { 99 | done(null, false, { message: 'Incorrect username.' }); 100 | } 101 | else { 102 | return done(null, user); 103 | } 104 | 105 | } 106 | ), 107 | 108 | twitterStrategy: function() { 109 | if(!process.env.TWITTER_CONSUMER_KEY) throw new Error('A Twitter Consumer Key is required if you want to enable login via Twitter.'); 110 | if(!process.env.TWITTER_CONSUMER_SECRET) throw new Error('A Twitter Consumer Secret is required if you want to enable login via Twitter.'); 111 | 112 | return new TwitterStrategy({ 113 | consumerKey: process.env.TWITTER_CONSUMER_KEY, 114 | consumerSecret: process.env.TWITTER_CONSUMER_SECRET, 115 | callbackURL: process.env.TWITTER_CALLBACK_URL || 'http://localhost:8000/auth/twitter/callback' 116 | }, 117 | function(token, tokenSecret, profile, done) { 118 | var user = module.exports.findOrCreateOauthUser(profile.provider, profile.id); 119 | done(null, user); 120 | }); 121 | }, 122 | 123 | facebookStrategy: function() { 124 | if(!process.env.FACEBOOK_APP_ID) throw new Error('A Facebook App ID is required if you want to enable login via Facebook.'); 125 | if(!process.env.FACEBOOK_APP_SECRET) throw new Error('A Facebook App Secret is required if you want to enable login via Facebook.'); 126 | 127 | return new FacebookStrategy({ 128 | clientID: process.env.FACEBOOK_APP_ID, 129 | clientSecret: process.env.FACEBOOK_APP_SECRET, 130 | callbackURL: process.env.FACEBOOK_CALLBACK_URL || "http://localhost:8000/auth/facebook/callback" 131 | }, 132 | function(accessToken, refreshToken, profile, done) { 133 | var user = module.exports.findOrCreateOauthUser(profile.provider, profile.id); 134 | done(null, user); 135 | }); 136 | }, 137 | 138 | googleStrategy: function() { 139 | 140 | return new GoogleStrategy({ 141 | returnURL: process.env.GOOGLE_RETURN_URL || "http://localhost:8000/auth/google/return", 142 | realm: process.env.GOOGLE_REALM || "http://localhost:8000/" 143 | }, 144 | function(identifier, profile, done) { 145 | var user = module.exports.findOrCreateOauthUser('google', identifier); 146 | done(null, user); 147 | }); 148 | }, 149 | 150 | linkedInStrategy: function() { 151 | if(!process.env.LINKED_IN_KEY) throw new Error('A LinkedIn App Key is required if you want to enable login via LinkedIn.'); 152 | if(!process.env.LINKED_IN_SECRET) throw new Error('A LinkedIn App Secret is required if you want to enable login via LinkedIn.'); 153 | 154 | return new LinkedInStrategy({ 155 | consumerKey: process.env.LINKED_IN_KEY, 156 | consumerSecret: process.env.LINKED_IN_SECRET, 157 | callbackURL: process.env.LINKED_IN_CALLBACK_URL || "http://localhost:8000/auth/linkedin/callback" 158 | }, 159 | function(token, tokenSecret, profile, done) { 160 | var user = module.exports.findOrCreateOauthUser('linkedin', profile.id); 161 | done(null,user); 162 | } 163 | ); 164 | }, 165 | serializeUser: function(user, done) { 166 | done(null, user.id); 167 | }, 168 | 169 | deserializeUser: function(id, done) { 170 | var user = module.exports.findById(id); 171 | 172 | if(user) { done(null, user); } 173 | else { done(null, false); } 174 | } 175 | }; -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore') 2 | , path = require('path') 3 | , passport = require('passport') 4 | , AuthCtrl = require('./controllers/auth') 5 | , UserCtrl = require('./controllers/user') 6 | , User = require('./models/User.js') 7 | , userRoles = require('../client/js/routingConfig').userRoles 8 | , accessLevels = require('../client/js/routingConfig').accessLevels; 9 | 10 | var routes = [ 11 | 12 | // Views 13 | { 14 | path: '/partials/*', 15 | httpMethod: 'GET', 16 | middleware: [function (req, res) { 17 | var requestedView = path.join('./', req.url); 18 | res.render(requestedView); 19 | }] 20 | }, 21 | 22 | // OAUTH 23 | { 24 | path: '/auth/twitter', 25 | httpMethod: 'GET', 26 | middleware: [passport.authenticate('twitter')] 27 | }, 28 | { 29 | path: '/auth/twitter/callback', 30 | httpMethod: 'GET', 31 | middleware: [passport.authenticate('twitter', { 32 | successRedirect: '/', 33 | failureRedirect: '/login' 34 | })] 35 | }, 36 | { 37 | path: '/auth/facebook', 38 | httpMethod: 'GET', 39 | middleware: [passport.authenticate('facebook')] 40 | }, 41 | { 42 | path: '/auth/facebook/callback', 43 | httpMethod: 'GET', 44 | middleware: [passport.authenticate('facebook', { 45 | successRedirect: '/', 46 | failureRedirect: '/login' 47 | })] 48 | }, 49 | { 50 | path: '/auth/google', 51 | httpMethod: 'GET', 52 | middleware: [passport.authenticate('google')] 53 | }, 54 | { 55 | path: '/auth/google/return', 56 | httpMethod: 'GET', 57 | middleware: [passport.authenticate('google', { 58 | successRedirect: '/', 59 | failureRedirect: '/login' 60 | })] 61 | }, 62 | { 63 | path: '/auth/linkedin', 64 | httpMethod: 'GET', 65 | middleware: [passport.authenticate('linkedin')] 66 | }, 67 | { 68 | path: '/auth/linkedin/callback', 69 | httpMethod: 'GET', 70 | middleware: [passport.authenticate('linkedin', { 71 | successRedirect: '/', 72 | failureRedirect: '/login' 73 | })] 74 | }, 75 | 76 | // Local Auth 77 | { 78 | path: '/register', 79 | httpMethod: 'POST', 80 | middleware: [AuthCtrl.register] 81 | }, 82 | { 83 | path: '/login', 84 | httpMethod: 'POST', 85 | middleware: [AuthCtrl.login] 86 | }, 87 | { 88 | path: '/logout', 89 | httpMethod: 'POST', 90 | middleware: [AuthCtrl.logout] 91 | }, 92 | 93 | // User resource 94 | { 95 | path: '/users', 96 | httpMethod: 'GET', 97 | middleware: [UserCtrl.index], 98 | accessLevel: accessLevels.admin 99 | }, 100 | 101 | // All other get requests should be handled by AngularJS's client-side routing system 102 | { 103 | path: '/*', 104 | httpMethod: 'GET', 105 | middleware: [function(req, res) { 106 | var role = userRoles.public, username = ''; 107 | if(req.user) { 108 | role = req.user.role; 109 | username = req.user.username; 110 | } 111 | res.cookie('user', JSON.stringify({ 112 | 'username': username, 113 | 'role': role 114 | })); 115 | res.render('index'); 116 | }] 117 | } 118 | ]; 119 | 120 | module.exports = function(app) { 121 | 122 | _.each(routes, function(route) { 123 | route.middleware.unshift(ensureAuthorized); 124 | var args = _.flatten([route.path, route.middleware]); 125 | 126 | switch(route.httpMethod.toUpperCase()) { 127 | case 'GET': 128 | app.get.apply(app, args); 129 | break; 130 | case 'POST': 131 | app.post.apply(app, args); 132 | break; 133 | case 'PUT': 134 | app.put.apply(app, args); 135 | break; 136 | case 'DELETE': 137 | app.delete.apply(app, args); 138 | break; 139 | default: 140 | throw new Error('Invalid HTTP method specified for route ' + route.path); 141 | break; 142 | } 143 | }); 144 | } 145 | 146 | function ensureAuthorized(req, res, next) { 147 | var role; 148 | if(!req.user) role = userRoles.public; 149 | else role = req.user.role; 150 | var accessLevel = _.findWhere(routes, { path: req.route.path, httpMethod: req.route.stack[0].method.toUpperCase() }).accessLevel || accessLevels.public; 151 | 152 | if(!(accessLevel.bitMask & role.bitMask)) return res.send(403); 153 | return next(); 154 | } 155 | -------------------------------------------------------------------------------- /server/tests/integration/index.spec.js: -------------------------------------------------------------------------------- 1 | var app = require('../../../server'), 2 | request = require('supertest'), 3 | passportStub = require('passport-stub'); 4 | passportStub.install(app); 5 | 6 | // user account 7 | var user = { 8 | 'username':'newUser', 9 | 'role':{bitMask: 2,title: "user"}, 10 | 'password':'12345' 11 | }; 12 | 13 | // user account 2 - no role 14 | var user2 = { 15 | 'username':'newUser', 16 | 'password':'12345' 17 | }; 18 | 19 | // admin account 20 | var admin = { 21 | 'username':'admin', 22 | 'role': { bitMask: 4, title: 'admin' }, 23 | 'id': '2', 24 | 'password':'123' 25 | }; 26 | 27 | describe('Server Integration Tests - ', function (done) { 28 | afterEach(function() { 29 | passportStub.logout(); // logout after each test 30 | }); 31 | it('Homepage - Return a 200', function(done) { 32 | request(app).get('/').expect(200, done); 33 | }); 34 | it('Logout - Return a 200', function(done) { 35 | request(app).post('/logout').expect(200, done); 36 | }); 37 | it('As a Logout user, on /users - Return a 403', function(done) { 38 | request(app).get('/users').expect(403, done); 39 | }); 40 | it('Register a new user(no role) - Return a 400', function(done) { 41 | request(app).post('/register').send(user2).expect(400, done); 42 | }); 43 | it('Register a new user - Return a 200', function(done) { 44 | request(app).post('/register').send(user).expect(200, done); 45 | }); 46 | it('As a normal user, on /users - Return a 403', function(done) { 47 | passportStub.login(user); // login as user 48 | request(app).get('/users').expect(403, done); 49 | }); 50 | it('Login as Admin - Return a 200', function(done) { 51 | request(app).post('/login').send(admin).expect(200, done); 52 | }); 53 | it('As a Admin user, on /users - Return a 200', function(done) { 54 | passportStub.login(admin); // login as admin 55 | request(app).get('/users').expect(200, done); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /server/tests/unit/controllers/auth.spec.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | , sinon = require('sinon') 3 | , AuthCtrl = require('../../../controllers/auth') 4 | , User = require('../../../models/User'); 5 | 6 | describe('Auth controller Unit Tests - ', function() { 7 | 8 | var req = { } 9 | , res = {} 10 | , next = {} 11 | , sandbox = sinon.sandbox.create(); 12 | 13 | beforeEach(function() { 14 | 15 | }); 16 | 17 | afterEach(function() { 18 | sandbox.restore(); 19 | }); 20 | 21 | describe('register()', function() { 22 | 23 | beforeEach(function() { 24 | req.body = { 25 | username: "user", 26 | password: "pass", 27 | role: 1 28 | }; 29 | }); 30 | 31 | it('should return a 400 when user validation fails', function(done) { 32 | 33 | var userValidateStub = sandbox.stub(User, 'validate').throws(); 34 | res.send = function(httpStatus) { 35 | expect(httpStatus).to.equal(400); 36 | done(); 37 | }; 38 | 39 | AuthCtrl.register(req, res, next); 40 | }); 41 | 42 | it('should return a 403 when UserAlreadyExists error is returned from User.addUser()', function(done) { 43 | var userValidateStub = sandbox.stub(User, 'validate').returns(); 44 | var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) { 45 | callback('UserAlreadyExists'); 46 | }); 47 | 48 | res.send = function(httpStatus) { 49 | expect(httpStatus).to.equal(403); 50 | done(); 51 | }; 52 | 53 | AuthCtrl.register(req, res, next); 54 | }); 55 | 56 | it('should return a 500 if error other than UserAlreadyExists is returned from User.addUser()', function(done) { 57 | var userValidateStub = sandbox.stub(User, 'validate').returns(); 58 | var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) { 59 | callback('SomeError'); 60 | }); 61 | 62 | res.send = function(httpStatus) { 63 | expect(httpStatus).to.equal(500); 64 | done(); 65 | }; 66 | 67 | AuthCtrl.register(req, res, next); 68 | }); 69 | 70 | it('should call next() with an error argument if req.logIn() returns error', function(done) { 71 | var userValidateStub = sandbox.stub(User, 'validate').returns(); 72 | var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) { 73 | callback(null, req.body); 74 | }); 75 | req.logIn = function(user, callback) { return callback('SomeError'); }; 76 | 77 | next = function(err) { 78 | expect(err).to.exist; 79 | done(); 80 | }; 81 | 82 | AuthCtrl.register(req, res, next); 83 | }); 84 | 85 | it('should return a 200 with a username and role in the response body', function(done) { 86 | var userValidateStub = sandbox.stub(User, 'validate').returns(); 87 | var userAddUserStub = sandbox.stub(User, 'addUser', function(username, password, role, callback) { 88 | callback(null, req.body); 89 | }); 90 | req.logIn = function(user, callback) { return callback(null); }; 91 | 92 | res.json = function(httpStatus, user) { 93 | expect(httpStatus).to.equal(200); 94 | expect(user.username).to.exist; 95 | expect(user.role).to.exist; 96 | done(); 97 | }; 98 | 99 | AuthCtrl.register(req, res, next); 100 | }); 101 | }); 102 | }); --------------------------------------------------------------------------------