From de76ba4e676dc79cadf0842f556745144f008808 Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Thu, 14 Feb 2019 15:58:45 +1300 Subject: [PATCH] feat(oauth): update OAuth UX --- api/http/handler/auth/authenticate_oauth.go | 2 +- api/oauth/oauth.go | 16 +- .../oauth-provider-selector-controller.js | 56 +++++ .../oauth-providers-selector.html | 58 +++-- .../oauth-providers-selector.js | 18 +- .../oauth-settings-controller.js | 45 +++- .../oauth-settings/oauth-settings.html | 206 ++++++++++-------- .../oauth-settings/oauth-settings.js | 5 +- app/portainer/views/auth/authController.js | 2 +- .../settingsAuthentication.html | 9 +- 10 files changed, 278 insertions(+), 139 deletions(-) create mode 100644 app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index d8a446d3e..89b828383 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -55,7 +55,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h } if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers { - return &httperror.HandlerError{http.StatusForbidden, "Unregistered account", portainer.ErrUnauthorized} + return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized} } if user == nil { diff --git a/api/oauth/oauth.go b/api/oauth/oauth.go index 3b3d210bc..b7de6792f 100644 --- a/api/oauth/oauth.go +++ b/api/oauth/oauth.go @@ -23,9 +23,18 @@ type Service struct{} // GetAccessToken takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint func (*Service) GetAccessToken(code string, settings *portainer.OAuthSettings) (string, error) { + unescapedCode, err := url.QueryUnescape(code) + if err != nil { + return "", err + } + config := buildConfig(settings) - token, err := config.Exchange(context.Background(), code) - return token.AccessToken, err + token, err := config.Exchange(context.Background(), unescapedCode) + if err != nil { + return "", err + } + + return token.AccessToken, nil } // GetUsername takes a token and retrieves the portainer OAuthSettings user identifier from resource server. @@ -109,7 +118,6 @@ func buildConfig(oauthSettings *portainer.OAuthSettings) *oauth2.Config { ClientSecret: oauthSettings.ClientSecret, Endpoint: endpoint, RedirectURL: oauthSettings.RedirectURI, - // TODO figure out how to handle different providers, see https://github.com/golang/oauth2/issues/119 - Scopes: []string{oauthSettings.Scopes}, + Scopes: []string{oauthSettings.Scopes}, } } diff --git a/app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js b/app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js new file mode 100644 index 000000000..2f5be632a --- /dev/null +++ b/app/extensions/oauth/components/oauth-providers-selector/oauth-provider-selector-controller.js @@ -0,0 +1,56 @@ +angular.module('portainer.extensions.oauth') + .controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() { + var ctrl = this; + + this.providers = [ + { + userIdentifier: 'mail', + scope: 'id,email,name', + name: 'microsoft' + }, + { + authUrl: 'https://accounts.google.com/o/oauth2/auth', + accessTokenUrl: 'https://accounts.google.com/o/oauth2/token', + resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', + userIdentifier: 'email', + scopes: 'profile email', + name: 'google' + }, + { + authUrl: 'https://github.com/login/oauth/authorize', + accessTokenUrl: 'https://github.com/login/oauth/access_token', + resourceUrl: 'https://api.github.com/user', + userIdentifier: 'login', + scopes: 'id email name', + name: 'github' + }, + { + name: 'custom' + } + ]; + + this.$onInit = onInit; + + function onInit() { + console.log(ctrl.provider.authUrl); + if (ctrl.provider.authUrl) { + ctrl.provider = getProviderByURL(ctrl.provider.authUrl); + } else { + ctrl.provider = ctrl.providers[0]; + } + ctrl.onSelect(ctrl.provider); + } + + function getProviderByURL(providerAuthURL) { + if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) { + return ctrl.providers[0]; + } + else if (providerAuthURL.indexOf('accounts.google.com') !== -1) { + return ctrl.providers[1]; + } + else if (providerAuthURL.indexOf('github.com') !== -1) { + return ctrl.providers[2]; + } + return ctrl.provider[3]; + } + }); diff --git a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html b/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html index f451768fe..8422f0900 100644 --- a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html +++ b/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html @@ -1,17 +1,49 @@
- Provider -
+ Provider + - -
-
- +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js b/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js index fa1cdb1e1..1376671fe 100644 --- a/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js +++ b/app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.js @@ -1,20 +1,8 @@ angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', { templateUrl: 'app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html', bindings: { - onSelect: '<' + onSelect: '<', + provider: '=' }, - controller: function oauthProvidersSelectorController() { - this.providers = [ - { - name: 'Facebook', - authUrl: 'https://www.facebook.com/v3.2/dialog/oauth', - accessTokenUrl: 'https://graph.facebook.com/v3.2/oauth/access_token', - resourceUrl: 'https://graph.facebook.com/v3.2/me?fields=email', - userIdentifier: 'email' - }, - { - name: 'Custom' - } - ]; - } + controller: 'OAuthProviderSelectorController' }); diff --git a/app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js b/app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js index ba324c49b..58690b511 100644 --- a/app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js +++ b/app/extensions/oauth/components/oauth-settings/oauth-settings-controller.js @@ -1,11 +1,38 @@ angular.module('portainer.extensions.oauth') -.controller('OAuthSettingsController', function OAuthSettingsController() { - this.onSelectProvider = onSelectProvider; + .controller('OAuthSettingsController', function OAuthSettingsController() { + var ctrl = this; - function onSelectProvider(provider) { - this.settings.AuthorizationURI = provider.authUrl; - this.settings.AccessTokenURI = provider.accessTokenUrl; - this.settings.ResourceURI = provider.resourceUrl; - this.settings.UserIdentifier = provider.userIdentifier; - } -}); + this.state = { + provider: {}, + overrideConfiguration: false, + microsoftTenantID: '' + }; + + this.$onInit = onInit; + this.onSelectProvider = onSelectProvider; + this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange; + + function onMicrosoftTenantIDChange() { + var tenantID = ctrl.state.microsoftTenantID; + + ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID); + ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID); + ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID); + } + + function onSelectProvider(provider) { + ctrl.state.provider = provider; + ctrl.settings.AuthorizationURI = provider.authUrl; + ctrl.settings.AccessTokenURI = provider.accessTokenUrl; + ctrl.settings.ResourceURI = provider.resourceUrl; + ctrl.settings.UserIdentifier = provider.userIdentifier; + ctrl.settings.Scopes = provider.scopes; + } + + function onInit() { + if (ctrl.settings.RedirectURI === '') { + ctrl.settings.RedirectURI = window.location.origin; + } + ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI; + } + }); diff --git a/app/extensions/oauth/components/oauth-settings/oauth-settings.html b/app/extensions/oauth/components/oauth-settings/oauth-settings.html index dc13135f1..d71effc69 100644 --- a/app/extensions/oauth/components/oauth-settings/oauth-settings.html +++ b/app/extensions/oauth/components/oauth-settings/oauth-settings.html @@ -1,48 +1,61 @@
- -
- Automatic user provisioning -
-
- - With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role. If - disabled, users must be created in Portainer in order to login. - -
-
-
- +
+ Automatic user provisioning +
+
+ + With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role. If + disabled, users must be created in Portainer in order to login. + +
+
+
-
-
-
- - The users created by the automatic provisioning feature can be added to a default team on creation. This setting is optional. - -
-
- - - You have not yet created any team. Head over the teams view to manage user teams. - - -
- +
+
+ + The users created by the automatic provisioning feature can be added to a default team on creation. This setting is optional. + +
+
+ + + You have not yet created any team. Head over the teams view to manage user teams. + + +
+ +
-
+
OAuth Configuration
+
+ +
+ +
+
+
@@ -63,129 +76,140 @@
-
+
-
+
-
+
-
+
-
+
- diff --git a/app/extensions/oauth/components/oauth-settings/oauth-settings.js b/app/extensions/oauth/components/oauth-settings/oauth-settings.js index 51a9ec553..f7a30e1c2 100644 --- a/app/extensions/oauth/components/oauth-settings/oauth-settings.js +++ b/app/extensions/oauth/components/oauth-settings/oauth-settings.js @@ -1,7 +1,8 @@ angular.module('portainer.extensions.oauth').component('oauthSettings', { templateUrl: 'app/extensions/oauth/components/oauth-settings/oauth-settings.html', bindings: { - settings: '<', + settings: '=', teams: '<' - } + }, + controller: 'OAuthSettingsController' }); diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 43aaebb08..98de83580 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -120,7 +120,7 @@ angular.module('portainer.app').controller('AuthenticationController', ['$q', '$ $state.go('portainer.home'); }) .catch(function error() { - $scope.state.AuthenticationError = 'Failed to authenticate with OAuth2 Provider'; + $scope.state.AuthenticationError = 'Unable to login via OAuth'; $scope.state.isInOAuthProcess = false; }); } diff --git a/app/portainer/views/settings/authentication/settingsAuthentication.html b/app/portainer/views/settings/authentication/settingsAuthentication.html index f47d02032..1458dc076 100644 --- a/app/portainer/views/settings/authentication/settingsAuthentication.html +++ b/app/portainer/views/settings/authentication/settingsAuthentication.html @@ -49,7 +49,7 @@
- +
Information @@ -58,7 +58,7 @@ When using internal authentication, Portainer will encrypt user passwords and store credentials locally.
- +
@@ -73,7 +73,7 @@
LDAP configuration
- +