From 9e9ee48918d5136cb2ba736dc5af410fdcdd7c25 Mon Sep 17 00:00:00 2001 From: Nikita Konovalov Date: Thu, 13 Feb 2014 13:39:31 +0400 Subject: [PATCH] Auth support - Created StringUtil class with some useful random string methods. - Create UrlUtil class with useful URL manipulation and builder methods. - Cleaned up some unused libraries (cookies, mocks) from index.html - Added LocalStorage dependency. - Added advanced routing to auth module for OAuth response routing. - Added state resolver methods so we can enforce UI states that require certain session states. - Removed AuthProvider resolver and resource, as they're no longer necessary. - Updated header to point to correct routes. - Updated header to correctly represent state. - Added busy template for "pending" activity. This shouldn't actually show up because the javascript will resolve the view logic too quickly, but it's included for the sake of completion. - Added error state in case we get an error response from the server. It's very basic. - Added request interceptor that attaches an access token to every request if a valid access token exists. - Added OpenId service to handle our redirection and token resolution. - Added Deauthorization (logout) controller. - Added session management controller. - Added search param provider to inject non-hashbang query parameters. Change-Id: Id9b1e7fe9ed98ad4be0a80f1acd4a9e125ec57c9 --- bower.json | 4 +- karma-integration.conf.js | 5 +- karma-unit.conf.js | 5 +- .../controller/auth_authorize_controller.js | 40 +++++ ...ller.js => auth_deauthorize_controller.js} | 18 +- .../controller/auth_error_controller.js} | 23 ++- .../auth/controller/auth_token_controller.js | 53 ++++++ .../auth/http/http_authorization_header.js | 44 +++++ src/app/auth/module.js | 58 +++--- src/app/auth/provider/session_state.js | 39 ++++ src/app/auth/resolver/session_resolver.js | 85 +++++++++ src/app/auth/service/access_token.js | 160 +++++++++++++++++ src/app/auth/service/current_user.js | 59 ++++++ src/app/auth/service/open_id.js | 106 +++++++++++ src/app/auth/service/session.js | 170 ++++++++++++++++++ src/app/services/module.js | 4 +- .../resolver/auth_provider_resolver.js | 84 --------- .../controllers/header_controller.js | 27 ++- src/app/storyboard/module.js | 4 +- .../auth/{provider/login.html => busy.html} | 10 +- src/app/templates/auth/error.html | 42 +++++ src/app/templates/auth/provider/list.html | 33 ---- src/app/templates/header.html | 16 +- src/app/util/helpers/string_util.js | 65 +++++++ src/app/util/helpers/url_util.js | 87 +++++++++ .../provider/search_param_provider.js} | 28 +-- src/index.html | 3 +- test/unit/services/module.js | 4 +- 28 files changed, 1067 insertions(+), 209 deletions(-) create mode 100644 src/app/auth/controller/auth_authorize_controller.js rename src/app/auth/controller/{auth_login_controller.js => auth_deauthorize_controller.js} (64%) rename src/app/{services/resource/auth_provider.js => auth/controller/auth_error_controller.js} (50%) create mode 100644 src/app/auth/controller/auth_token_controller.js create mode 100644 src/app/auth/http/http_authorization_header.js create mode 100644 src/app/auth/provider/session_state.js create mode 100644 src/app/auth/resolver/session_resolver.js create mode 100644 src/app/auth/service/access_token.js create mode 100644 src/app/auth/service/current_user.js create mode 100644 src/app/auth/service/open_id.js create mode 100644 src/app/auth/service/session.js delete mode 100644 src/app/services/resolver/auth_provider_resolver.js rename src/app/templates/auth/{provider/login.html => busy.html} (65%) create mode 100644 src/app/templates/auth/error.html delete mode 100644 src/app/templates/auth/provider/list.html create mode 100644 src/app/util/helpers/string_util.js create mode 100644 src/app/util/helpers/url_util.js rename src/app/{auth/controller/auth_list_controller.js => util/provider/search_param_provider.js} (54%) diff --git a/bower.json b/bower.json index 80d355de..9080388d 100644 --- a/bower.json +++ b/bower.json @@ -6,11 +6,11 @@ "font-awesome": "4.0", "angular": "1.2.13", "angular-resource": "1.2.13", - "angular-cookies": "1.2.13", "angular-sanitize": "1.2.13", "bootstrap": "3.1.0", "angular-ui-router": "0.2.8-bowratic-tedium", - "angular-bootstrap": "0.10.0" + "angular-bootstrap": "0.10.0", + "angular-local-storage": "0.0.1" }, "devDependencies": { "angular-mocks": "1.2.13", diff --git a/karma-integration.conf.js b/karma-integration.conf.js index f042eaff..df7b977b 100644 --- a/karma-integration.conf.js +++ b/karma-integration.conf.js @@ -35,7 +35,10 @@ module.exports = function (config) { ], files: [ - './dist/js/*.js', + './dist/js/libs.js', + './bower_components/angular-mocks/angular-mocks.js', + './dist/js/storyboard.js', + './dist/js/templates.js', './test/unit/**/*.js' ], diff --git a/karma-unit.conf.js b/karma-unit.conf.js index ccfdd551..29b3c7d0 100644 --- a/karma-unit.conf.js +++ b/karma-unit.conf.js @@ -35,7 +35,10 @@ module.exports = function (config) { ], files: [ - './dist/js/*.js', + './dist/js/libs.js', + './bower_components/angular-mocks/angular-mocks.js', + './dist/js/storyboard.js', + './dist/js/templates.js', './test/unit/**/*.js' ], diff --git a/src/app/auth/controller/auth_authorize_controller.js b/src/app/auth/controller/auth_authorize_controller.js new file mode 100644 index 00000000..2d9d535c --- /dev/null +++ b/src/app/auth/controller/auth_authorize_controller.js @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * This controller is responsible for getting an authorization code + * having a state and an openid. + * + * @author Nikita Konovalov + */ + +angular.module('sb.auth').controller('AuthAuthorizeController', + function ($stateParams, $state, $log, OpenId) { + 'use strict'; + + // First, check for the edge case where the API returns an error code + // back to us. This should only happen when it fails to properly parse + // our redirect_uri and thus just sends the error back to referrer, but + // we should still catch it. + if (!!$stateParams.error) { + $log.debug('Error received, redirecting to auth.error.'); + $state.go('auth.error', $stateParams); + return; + } + + // We're not an error, let's fire the authorization. + OpenId.authorize(); + }); \ No newline at end of file diff --git a/src/app/auth/controller/auth_login_controller.js b/src/app/auth/controller/auth_deauthorize_controller.js similarity index 64% rename from src/app/auth/controller/auth_login_controller.js rename to src/app/auth/controller/auth_deauthorize_controller.js index 4051c63f..3374a914 100644 --- a/src/app/auth/controller/auth_login_controller.js +++ b/src/app/auth/controller/auth_deauthorize_controller.js @@ -5,7 +5,7 @@ * not use this file except in compliance with the License. You may obtain * a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT @@ -15,14 +15,14 @@ */ /** - * This controller handles the logic for the authorization provider list page. - * - * @author Michael Krotscheck + * This controller deauthorizes the session and destroys all tokens. */ -angular.module('sb.auth').controller('AuthLoginController', - function ($scope, authProvider) { + +angular.module('sb.auth').controller('AuthDeauthorizeController', + function (Session, $state, $log) { 'use strict'; - $scope.authProvider = authProvider; - - }); + $log.debug('Logging out'); + Session.destroySession(); + $state.go('index'); + }); \ No newline at end of file diff --git a/src/app/services/resource/auth_provider.js b/src/app/auth/controller/auth_error_controller.js similarity index 50% rename from src/app/services/resource/auth_provider.js rename to src/app/auth/controller/auth_error_controller.js index 4a4411ec..9b0db5fe 100644 --- a/src/app/services/resource/auth_provider.js +++ b/src/app/auth/controller/auth_error_controller.js @@ -5,7 +5,7 @@ * not use this file except in compliance with the License. You may obtain * a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT @@ -15,19 +15,16 @@ */ /** - * This resource exposes authorization providers to our angularjs environment, - * allowing us to manage & control them. It's also used during the - * authorization/login process to determine how we're going to allow users to - * log in to storyboard. - * - * @author Michael Krotscheck + * View controller for authorization error conditions. */ - -angular.module('sb.services').factory('AuthProvider', - function ($resource, storyboardApiBase, storyboardApiSignature) { +angular.module('sb.auth').controller('AuthErrorController', + function ($scope, $stateParams) { 'use strict'; - return $resource(storyboardApiBase + '/auth/provider/:id', - {id: '@id'}, - storyboardApiSignature); + console.warn('AuthErrorController'); + + + $scope.error = $stateParams.error || 'Unknown'; + $scope.errorDescription = $stateParams.error_description || + 'No description received from server.'; }); \ No newline at end of file diff --git a/src/app/auth/controller/auth_token_controller.js b/src/app/auth/controller/auth_token_controller.js new file mode 100644 index 00000000..af8f7d54 --- /dev/null +++ b/src/app/auth/controller/auth_token_controller.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2014 Mirantis Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * This controller is responsible for getting an access_token and + * a refresh token having an authorization_code. + * + * @author Nikita Konovalov + */ + +angular.module('sb.auth').controller('AuthTokenController', + function ($state, $log, OpenId, Session, $searchParams) { + 'use strict'; + + // First, check for the edge case where the API returns an error code + // back to us. This should only happen when it fails to properly parse + // our redirect_uri and thus just sends the error back to referrer, but + // we should still catch it. + if (!!$searchParams.error) { + $log.debug('Error received, redirecting to auth.error.'); + $state.go('auth.error', $searchParams); + return; + } + + // Looks like there's no error, so let's see if we can resolve a token. + // TODO: Finish implementing. + OpenId.token($searchParams) + .then( + function (token) { + Session.updateSession(token) + .then(function () { + $state.go('index'); + }); + }, + function (error) { + Session.destroySession(); + $state.go('auth.error', error); + } + ); + }); \ No newline at end of file diff --git a/src/app/auth/http/http_authorization_header.js b/src/app/auth/http/http_authorization_header.js new file mode 100644 index 00000000..0d31a472 --- /dev/null +++ b/src/app/auth/http/http_authorization_header.js @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * An HTTP request interceptor that attaches an authorization to every HTTP + * request, assuming it exists and isn't expired. + */ +angular.module('sb.auth').factory('httpAuthorizationHeader', + function (AccessToken) { + 'use strict'; + + return { + request: function (request) { + + // TODO(krotscheck): Only apply the token to requests to + // storyboardApiBase. + var token = AccessToken.getAccessToken(); + var type = AccessToken.getTokenType(); + if (!!token && !AccessToken.isExpired()) { + request.headers.Authorization = type + ' ' + token; + } + return request; + } + }; + }) + // Attach the HTTP interceptor. + .config(function ($httpProvider) { + 'use strict'; + + $httpProvider.interceptors.push('httpAuthorizationHeader'); + }); \ No newline at end of file diff --git a/src/app/auth/module.js b/src/app/auth/module.js index e6df7717..5b62be69 100644 --- a/src/app/auth/module.js +++ b/src/app/auth/module.js @@ -15,48 +15,48 @@ */ /** - * This Storyboard module contains our adaptive authentication and authorization - * logic. - * - * @author Michael Krotscheck + * This Storyboard module contains our authentication and authorization logic. */ -angular.module('sb.auth', - [ 'sb.services', 'sb.templates', 'ui.router'] +angular.module('sb.auth', [ 'sb.services', 'sb.templates', 'ui.router', + 'sb.util', 'LocalStorageModule'] ) - .config(function ($stateProvider, $urlRouterProvider, - AuthProviderResolver) { + .config(function ($stateProvider, SessionResolver) { 'use strict'; - // Default rerouting. - $urlRouterProvider.when('/auth', '/auth/provider/list'); - $urlRouterProvider.when('/auth/provider', '/auth/provider/list'); - // Declare the states for this module. $stateProvider .state('auth', { abstract: true, - url: '/auth', - template: '
' + template: '
', + url: '/auth' }) - .state('auth.provider', { - abstract: true, - url: '/provider', - template: '
' - }) - .state('auth.provider.list', { - url: '/list', - templateUrl: 'app/templates/auth/provider/list.html', - controller: 'AuthListController', + .state('auth.authorize', { + url: '/authorize?error&error_description', + templateUrl: 'app/templates/auth/busy.html', + controller: 'AuthAuthorizeController', resolve: { - authProviders: AuthProviderResolver.resolveAuthProviders + isLoggedOut: SessionResolver.requireLoggedOut } }) - .state('auth.provider.id', { - url: '/:id', - templateUrl: 'app/templates/auth/provider/login.html', - controller: 'AuthLoginController', + .state('auth.deauthorize', { + url: '/deauthorize', + templateUrl: 'app/templates/auth/busy.html', + controller: 'AuthDeauthorizeController', resolve: { - authProvider: AuthProviderResolver.resolveAuthProvider('id') + isLoggedIn: SessionResolver.requireLoggedIn } + }) + .state('auth.token', { + url: '/token?code&state&error&error_description', + templateUrl: 'app/templates/auth/busy.html', + controller: 'AuthTokenController', + resolve: { + isLoggedOut: SessionResolver.requireLoggedOut + } + }) + .state('auth.error', { + url: '/error?error&error_description', + templateUrl: 'app/templates/auth/error.html', + controller: 'AuthErrorController' }); }); \ No newline at end of file diff --git a/src/app/auth/provider/session_state.js b/src/app/auth/provider/session_state.js new file mode 100644 index 00000000..03e3d66a --- /dev/null +++ b/src/app/auth/provider/session_state.js @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * A list of constants used by the session service to maintain the user's + * current authentication state. + */ +angular.module('sb.auth').value('SessionState', { + + /** + * Session state constant, used to indicate that the user is logged in. + */ + LOGGED_IN: 'logged_in', + + /** + * Session state constant, used to indicate that the user is logged out. + */ + LOGGED_OUT: 'logged_out', + + /** + * Session state constant, used during initialization when we're not quite + * certain yet whether we're logged in or logged out. + */ + PENDING: 'pending' + +}); \ No newline at end of file diff --git a/src/app/auth/resolver/session_resolver.js b/src/app/auth/resolver/session_resolver.js new file mode 100644 index 00000000..5f514beb --- /dev/null +++ b/src/app/auth/resolver/session_resolver.js @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * A set of utility methods that may be used during state declaration to enforce + * session state. They return asynchronous promises which will either resolve + * or reject the state change, depending on what you're asking for. + */ +angular.module('sb.auth').constant('SessionResolver', + (function () { + 'use strict'; + + /** + * Resolve the promise based on the current session state. We can't + * inject here, since the injector's not ready yet. + */ + function resolveSessionState(deferred, desiredSessionState, Session) { + return function () { + var sessionState = Session.getSessionState(); + if (sessionState === desiredSessionState) { + deferred.resolve(sessionState); + } else { + deferred.reject(sessionState); + } + }; + } + + return { + /** + * This resolver asserts that the user is logged + * out before allowing a route. Otherwise it fails. + */ + requireLoggedOut: function ($q, $log, Session, SessionState) { + + $log.debug('Resolving logged-out-only route...'); + var deferred = $q.defer(); + var resolveLoggedOut = resolveSessionState(deferred, + SessionState.LOGGED_OUT, Session); + + // Do we have to wait for state resolution? + if (Session.getSessionState() === SessionState.PENDING) { + Session.resolveSessionState().then(resolveLoggedOut); + } else { + resolveLoggedOut(); + } + + return deferred.promise; + }, + + /** + * This resolver asserts that the user is logged + * in before allowing a route. Otherwise it fails. + */ + requireLoggedIn: function ($q, $log, Session, $rootScope, + SessionState) { + + $log.debug('Resolving logged-in-only route...'); + var deferred = $q.defer(); + var resolveLoggedIn = resolveSessionState(deferred, + SessionState.LOGGED_IN, Session); + + // Do we have to wait for state resolution? + if (Session.getSessionState() === SessionState.PENDING) { + Session.resolveSessionState().then(resolveLoggedIn); + } else { + resolveLoggedIn(); + } + + return deferred.promise; + } + }; + })()); diff --git a/src/app/auth/service/access_token.js b/src/app/auth/service/access_token.js new file mode 100644 index 00000000..46a5c8d0 --- /dev/null +++ b/src/app/auth/service/access_token.js @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * AccessToken storage service, an abstraction layer between our token storage + * and the rest of the system. This feature uses localStorage, which means that + * our application will NOT support IE7. Once that becomes a requirement, we'll + * have to use this abstraction layer to store data in a cookie instead. + */ +angular.module('sb.auth').factory('AccessToken', + function (localStorageService) { + 'use strict'; + + /** + * Our local storage key name constants + */ + var TOKEN_TYPE = 'token_type'; + var ACCESS_TOKEN = 'access_token'; + var REFRESH_TOKEN = 'refresh_token'; + var ID_TOKEN = 'id_token'; + var EXPIRES_IN = 'expires_in'; + var ISSUE_DATE = 'issue_date'; + + return { + + /** + * Clears the token + */ + clear: function () { + localStorageService.remove(TOKEN_TYPE); + localStorageService.remove(ACCESS_TOKEN); + localStorageService.remove(REFRESH_TOKEN); + localStorageService.remove(ID_TOKEN); + localStorageService.remove(EXPIRES_IN); + localStorageService.remove(ISSUE_DATE); + }, + + /** + * Sets all token properties at once. + */ + setToken: function (jsonToken) { + this.setTokenType(jsonToken.token_type); + this.setAccessToken(jsonToken.access_token); + this.setRefreshToken(jsonToken.refresh_token); + this.setIdToken(jsonToken.id_token); + this.setIssueDate(jsonToken.issue_date); + this.setExpiresIn(jsonToken.expires_in); + }, + + /** + * Is the current access token expired? + */ + isExpired: function () { + var expiresIn = this.getExpiresIn() || 0; + var issueDate = this.getIssueDate() || 0; + var now = Math.round((new Date()).getTime() / 1000); + + return issueDate + expiresIn < now; + }, + + /** + * Get the token type. Bearer, etc. + */ + getTokenType: function () { + return localStorageService.get(TOKEN_TYPE); + }, + + /** + * Set the token type. + */ + setTokenType: function (value) { + return localStorageService.set(TOKEN_TYPE, value); + }, + + /** + * Retrieve the date this token was issued. + */ + getIssueDate: function () { + return localStorageService.get(ISSUE_DATE) || null; + }, + + /** + * Set the issue date for the current access token. + */ + setIssueDate: function (value) { + return localStorageService.set(ISSUE_DATE, value); + }, + + /** + * Get the number of seconds after the issue date when this token + * is considered expired. + */ + getExpiresIn: function () { + return localStorageService.get(EXPIRES_IN) || 0; + }, + + /** + * Set the number of seconds from the issue date when this token + * will expire. + */ + setExpiresIn: function (value) { + return localStorageService.set(EXPIRES_IN, value); + }, + + /** + * Retrieve the access token. + */ + getAccessToken: function () { + return localStorageService.get(ACCESS_TOKEN) || null; + }, + + /** + * Set the access token. + */ + setAccessToken: function (value) { + return localStorageService.set(ACCESS_TOKEN, value); + }, + + /** + * Retrieve the refresh token. + */ + getRefreshToken: function () { + return localStorageService.get(REFRESH_TOKEN) || null; + }, + + /** + * Set the refresh token. + */ + setRefreshToken: function (value) { + return localStorageService.set(REFRESH_TOKEN, value); + }, + + /** + * Retrieve the id token. + */ + getIdToken: function () { + return localStorageService.get(ID_TOKEN) || null; + }, + + /** + * Set the id token. + */ + setIdToken: function (value) { + return localStorageService.set(ID_TOKEN, value); + } + }; + }); \ No newline at end of file diff --git a/src/app/auth/service/current_user.js b/src/app/auth/service/current_user.js new file mode 100644 index 00000000..6faa5bff --- /dev/null +++ b/src/app/auth/service/current_user.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * The current user service. It pays attention to changes in the application's + * session state, and loads the user found in the AccessToken when a valid + * session is detected. + */ +angular.module('sb.auth').factory('CurrentUser', + function (SessionState, Session, AccessToken, $rootScope, $log, $q, User) { + 'use strict'; + + /** + * The current user + */ + var currentUser = null; + + /** + * Load the current user, if such exists. + */ + function loadCurrentUser() { + if (Session.getSessionState() === SessionState.LOGGED_IN) { + var userId = AccessToken.getIdToken(); + + $log.debug('Loading Current User ' + userId); + currentUser = User.get({id: userId}); + } else { + currentUser = null; + } + } + + $rootScope.$on(SessionState.LOGGED_IN, loadCurrentUser); + $rootScope.$on(SessionState.LOGGED_OUT, loadCurrentUser); + + loadCurrentUser(); + + // Expose the methods for this service. + return { + /** + * Retrieve the current user. + */ + get: function () { + return currentUser; + } + }; + }); \ No newline at end of file diff --git a/src/app/auth/service/open_id.js b/src/app/auth/service/open_id.js new file mode 100644 index 00000000..37726383 --- /dev/null +++ b/src/app/auth/service/open_id.js @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * Our OpenID token resource, which adheres to the OpenID connect specification + * found here; http://openid.net/specs/openid-connect-basic-1_0.html + */ +angular.module('sb.auth').factory('OpenId', + function ($location, $window, $log, $http, $q, StringUtil, UrlUtil, + storyboardApiBase, localStorageService) { + 'use strict'; + + var storageKey = 'openid_authorize_state'; + var authorizeUrl = storyboardApiBase + '/openid/authorize'; + var tokenUrl = storyboardApiBase + '/openid/token'; + var redirectUri = UrlUtil.buildApplicationUrl('/auth/token'); + var clientId = $location.host(); + + return { + /** + * Asks the OAuth endpoint for an authorization token given + * the passed parameters. + */ + authorize: function () { + // Create and store a random state parameter. + var state = StringUtil.randomAlphaNumeric(20); + localStorageService.set(storageKey, state); + + var openIdParams = { + response_type: 'code', + client_id: clientId, + redirect_uri: redirectUri, + scope: 'user', + state: state + }; + + $window.location.href = authorizeUrl + '?' + + UrlUtil.serializeParameters(openIdParams); + }, + + /** + * Asks our OpenID endpoint to convert an authorization token to + * an access token. + */ + token: function (params) { + var deferred = $q.defer(); + var authorizationCode = params.code; + + var tokenParams = { + grant_type: 'authorization_code', + code: authorizationCode + }; + + var url = tokenUrl + '?' + + UrlUtil.serializeParameters(tokenParams); + + $http({method: 'POST', url: url}) + .then(function (response) { + $log.debug('Token creation succeeded.'); + // Extract the data + var data = response.data; + + // Derive an issue date, from the Date header if + // possible. + var dateHeader = response.headers('Date'); + if (!dateHeader) { + data.issue_date = Math.floor(Date.now() / 1000); + } else { + data.issue_date = Math.floor( + new Date(dateHeader) / 1000 + ); + } + + deferred.resolve(data); + }, + function (response) { + $log.debug('Token creation failed.'); + + // Construct a conformant error response. + var error = response.data; + if (!error.hasOwnProperty('error')) { + error = { + error: response.status, + error_description: response.data + }; + } + deferred.reject(error); + }); + + return deferred.promise; + } + }; + }); \ No newline at end of file diff --git a/src/app/auth/service/session.js b/src/app/auth/service/session.js new file mode 100644 index 00000000..0b95f932 --- /dev/null +++ b/src/app/auth/service/session.js @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * Session management service - keeps track of our current session state, mostly + * by verifying the token state returned from the OpenID service. + */ +angular.module('sb.auth').factory('Session', + function (SessionState, AccessToken, $rootScope, $log, $q, User) { + 'use strict'; + + /** + * The current session state. + * + * @type String + */ + var sessionState = SessionState.PENDING; + + /** + * Initialize the session. + */ + function initializeSession() { + var deferred = $q.defer(); + + if (!AccessToken.getAccessToken() || AccessToken.isExpired()) { + $log.debug('No token found'); + updateSessionState(SessionState.LOGGED_OUT); + deferred.resolve(); + } else { + // Validate the token currently in the cache. + validateToken() + .then(function () { + $log.debug('Token validated'); + updateSessionState(SessionState.LOGGED_IN); + deferred.resolve(sessionState); + }, function () { + $log.debug('Token not validated'); + AccessToken.clear(); + updateSessionState(SessionState.LOGGED_OUT); + deferred.resolve(sessionState); + }); + } + + return deferred.promise; + } + + /** + * Validate the token. + */ + function validateToken() { + var deferred = $q.defer(); + + var id = AccessToken.getIdToken(); + + User.read({id: id}, + function (user) { + deferred.resolve(user); + }, function (error) { + deferred.reject(error); + }); + return deferred.promise; + } + + /** + * Handles state updates and broadcasts. + */ + function updateSessionState(newState) { + if (newState !== sessionState) { + sessionState = newState; + $rootScope.$broadcast(sessionState); + } + } + + /** + * Destroy the session (Clear the token). + */ + function destroySession() { + AccessToken.clear(); + updateSessionState(SessionState.LOGGED_OUT); + } + + /** + * Initialize and test our current session token. + */ + initializeSession(); + + // If we ever encounter a 401 error, make sure the session is destroyed. + $rootScope.$on('http_401', function () { + destroySession(); + }); + + // Expose the methods for this service. + return { + /** + * The current session state. + */ + getSessionState: function () { + return sessionState; + }, + + /** + * Resolve the current session state, as a promise. + */ + resolveSessionState: function () { + var deferred = $q.defer(); + if (sessionState !== SessionState.PENDING) { + deferred.resolve(sessionState); + } else { + var unwatch = $rootScope.$watch(function () { + return sessionState; + }, function () { + deferred.resolve(sessionState); + unwatch(); + }); + } + + return deferred.promise; + }, + + /** + * Are we logged in? + */ + isLoggedIn: function () { + return sessionState === SessionState.LOGGED_IN; + }, + + /** + * Destroy the session. + */ + destroySession: function () { + destroySession(); + }, + + /** + * Update the session with a new (or null) token. + */ + updateSession: function (token) { + var deferred = $q.defer(); + if (!token) { + destroySession(); + deferred.resolve(sessionState); + } else { + AccessToken.setToken(token); + initializeSession().then( + function () { + deferred.resolve(sessionState); + }, + function () { + deferred.resolve(sessionState); + } + ); + } + + return deferred.promise; + } + }; + }); \ No newline at end of file diff --git a/src/app/services/module.js b/src/app/services/module.js index ffaa62f8..d93b1c73 100644 --- a/src/app/services/module.js +++ b/src/app/services/module.js @@ -18,7 +18,5 @@ * The Storyboard Services module contains all of the necessary API resources * used by the storyboard client. Its resources are available via injection to * any module that declares it as a dependency. - * - * @author Michael Krotscheck */ -angular.module('sb.services', ['ngResource', 'ngCookies']); \ No newline at end of file +angular.module('sb.services', ['ngResource']); \ No newline at end of file diff --git a/src/app/services/resolver/auth_provider_resolver.js b/src/app/services/resolver/auth_provider_resolver.js deleted file mode 100644 index 03f4e616..00000000 --- a/src/app/services/resolver/auth_provider_resolver.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. You may obtain - * a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ - -/** - * This collection of utility methods allow us to pre-resolve AuthProvider - * resources before a UI route switch is completed. - * - * @author Michael Krotscheck - */ -angular.module('sb.services').constant('AuthProviderResolver', { - - /** - * Resolves all available authorization providers. - */ - resolveAuthProviders: function ($q, AuthProvider, $log) { - 'use strict'; - - $log.debug('Resolving AuthProviders'); - - var deferred = $q.defer(); - - AuthProvider.query( - function (result) { - deferred.resolve(result); - }, - function (error) { - $log.warn('Route resolution rejected for AuthProviders'); - deferred.reject(error); - }); - - return deferred.promise; - }, - - /** - * Resolves an AuthProvider based on the unique ID passed via the - * stateParams. - */ - resolveAuthProvider: function (stateParamName) { - 'use strict'; - - return function ($q, AuthProvider, $stateParams, $log) { - - var deferred = $q.defer(); - - if (!$stateParams.hasOwnProperty(stateParamName)) { - $log.warn('State did not contain property of name ' + - stateParamName); - - deferred.reject({ - 'error': true - }); - } else { - var id = $stateParams[stateParamName]; - - $log.debug('Resolving AuthProvider: ' + id); - - AuthProvider.get({'id': id}, - function (result) { - deferred.resolve(result); - }, - function (error) { - $log.warn('Route resolution rejected for ' + - 'AuthProvider ' + id); - deferred.reject(error); - }); - - return deferred.promise; - } - }; - } -}); diff --git a/src/app/storyboard/controllers/header_controller.js b/src/app/storyboard/controllers/header_controller.js index c278f79b..aabdacf3 100644 --- a/src/app/storyboard/controllers/header_controller.js +++ b/src/app/storyboard/controllers/header_controller.js @@ -18,10 +18,35 @@ * Controller for our application header. */ angular.module('storyboard').controller('HeaderController', - function ($scope, $modal, NewStoryService) { + function ($scope, NewStoryService, Session, SessionState, CurrentUser) { 'use strict'; + /** + * Load and maintain the current user. + */ + $scope.currentUser = CurrentUser.get(); + + /** + * Create a new story. + */ $scope.newStory = function () { NewStoryService.showNewStoryModal(); }; + + /** + * View handle to show the current logged in state. + */ + $scope.isLoggedIn = + (Session.getSessionState() === SessionState.LOGGED_IN); + + // Watch for changes to the session state. + $scope.$watch( + function () { + return Session.getSessionState(); + }, + function (sessionState) { + $scope.isLoggedIn = sessionState === SessionState.LOGGED_IN; + $scope.currentUser = CurrentUser.get(); + } + ); }); diff --git a/src/app/storyboard/module.js b/src/app/storyboard/module.js index 4aa039cd..cc1441a6 100644 --- a/src/app/storyboard/module.js +++ b/src/app/storyboard/module.js @@ -48,13 +48,13 @@ angular.module('storyboard', $httpProvider.defaults.headers.common['X-Client'] = 'Storyboard'; }) - .run(function ($log, $rootScope, $location) { + .run(function ($log, $rootScope, $state) { 'use strict'; // Listen to changes on the root scope. If it's an error in the state // changes (i.e. a 404) take the user back to the index. $rootScope.$on('$stateChangeError', function () { - $location.path('/'); + $state.go('index'); }); }); diff --git a/src/app/templates/auth/provider/login.html b/src/app/templates/auth/busy.html similarity index 65% rename from src/app/templates/auth/provider/login.html rename to src/app/templates/auth/busy.html index 1d6b94f4..ecb18667 100644 --- a/src/app/templates/auth/provider/login.html +++ b/src/app/templates/auth/busy.html @@ -1,11 +1,11 @@ + +
+
+
+

Oh no!

+ +

We encountered an unexpected error while trying to + log you in. The error message below should be helpful, + though if it's not you can contact our engineers in + #storyboard on + + Freenode + . +

+ +
+
Error Code:
+
{{error}}
+
Error Description:
+
{{errorDescription}}
+
+ + +
+
+
\ No newline at end of file diff --git a/src/app/templates/auth/provider/list.html b/src/app/templates/auth/provider/list.html deleted file mode 100644 index 198ae419..00000000 --- a/src/app/templates/auth/provider/list.html +++ /dev/null @@ -1,33 +0,0 @@ - - -
-
-
-

How would you like to log in?

-
-
- - -
-
\ No newline at end of file diff --git a/src/app/templates/header.html b/src/app/templates/header.html index ff21d080..498b4b4c 100644 --- a/src/app/templates/header.html +++ b/src/app/templates/header.html @@ -46,8 +46,8 @@
  • @@ -67,10 +67,10 @@
  • - + Log in - + Log out
  • @@ -90,19 +90,19 @@
  •   - {{currentUser.firstName}} - {{currentUser.lastName}}  + {{currentUser.first_name}} + {{currentUser.last_name}} 
  • - Log in + Log in
  • diff --git a/src/app/util/helpers/string_util.js b/src/app/util/helpers/string_util.js new file mode 100644 index 00000000..9f4a6609 --- /dev/null +++ b/src/app/util/helpers/string_util.js @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014 Mirantis Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/* + * A collection of string utilities. + * + * @author Nikita Konovalov + */ + +angular.module('sb.util').factory('StringUtil', + function () { + 'use strict'; + + return { + /** + * Helper to generate a random alphanumeric string for the state + * parameter. + * + * @param length The length of the string to generate. + * @returns {string} A random alphanumeric string. + */ + randomAlphaNumeric: function (length) { + var possible = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + + 'abcdefghijklmnopqrstuvwxyz' + + '0123456789'; + + return this.random(length, possible); + }, + + /** + * Helper to generate a random string of specified length, using a + * provided list of characters. + * + * @param length The length of the string to generate. + * @param characters The list of valid characters. + * @returns {string} A random string composed of provided + * characters. + */ + random: function (length, characters) { + var text = ''; + + for (var i = 0; i < length; i++) { + text += characters.charAt(Math.floor( + Math.random() * characters.length)); + } + + return text; + } + }; + } +); \ No newline at end of file diff --git a/src/app/util/helpers/url_util.js b/src/app/util/helpers/url_util.js new file mode 100644 index 00000000..3e7a95b1 --- /dev/null +++ b/src/app/util/helpers/url_util.js @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * URL and location manipulation utilities. + * + * @author Nikita Konovalov + */ +angular.module('sb.util').factory('UrlUtil', + function ($location) { + 'use strict'; + + return { + /** + * Return the full URL prefix of the application, without the #! + * component. + */ + getFullUrlPrefix: function () { + var protocol = $location.protocol(); + var host = $location.host(); + var port = $location.port(); + + return protocol + '://' + host + ':' + port; + }, + + /** + * Build a HashBang url for this application given the provided + * fragment. + */ + buildApplicationUrl: function (fragment) { + return this.getFullUrlPrefix() + '/#!' + fragment; + }, + + /** + * Serialize an object into HTTP parameters. + */ + serializeParameters: function (params) { + var pairs = []; + for (var prop in params) { + // Filter out system params. + if (!params.hasOwnProperty(prop)) { + continue; + } + pairs.push( + encodeURIComponent(prop) + + '=' + + encodeURIComponent(params[prop]) + ); + } + return pairs.join('&'); + }, + + + /** + * Deserialize URI query parameters into an object. + */ + deserializeParameters: function (queryString) { + + var params = {}; + var queryComponents = queryString.split('&'); + for (var i = 0; i < queryComponents.length; i++) { + var parts = queryComponents[i].split('='); + var key = decodeURIComponent(parts[0]) || null; + var value = decodeURIComponent(parts[1]) || null; + + if (!!key && !!value) { + params[key] = value; + } + } + return params; + } + }; + } +); \ No newline at end of file diff --git a/src/app/auth/controller/auth_list_controller.js b/src/app/util/provider/search_param_provider.js similarity index 54% rename from src/app/auth/controller/auth_list_controller.js rename to src/app/util/provider/search_param_provider.js index a2518661..ac44eb7f 100644 --- a/src/app/auth/controller/auth_list_controller.js +++ b/src/app/util/provider/search_param_provider.js @@ -5,7 +5,7 @@ * not use this file except in compliance with the License. You may obtain * a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT @@ -15,19 +15,21 @@ */ /** - * This controller handles the logic for the authorization provider list page. - * - * @author Michael Krotscheck + * Utility injector, injects the query parameters from the NON-hashbang URL as + * $searchParams. */ -angular.module('sb.auth').controller('AuthListController', - function ($scope, authProviders, $state) { +angular.module('sb.util').factory('$searchParams', + function ($window, UrlUtil) { 'use strict'; - // If there's only one auth provider, just use that. - if (!!authProviders && authProviders.length === 1) { - $state.go('auth.provider.id', {id: authProviders[0].id}); + var params = {}; + var search = $window.location.search; + if (!!search) { + if (search.charAt(0) === '?') { + search = search.substr(1); + } + + return UrlUtil.deserializeParameters(search); } - - $scope.authProviders = authProviders; - - }); + return params; + }); \ No newline at end of file diff --git a/src/index.html b/src/index.html index 3b121e9d..af5e5b65 100644 --- a/src/index.html +++ b/src/index.html @@ -34,8 +34,7 @@ - - + diff --git a/test/unit/services/module.js b/test/unit/services/module.js index 4fa4cdd2..6cda8422 100644 --- a/test/unit/services/module.js +++ b/test/unit/services/module.js @@ -34,7 +34,7 @@ describe('sb.services', function () { expect(module).toBeTruthy(); }); - it('should load cookies module', function () { - expect(hasModule('ngCookies')).toBeTruthy(); + it('should load resource module', function () { + expect(hasModule('ngResource')).toBeTruthy(); }); });