From 241a701eca7fe1516f04861823c889d91bd7523d Mon Sep 17 00:00:00 2001 From: Chaim Lev Ari Date: Sun, 30 Dec 2018 18:02:22 +0200 Subject: [PATCH] feat(oauth): merge pr from https://github.com/portainer/portainer/pull/2515 --- api/cmd/portainer/main.go | 9 + api/http/handler/auth/authenticate.go | 63 +++++++ api/http/handler/auth/handler.go | 3 + api/http/handler/handler.go | 2 + api/http/handler/settings/handler.go | 2 + api/http/handler/settings/settings_public.go | 8 + api/http/handler/settings/settings_update.go | 7 +- api/http/server.go | 3 + api/oauth/oauth.go | 168 ++++++++++++++++++ api/portainer.go | 22 +++ app/constants.js | 1 + .../users-datatable/usersDatatable.html | 3 +- app/portainer/models/settings.js | 17 ++ app/portainer/rest/oauth.js | 9 + app/portainer/services/authentication.js | 21 ++- app/portainer/views/auth/auth.html | 4 +- app/portainer/views/auth/authController.js | 45 ++++- .../settingsAuthentication.html | 120 ++++++++++++- .../settingsAuthenticationController.js | 1 + 19 files changed, 501 insertions(+), 7 deletions(-) create mode 100644 api/oauth/oauth.go create mode 100644 app/portainer/rest/oauth.js diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index e0a9dd2f9..83f85aa8f 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -20,6 +20,7 @@ import ( "github.com/portainer/portainer/jwt" "github.com/portainer/portainer/ldap" "github.com/portainer/portainer/libcompose" + "github.com/portainer/portainer/oauth" "log" ) @@ -100,6 +101,10 @@ func initLDAPService() portainer.LDAPService { return &ldap.Service{} } +func initOAuthService() portainer.OAuthService { + return &oauth.Service{} +} + func initGitService() portainer.GitService { return &git.Service{} } @@ -260,6 +265,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL portainer.LDAPGroupSearchSettings{}, }, }, + OAuthSettings: portainer.OAuthSettings{}, AllowBindMountsForRegularUsers: true, AllowPrivilegedModeForRegularUsers: true, EnableHostManagementFeatures: false, @@ -520,6 +526,8 @@ func main() { ldapService := initLDAPService() + oauthService := initOAuthService() + gitService := initGitService() cryptoService := initCryptoService() @@ -665,6 +673,7 @@ func main() { JWTService: jwtService, FileService: fileService, LDAPService: ldapService, + OAuthService: oauthService, GitService: gitService, SignatureService: digitalSignatureService, JobScheduler: jobScheduler, diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index d5562edd3..163023d0e 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -17,6 +17,10 @@ type authenticatePayload struct { Password string } +type oauthPayload struct { + Code string +} + type authenticateResponse struct { JWT string `json:"jwt"` } @@ -31,6 +35,13 @@ func (payload *authenticatePayload) Validate(r *http.Request) error { return nil } +func (payload *oauthPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Code) { + return portainer.Error("Invalid OAuth authorization code") + } + return nil +} + func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { if handler.authDisabled { return &httperror.HandlerError{http.StatusServiceUnavailable, "Cannot authenticate user. Portainer was started with the --no-auth flag", ErrAuthDisabled} @@ -82,6 +93,58 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer. return handler.writeToken(w, user) } +func (handler *Handler) authenticateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload oauthPayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + settings, err := handler.SettingsService.Settings() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err} + } + + if settings.AuthenticationMethod != 3 { + return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not being used", err} + } + + token, err := handler.OAuthService.GetAccessToken(payload.Code, &settings.OAuthSettings) + if err != nil { + return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid access token", portainer.ErrUnauthorized} + } + + username, err := handler.OAuthService.GetUsername(token, &settings.OAuthSettings) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Unable to acquire username", portainer.ErrUnauthorized} + } + + u, err := handler.UserService.UserByUsername(username) + if err != nil && err != portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err} + } + + if u == nil && !settings.OAuthSettings.OAuthAutoCreateUsers { + return &httperror.HandlerError{http.StatusForbidden, "Unregistered account", portainer.ErrUnauthorized} + } + + if u == nil { + user := &portainer.User{ + Username: username, + Role: portainer.StandardUserRole, + } + + err = handler.UserService.CreateUser(user) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err} + } + + return handler.writeToken(w, user) + } + + return handler.writeToken(w, u) +} + func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError { err := handler.CryptoService.CompareHashAndData(user.Password, password) if err != nil { diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 1f0769e08..073da3a69 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -25,6 +25,7 @@ type Handler struct { CryptoService portainer.CryptoService JWTService portainer.JWTService LDAPService portainer.LDAPService + OAuthService portainer.OAuthService SettingsService portainer.SettingsService TeamService portainer.TeamService TeamMembershipService portainer.TeamMembershipService @@ -38,6 +39,8 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi } h.Handle("/auth", rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost) + h.Handle("/oauth", + rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticateOAuth)))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 9cb3059df..f818edf4c 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -60,6 +60,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case strings.HasPrefix(r.URL.Path, "/api/auth"): http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/oauth"): + http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"): diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 0acbd2ca6..ee277d676 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -11,6 +11,7 @@ import ( func hideFields(settings *portainer.Settings) { settings.LDAPSettings.Password = "" + settings.OAuthSettings.ClientSecret = "" } // Handler is the HTTP handler used to handle settings operations. @@ -18,6 +19,7 @@ type Handler struct { *mux.Router SettingsService portainer.SettingsService LDAPService portainer.LDAPService + OAuthService portainer.OAuthService FileService portainer.FileService JobScheduler portainer.JobScheduler ScheduleService portainer.ScheduleService diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 9744b319a..a803c22a5 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -15,6 +15,10 @@ type publicSettingsResponse struct { AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` ExternalTemplates bool `json:"ExternalTemplates"` + AuthorizationURI string `json:"AuthorizationURI"` + ClientID string `json:"ClientID"` + RedirectURI string `json:"RedirectURI"` + Scopes string `json:"Scopes"` } // GET request on /api/settings/public @@ -31,6 +35,10 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, EnableHostManagementFeatures: settings.EnableHostManagementFeatures, ExternalTemplates: false, + AuthorizationURI: settings.OAuthSettings.AuthorizationURI, + ClientID: settings.OAuthSettings.ClientID, + RedirectURI: settings.OAuthSettings.RedirectURI, + Scopes: settings.OAuthSettings.Scopes, } if settings.TemplatesURL != "" { diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 5b4d33ae3..57c6d1d0a 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -16,6 +16,7 @@ type settingsUpdatePayload struct { BlackListedLabels []portainer.Pair AuthenticationMethod *int LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings AllowBindMountsForRegularUsers *bool AllowPrivilegedModeForRegularUsers *bool EnableHostManagementFeatures *bool @@ -24,7 +25,7 @@ type settingsUpdatePayload struct { } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { - if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 { + if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 { return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)") } if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) { @@ -69,6 +70,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.LDAPSettings = *payload.LDAPSettings } + if payload.OAuthSettings != nil { + settings.OAuthSettings = *payload.OAuthSettings + } + if payload.AllowBindMountsForRegularUsers != nil { settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers } diff --git a/api/http/server.go b/api/http/server.go index 1220d94d1..cf3c9ef2b 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -55,6 +55,7 @@ type Server struct { GitService portainer.GitService JWTService portainer.JWTService LDAPService portainer.LDAPService + OAuthService portainer.OAuthService ExtensionService portainer.ExtensionService RegistryService portainer.RegistryService ResourceControlService portainer.ResourceControlService @@ -104,6 +105,7 @@ func (server *Server) Start() error { authHandler.CryptoService = server.CryptoService authHandler.JWTService = server.JWTService authHandler.LDAPService = server.LDAPService + authHandler.OAuthService = server.OAuthService authHandler.SettingsService = server.SettingsService authHandler.TeamService = server.TeamService authHandler.TeamMembershipService = server.TeamMembershipService @@ -155,6 +157,7 @@ func (server *Server) Start() error { var settingsHandler = settings.NewHandler(requestBouncer) settingsHandler.SettingsService = server.SettingsService settingsHandler.LDAPService = server.LDAPService + settingsHandler.OAuthService = server.OAuthService settingsHandler.FileService = server.FileService settingsHandler.JobScheduler = server.JobScheduler settingsHandler.ScheduleService = server.ScheduleService diff --git a/api/oauth/oauth.go b/api/oauth/oauth.go new file mode 100644 index 000000000..bc175d46c --- /dev/null +++ b/api/oauth/oauth.go @@ -0,0 +1,168 @@ +package oauth + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "mime" + "net/http" + "net/url" + "strings" + + "github.com/portainer/portainer" + "golang.org/x/oauth2" +) + +const ( + // ErrInvalidCode defines an error raised when the user authorization code is invalid + ErrInvalidCode = portainer.Error("Authorization code is invalid") +) + +// Service represents a service used to authenticate users against an authorization server +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) { + v := url.Values{} + v.Set("client_id", settings.ClientID) + v.Set("client_secret", settings.ClientSecret) + v.Set("redirect_uri", settings.RedirectURI) + v.Set("code", code) + v.Set("grant_type", "authorization_code") + + req, err := http.NewRequest("POST", settings.AccessTokenURI, strings.NewReader(v.Encode())) + if err != nil { + return "", err + } + + client := &http.Client{} + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + r, err := client.Do(req) + if err != nil { + return "", err + } + + body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) + if err != nil { + return "", fmt.Errorf("oauth2: cannot fetch token: %v", err) + } + + if r.StatusCode != http.StatusOK { + log.Printf("[Error] - request returned with bad status code %v, body is %v", r.StatusCode, string(body)) + type ErrorMessage struct { + Message string + Type string + Code int + } + type ErrorResponse struct { + Error ErrorMessage + } + + var response ErrorResponse + if err = json.Unmarshal(body, &response); err != nil { + // report also error + log.Printf("[Error] - Failed parsing error body: %v", err) + return "", errors.New("oauth2: cannot fetch token") + } + + return "", errors.New(response.Error.Message) + } + + content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) + if content == "application/x-www-form-urlencoded" || content == "text/plain" { + values, err := url.ParseQuery(string(body)) + if err != nil { + return "", err + } + + token := values.Get("access_token") + log.Printf("[DEBUG] - returned body %v", values) + + if token == "" { + log.Printf("[DEBUG] - access token returned empty - %v", values) + return "", errors.New("oauth2: cannot fetch token") + } + + return token, nil + } + + type tokenJSON struct { + AccessToken string `json:"access_token"` + } + + var tj tokenJSON + if err = json.Unmarshal(body, &tj); err != nil { + return "", err + } + + token := tj.AccessToken + + if token == "" { + log.Printf("[DEBUG] - access token returned empty - %v with status code", string(body), r.StatusCode) + return "", errors.New("oauth2: cannot fetch token") + } + return token, nil +} + +// GetUsername takes a token and retrieves the portainer OAuthSettings user identifier from resource server. +func (*Service) GetUsername(token string, settings *portainer.OAuthSettings) (string, error) { + req, err := http.NewRequest("GET", settings.ResourceURI, nil) + if err != nil { + return "", err + } + + client := &http.Client{} + req.Header.Set("Authorization", "Bearer "+token) + resp, err := client.Do(req) + if err != nil { + return "", err + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", &oauth2.RetrieveError{ + Response: resp, + Body: body, + } + } + + content, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + if content == "application/x-www-form-urlencoded" || content == "text/plain" { + values, err := url.ParseQuery(string(body)) + if err != nil { + return "", err + } + + username := values.Get(settings.UserIdentifier) + return username, nil + } + + var datamap map[string]interface{} + if err = json.Unmarshal(body, &datamap); err != nil { + return "", err + } + + username, ok := datamap[settings.UserIdentifier].(string) + if ok && username != "" { + return username, nil + } + + if !ok { + username, ok := datamap[settings.UserIdentifier].(float64) + if ok { + return fmt.Sprint(int(username)), nil + } + } + return "", &oauth2.RetrieveError{ + Response: resp, + Body: body, + } +} diff --git a/api/portainer.go b/api/portainer.go index c8062ed7a..fe099769e 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -56,6 +56,19 @@ type ( AutoCreateUsers bool `json:"AutoCreateUsers"` } + // OAuthSettings represents the settings used to authorize with an authorization server + OAuthSettings struct { + ClientID string `json:"ClientID"` + ClientSecret string `json:"ClientSecret,omitempty"` + AccessTokenURI string `json:"AccessTokenURI"` + AuthorizationURI string `json:"AuthorizationURI"` + ResourceURI string `json:"ResourceURI"` + RedirectURI string `json:"RedirectURI"` + UserIdentifier string `json:"UserIdentifier"` + Scopes string `json:"Scopes"` + OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"` + } + // TLSConfiguration represents a TLS configuration TLSConfiguration struct { TLS bool `json:"TLS"` @@ -85,6 +98,7 @@ type ( BlackListedLabels []Pair `json:"BlackListedLabels"` AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` LDAPSettings LDAPSettings `json:"LDAPSettings"` + OAuthSettings OAuthSettings `json:"OAuthSettings"` AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` SnapshotInterval string `json:"SnapshotInterval"` @@ -748,6 +762,12 @@ type ( GetUserGroups(username string, settings *LDAPSettings) ([]string, error) } + // OAuthService represents a service used to authenticate users against an authorization server + OAuthService interface { + GetAccessToken(code string, settings *OAuthSettings) (string, error) + GetUsername(token string, settings *OAuthSettings) (string, error) + } + // SwarmStackManager represents a service to manage Swarm stacks SwarmStackManager interface { Login(dockerhub *DockerHub, registries []Registry, endpoint *Endpoint) @@ -833,6 +853,8 @@ const ( AuthenticationInternal // AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server) AuthenticationLDAP + //AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server) + AuthenticationOAuth ) const ( diff --git a/app/constants.js b/app/constants.js index 464089158..f1b941de6 100644 --- a/app/constants.js +++ b/app/constants.js @@ -4,6 +4,7 @@ angular.module('portainer') .constant('API_ENDPOINT_ENDPOINTS', 'api/endpoints') .constant('API_ENDPOINT_ENDPOINT_GROUPS', 'api/endpoint_groups') .constant('API_ENDPOINT_MOTD', 'api/motd') +.constant('API_ENDPOINT_OAUTH', 'api/oauth') .constant('API_ENDPOINT_EXTENSIONS', 'api/extensions') .constant('API_ENDPOINT_REGISTRIES', 'api/registries') .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') diff --git a/app/portainer/components/datatables/users-datatable/usersDatatable.html b/app/portainer/components/datatables/users-datatable/usersDatatable.html index 11692974d..e99fd16f5 100644 --- a/app/portainer/components/datatables/users-datatable/usersDatatable.html +++ b/app/portainer/components/datatables/users-datatable/usersDatatable.html @@ -65,8 +65,9 @@ - Internal + Internal LDAP + OAuth diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index a4f2e8bf6..cbf0c958a 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -3,6 +3,11 @@ function SettingsViewModel(data) { this.BlackListedLabels = data.BlackListedLabels; this.AuthenticationMethod = data.AuthenticationMethod; this.LDAPSettings = data.LDAPSettings; + this.OAuthSettings = data.OAuthSettings; + this.ClientID = data.ClientID; + this.RedirectURI = data.RedirectURI; + this.Scopes = data.Scopes; + this.AuthorizationURI = data.AuthorizationURI; this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers; this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers; this.SnapshotInterval = data.SnapshotInterval; @@ -31,3 +36,15 @@ function LDAPGroupSearchSettings(GroupBaseDN, GroupAttribute, GroupFilter) { this.GroupAttribute = GroupAttribute; this.GroupFilter = GroupFilter; } + +function OAuthSettingsViewModel(data) { + this.ClientID = data.ClientID; + this.ClientSecret = data.ClientSecret; + this.AccessTokenURI = data.AccessTokenURI; + this.AuthorizationURI = data.AuthorizationURI; + this.ResourceURI = data.ResourceURI; + this.RedirectURI = data.RedirectURI; + this.UserIdentifier = data.UserIdentifier; + this.Scopes = data.Scopes; + this.OAuthAutoCreateUsers = data.OAuthAutoCreateUsers; +} \ No newline at end of file diff --git a/app/portainer/rest/oauth.js b/app/portainer/rest/oauth.js new file mode 100644 index 000000000..c1d2a68ad --- /dev/null +++ b/app/portainer/rest/oauth.js @@ -0,0 +1,9 @@ +angular.module('portainer.app') +.factory('OAuth', ['$resource', 'API_ENDPOINT_OAUTH', function OAuthFactory($resource, API_ENDPOINT_OAUTH) { + 'use strict'; + return $resource(API_ENDPOINT_OAUTH, {}, { + login: { + method: 'POST', ignoreLoadingBar: true + } + }); +}]); \ No newline at end of file diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index 0e9f19a93..e3ac610e9 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -1,11 +1,12 @@ angular.module('portainer.app') -.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager, EndpointProvider) { +.factory('Authentication', ['$q', 'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider) { 'use strict'; var service = {}; var user = {}; service.init = init; + service.oAuthLogin = oAuthLogin; service.login = login; service.logout = logout; service.isAuthenticated = isAuthenticated; @@ -22,6 +23,24 @@ angular.module('portainer.app') } } + function oAuthLogin(code) { + var deferred = $q.defer(); + + OAuth.login({code: code}).$promise + .then(function success(data) { + LocalStorage.storeJWT(data.jwt); + var tokenPayload = jwtHelper.decodeToken(data.jwt); + user.username = tokenPayload.username; + user.ID = tokenPayload.id; + user.role = tokenPayload.role; + deferred.resolve(); + }) + .catch(function error() { + deferred.reject(); + }); + return deferred.promise; + } + function login(username, password) { var deferred = $q.defer(); diff --git a/app/portainer/views/auth/auth.html b/app/portainer/views/auth/auth.html index 6cde5c633..835303ff2 100644 --- a/app/portainer/views/auth/auth.html +++ b/app/portainer/views/auth/auth.html @@ -28,13 +28,15 @@
+
OAuth Login
- + {{ state.AuthenticationError }}
+ diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 055d359be..6a04ecf36 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -1,6 +1,6 @@ angular.module('portainer.app') -.controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', -function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) { +.controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', '$location', '$window', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', +function ($q, $scope, $state, $transition$, $sanitize, $location, $window, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) { $scope.logo = StateManager.getState().application.logo; @@ -12,6 +12,16 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi $scope.state = { AuthenticationError: '' }; + + SettingsService.publicSettings() + .then(function success(settings) { + $scope.AuthenticationMethod = settings.AuthenticationMethod; + $scope.ClientID = settings.ClientID; + $scope.RedirectURI = settings.RedirectURI; + $scope.Scopes = settings.Scopes; + $scope.AuthorizationURI = settings.AuthorizationURI; + }); + $scope.authenticateUser = function() { var username = $scope.formValues.Username; @@ -74,6 +84,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi $state.go('portainer.init.endpoint'); } else { $state.go('portainer.home'); + $window.location.search = ''; } }) .catch(function error(err) { @@ -100,5 +111,35 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi } } + function oAuthLogin(code) { + Authentication.oAuthLogin(code) + .then(function success() { + $state.go('portainer.home'); + $window.location.search = ''; + }) + .catch(function error() { + $scope.state.AuthenticationError = 'Failed to authenticate with OAuth2 Provider'; + }); + } + + function getParameter(param) { + var URL = $location.absUrl(); + var params = URL.split('?')[1]; + if (params === undefined) { + return null; + } + params = params.split('&'); + for (var i = 0; i < params.length; i++) { + var parameter = params[i].split('='); + if (parameter[0] === param) { + return parameter[1].split('#')[0]; + } + } + return null; + } + initView(); + if (getParameter('code') !== null) { + oAuthLogin(getParameter('code')); + } }]); diff --git a/app/portainer/views/settings/authentication/settingsAuthentication.html b/app/portainer/views/settings/authentication/settingsAuthentication.html index 72db0edef..1cef110f7 100644 --- a/app/portainer/views/settings/authentication/settingsAuthentication.html +++ b/app/portainer/views/settings/authentication/settingsAuthentication.html @@ -37,6 +37,16 @@

LDAP authentication

+
+ + +
@@ -52,6 +62,11 @@ When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
+
+ + When using OAuth authentication, Portainer will allow users to optionally authenticate with an OAuth authorization server. + +
@@ -306,7 +321,110 @@
- +
+ +
+ OAuth Configuration +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + 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. + +
+
+
+ + +
+
+
+ +