├── .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 | [](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 | });
--------------------------------------------------------------------------------