diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 2c8529140..5d96da47a 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -56,7 +56,8 @@ angular.module('portainer.app', []) views: { 'content@': { templateUrl: './views/auth/auth.html', - controller: 'AuthenticationController' + controller: 'AuthenticationController', + controllerAs: 'ctrl' }, 'sidebar@': {} }, diff --git a/app/portainer/helpers/urlHelper.js b/app/portainer/helpers/urlHelper.js index 2b40c7274..d8103a3a2 100644 --- a/app/portainer/helpers/urlHelper.js +++ b/app/portainer/helpers/urlHelper.js @@ -23,7 +23,7 @@ angular.module('portainer.app') } function cleanParameters() { - $window.location.search = ''; + $window.location.replace($window.location.origin + $window.location.pathname + $window.location.hash); } return helper; diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index 2f0181c87..6272b72b2 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -1,7 +1,7 @@ angular.module('portainer.app') .factory('Authentication', [ -'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', 'UserService', -function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider, UserService) { +'$async', 'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', 'UserService', +function AuthenticationFactory($async, Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider, UserService) { 'use strict'; var service = {}; @@ -25,24 +25,29 @@ function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManage } } + async function OAuthLoginAsync(code) { + const response = await OAuth.validate({ code: code }).$promise; + setUser(response.jwt); + } + function OAuthLogin(code) { - return OAuth.validate({ code: code }).$promise - .then(function onLoginSuccess(response) { - return setUser(response.jwt); - }); + return $async(OAuthLoginAsync, code) + } + + async function loginAsync(username, password) { + const response = await Auth.login({ username: username, password: password }).$promise; + setUser(response.jwt); } function login(username, password) { - return Auth.login({ username: username, password: password }).$promise - .then(function onLoginSuccess(response) { - return setUser(response.jwt); - }); + return $async(loginAsync, username, password); } function logout() { StateManager.clean(); EndpointProvider.clean(); LocalStorage.clean(); + LocalStorage.storeLoginStateUUID(''); } function isAuthenticated() { diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index d50369710..2accbca33 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -15,10 +15,10 @@ angular.module('portainer.app') return localStorageService.get('ENDPOINT_PUBLIC_URL'); }, storeLoginStateUUID: function(uuid) { - localStorageService.set('LOGIN_STATE_UUID', uuid); + localStorageService.cookie.set('LOGIN_STATE_UUID', uuid); }, getLoginStateUUID: function() { - return localStorageService.get('LOGIN_STATE_UUID'); + return localStorageService.cookie.get('LOGIN_STATE_UUID'); }, storeOfflineMode: function(isOffline) { localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline); diff --git a/app/portainer/views/auth/auth.html b/app/portainer/views/auth/auth.html index 4f1caf491..1aaa7a1df 100644 --- a/app/portainer/views/auth/auth.html +++ b/app/portainer/views/auth/auth.html @@ -4,53 +4,53 @@
- - + +
-
+
- +
- +
- -
+ +
Login with Microsoft
-
+
Login with Google
-
+
Login with Github
-
+
Login with OAuth
- - + - {{ state.AuthenticationError }} + {{ ctrl.state.AuthenticationError }}
@@ -61,10 +61,10 @@
-
+
- OAuth authentication in progress... + Authentication in progress...
diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index d79889ec2..76d6b00f1 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -1,123 +1,71 @@ +import angular from 'angular'; import uuidv4 from 'uuid/v4'; -angular.module('portainer.app') -.controller('AuthenticationController', ['$async', '$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'ExtensionService', 'StateManager', 'Notifications', 'SettingsService', 'URLHelper', 'LocalStorage', -function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, ExtensionService, StateManager, Notifications, SettingsService, URLHelper, LocalStorage) { - $scope.logo = StateManager.getState().application.logo; +class AuthenticationController { + /* @ngInject */ + constructor($async, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, ExtensionService, StateManager, Notifications, SettingsService, URLHelper, LocalStorage) { + this.$async = $async; + this.$scope = $scope; + this.$state = $state; + this.$stateParams = $stateParams; + this.$sanitize = $sanitize; + this.Authentication = Authentication; + this.UserService = UserService; + this.EndpointService = EndpointService; + this.ExtensionService = ExtensionService; + this.StateManager = StateManager; + this.Notifications = Notifications; + this.SettingsService = SettingsService; + this.URLHelper = URLHelper; + this.LocalStorage = LocalStorage; - $scope.formValues = { - Username: '', - Password: '' - }; + this.logo = this.StateManager.getState().application.logo; + this.formValues = { + Username: '', + Password: '' + }; + this.state = { + AuthenticationError: '', + loginInProgress: true, + OAuthProvider: '' + }; - $scope.state = { - AuthenticationError: '', - isInOAuthProcess: true, - OAuthProvider: '' - }; + this.retrieveAndSaveEnabledExtensionsAsync = this.retrieveAndSaveEnabledExtensionsAsync.bind(this); + this.retrievePermissionsAsync = this.retrievePermissionsAsync.bind(this); + this.checkForEndpointsAsync = this.checkForEndpointsAsync.bind(this); + this.postLoginSteps = this.postLoginSteps.bind(this); - function retrieveAndSaveEnabledExtensions() { - return $async(retrieveAndSaveEnabledExtensionsAsync); + this.oAuthLoginAsync = this.oAuthLoginAsync.bind(this); + this.retryLoginSanitizeAsync = this.retryLoginSanitizeAsync.bind(this); + this.internalLoginAsync = this.internalLoginAsync.bind(this); + + this.authenticateUserAsync = this.authenticateUserAsync.bind(this); + + this.manageOauthCodeReturn = this.manageOauthCodeReturn.bind(this); + this.authEnabledFlowAsync = this.authEnabledFlowAsync.bind(this); + this.onInit = this.onInit.bind(this); } - async function retrieveAndSaveEnabledExtensionsAsync() { - try { - await ExtensionService.retrieveAndSaveEnabledExtensions(); - } catch (err) { - Notifications.error('Failure', err, 'Unable to retrieve enabled extensions'); - $scope.state.loginInProgress = false; + /** + * UTILS FUNCTIONS SECTION + */ + + logout() { + this.Authentication.logout(); + this.state.loginInProgress = false; + this.generateOAuthLoginURI(); + } + + error(err, message) { + this.state.AuthenticationError = message; + if (!err) { + err = {}; } + this.Notifications.error('Failure', err, message); + this.state.loginInProgress = false; } - function permissionsError() { - $scope.state.permissionsError = true; - Authentication.logout(); - $scope.state.AuthenticationError = 'Unable to retrieve permissions.' - $scope.state.loginInProgress = false; - return Promise.reject(); - } - - $scope.authenticateUser = function() { - var username = $scope.formValues.Username; - var password = $scope.formValues.Password; - $scope.state.loginInProgress = true; - - Authentication.login(username, password) - .then(() => Authentication.retrievePermissions().catch(permissionsError)) - .then(function success() { - return retrieveAndSaveEnabledExtensions(); - }) - .then(function () { - checkForEndpoints(); - }) - .catch(function error() { - if ($scope.state.permissionsError) { - return; - } - SettingsService.publicSettings() - .then(function success(settings) { - if (settings.AuthenticationMethod === 1) { - return Authentication.login($sanitize(username), $sanitize(password)); - } - return $q.reject(); - }) - .then(function success() { - return retrieveAndSaveEnabledExtensions(); - }) - .then(function() { - $state.go('portainer.updatePassword'); - }) - .catch(function error() { - $scope.state.AuthenticationError = 'Invalid credentials'; - $scope.state.loginInProgress = false; - }); - }); - }; - - function unauthenticatedFlow() { - EndpointService.endpoints(0, 100) - .then(function success(endpoints) { - if (endpoints.value.length === 0) { - $state.go('portainer.init.endpoint'); - } else { - $state.go('portainer.home'); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoints'); - }); - } - - function authenticatedFlow() { - UserService.administratorExists() - .then(function success(exists) { - if (!exists) { - $state.go('portainer.init.admin'); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to verify administrator account existence'); - }); - } - - function checkForEndpoints() { - EndpointService.endpoints(0, 100) - .then(function success(data) { - var endpoints = data.value; - - if (endpoints.length === 0 && Authentication.isAdmin()) { - $state.go('portainer.init.endpoint'); - } else { - $state.go('portainer.home'); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve endpoints'); - $scope.state.loginInProgress = false; - }); - } - - function determineOauthProvider(LoginURI) { + determineOauthProvider(LoginURI) { if (LoginURI.indexOf('login.microsoftonline.com') !== -1) { return 'Microsoft'; } @@ -130,70 +78,199 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us return 'OAuth'; } - function generateState() { + generateState() { const uuid = uuidv4(); - LocalStorage.storeLoginStateUUID(uuid); + this.LocalStorage.storeLoginStateUUID(uuid); return '&state=' + uuid; } - function hasValidState(state) { - const savedUUID = LocalStorage.getLoginStateUUID(); - return savedUUID === state; + generateOAuthLoginURI() { + this.OAuthLoginURI = this.state.OAuthLoginURI + this.generateState(); } - function initView() { - SettingsService.publicSettings() - .then(function success(settings) { - $scope.AuthenticationMethod = settings.AuthenticationMethod; - $scope.state.OAuthProvider = determineOauthProvider(settings.OAuthLoginURI); - $scope.OAuthLoginURI = settings.OAuthLoginURI + generateState(); - }); + hasValidState(state) { + const savedUUID = this.LocalStorage.getLoginStateUUID(); + return savedUUID && state && savedUUID === state; + } - if ($stateParams.logout || $stateParams.error) { - Authentication.logout(); - $scope.state.AuthenticationError = $stateParams.error; - $scope.state.isInOAuthProcess = false; - return; - } + /** + * END UTILS FUNCTIONS SECTION + */ - if (Authentication.isAuthenticated()) { - $state.go('portainer.home'); - } + /** + * POST LOGIN STEPS SECTION + */ - var authenticationEnabled = $scope.applicationState.application.authentication; - if (!authenticationEnabled) { - unauthenticatedFlow(); - } else { - authenticatedFlow(); - } - - const code = URLHelper.getParameter('code'); - const state = URLHelper.getParameter('state'); - if (code && hasValidState(state)) { - oAuthLogin(code); - } else { - $scope.state.isInOAuthProcess = false; + async retrievePermissionsAsync() { + try { + await this.Authentication.retrievePermissions(); + } catch (err) { + this.state.permissionsError = true; + this.logout(); + this.error(err, 'Unable to retrieve permissions.'); } } - function oAuthLogin(code) { - return Authentication.OAuthLogin(code) - .then(() => Authentication.retrievePermissions().catch(permissionsError)) - .then(function success() { - return retrieveAndSaveEnabledExtensions(); - }) - .then(function() { - URLHelper.cleanParameters(); - }) - .catch(function error() { - if ($scope.state.permissionsError) { + async retrieveAndSaveEnabledExtensionsAsync() { + try { + await this.ExtensionService.retrieveAndSaveEnabledExtensions(); + } catch (err) { + this.error(err, 'Unable to retrieve enabled extensions'); + } + } + + async checkForEndpointsAsync(noAuth) { + try { + const endpoints = await this.EndpointService.endpoints(0, 1); + const isAdmin = noAuth || this.Authentication.isAdmin(); + + if (endpoints.value.length === 0 && isAdmin) { + return this.$state.go('portainer.init.endpoint'); + } else { + return this.$state.go('portainer.home'); + } + } catch (err) { + this.error(err, 'Unable to retrieve endpoints'); + } + } + + async postLoginSteps() { + await this.retrievePermissionsAsync(); + await this.retrieveAndSaveEnabledExtensionsAsync(); + await this.checkForEndpointsAsync(false); + } + /** + * END POST LOGIN STEPS SECTION + */ + + /** + * LOGIN METHODS SECTION + */ + + async oAuthLoginAsync(code) { + try { + await this.Authentication.OAuthLogin(code); + this.URLHelper.cleanParameters(); + } catch (err) { + this.error(err, 'Unable to login via OAuth'); + } + } + + async retryLoginSanitizeAsync(username, password) { + try { + await this.internalLoginAsync(this.$sanitize(username), this.$sanitize(password)); + this.$state.go('portainer.updatePassword'); + } catch (err) { + this.error(err, 'Invalid credentials'); + } + } + + async internalLoginAsync(username, password) { + await this.Authentication.login(username, password); + await this.postLoginSteps(); + } + + /** + * END LOGIN METHODS SECTION + */ + + /** + * AUTHENTICATE USER SECTION + */ + + async authenticateUserAsync() { + try { + var username = this.formValues.Username; + var password = this.formValues.Password; + this.state.loginInProgress = true; + await this.internalLoginAsync(username, password); + } catch (err) { + if (this.state.permissionsError) { return; } - $scope.state.AuthenticationError = 'Unable to login via OAuth'; - $scope.state.isInOAuthProcess = false; - }); + // This login retry is necessary to avoid conflicts with databases + // containing users created before Portainer 1.19.2 + // See https://github.com/portainer/portainer/issues/2199 for more info + await this.retryLoginSanitizeAsync(username, password); + } } + authenticateUser() { + return this.$async(this.authenticateUserAsync) + } - initView(); -}]); + /** + * END AUTHENTICATE USER SECTION + */ + + /** + * ON INIT SECTION + */ + async manageOauthCodeReturn(code, state) { + if (this.hasValidState(state)) { + await this.oAuthLoginAsync(code); + } else { + this.error(null, 'Invalid OAuth state, try again.'); + } + } + + async authEnabledFlowAsync() { + try { + const exists = await this.UserService.administratorExists(); + if (!exists) { + this.$state.go('portainer.init.admin'); + } + } catch (err) { + this.error(err, 'Unable to verify administrator account existence') + } + } + + async onInit() { + try { + const settings = await this.SettingsService.publicSettings(); + this.AuthenticationMethod = settings.AuthenticationMethod; + this.state.OAuthProvider = this.determineOauthProvider(settings.OAuthLoginURI); + this.state.OAuthLoginURI = settings.OAuthLoginURI; + + const code = this.URLHelper.getParameter('code'); + const state = this.URLHelper.getParameter('state'); + if (code && state) { + await this.manageOauthCodeReturn(code, state); + this.generateOAuthLoginURI(); + return; + } + this.generateOAuthLoginURI(); + + if (this.$stateParams.logout || this.$stateParams.error) { + this.logout(); + this.state.AuthenticationError = this.$stateParams.error; + return; + } + + if (this.Authentication.isAuthenticated()) { + await this.postLoginSteps(); + } + this.state.loginInProgress = false; + + const authenticationEnabled = this.$scope.applicationState.application.authentication; + if (!authenticationEnabled) { + await this.checkForEndpointsAsync(true); + } else { + await this.authEnabledFlowAsync(); + } + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to retrieve public settings'); + } + } + + $onInit() { + return this.$async(this.onInit); + } + + /** + * END ON INIT SECTION + */ +} + +export default AuthenticationController; +angular.module('portainer.app').controller('AuthenticationController', AuthenticationController);