1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

feat(auth): integrate oauth extension (#4152)

* refactor(oauth): move oauth client code

* feat(oauth): move extension code into server code

* feat(oauth): enable oauth without extension

* refactor(oauth): make it easier to remove providers
This commit is contained in:
Chaim Lev-Ari 2020-08-05 11:36:46 +03:00 committed by GitHub
parent 148ccd1bc4
commit 00f4fe0039
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 201 additions and 135 deletions

View file

@ -6,7 +6,7 @@ import (
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
@ -24,6 +24,7 @@ import (
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/libcompose"
"github.com/portainer/portainer/api/oauth"
)
func initCLI() *portainer.CLIFlags {
@ -108,6 +109,10 @@ func initLDAPService() portainer.LDAPService {
return &ldap.Service{}
}
func initOAuthService() portainer.OAuthService {
return oauth.NewService()
}
func initGitService() portainer.GitService {
return git.NewService()
}
@ -354,6 +359,8 @@ func main() {
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
cryptoService := initCryptoService()
@ -467,6 +474,7 @@ func main() {
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,

View file

@ -30,6 +30,7 @@ require (
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/src-d/go-git.v4 v4.13.1
k8s.io/api v0.17.2

View file

@ -1,9 +1,7 @@
package auth
import (
"encoding/json"
"errors"
"io/ioutil"
"log"
"net/http"
@ -27,52 +25,22 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
return nil
}
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
if code == "" {
return "", errors.New("Invalid OAuth authorization code")
}
encodedConfiguration, err := json.Marshal(settings)
if settings == nil {
return "", errors.New("Invalid OAuth configuration")
}
username, err := handler.OAuthService.Authenticate(code, settings)
if err != nil {
log.Printf("[DEBUG] - Unable to authenticate user via OAuth: %v", err)
return "", nil
}
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
if err != nil {
return "", err
}
client := &http.Client{}
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
req.Header.Set("X-OAuth-Code", code)
req.Header.Set("X-PortainerExtension-License", licenseKey)
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
}
type extensionResponse struct {
Username string `json:"Username,omitempty"`
Err string `json:"err,omitempty"`
Details string `json:"details,omitempty"`
}
var extResp extensionResponse
err = json.Unmarshal(body, &extResp)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", errors.New(extResp.Err + ":" + extResp.Details)
}
return extResp.Username, nil
return username, nil
}
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
@ -91,14 +59,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
}
extension, err := handler.DataStore.Extension().Extension(portainer.OAuthAuthenticationExtension)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}

View file

@ -19,6 +19,7 @@ type Handler struct {
CryptoService portainer.CryptoService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
ProxyManager *proxy.Manager
AuthorizationService *authorization.Service
KubernetesTokenCacheManager *kubernetes.TokenCacheManager

View file

@ -61,6 +61,7 @@ type Server struct {
GitService portainer.GitService
JWTService portainer.JWTService
LDAPService portainer.LDAPService
OAuthService portainer.OAuthService
SwarmStackManager portainer.SwarmStackManager
Handler *handler.Handler
SSL bool
@ -90,6 +91,7 @@ func (server *Server) Start() error {
authHandler.ProxyManager = proxyManager
authHandler.AuthorizationService = authorizationService
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
authHandler.OAuthService = server.OAuthService
var roleHandler = roles.NewHandler(requestBouncer)
roleHandler.DataStore = server.DataStore

130
api/oauth/oauth.go Normal file
View file

@ -0,0 +1,130 @@
package oauth
import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"github.com/portainer/portainer/api"
)
// Service represents a service used to authenticate users against an authorization server
type Service struct{}
// NewService returns a pointer to a new instance of this service
func NewService() *Service {
return &Service{}
}
// Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint.
// On success, it will then return the username associated to authenticated user by fetching this information
// from the resource server and matching it with the user identifier setting.
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
token, err := getAccessToken(code, configuration)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
return "", err
}
return getUsername(token, configuration)
}
func getAccessToken(code string, configuration *portainer.OAuthSettings) (string, error) {
unescapedCode, err := url.QueryUnescape(code)
if err != nil {
return "", err
}
config := buildConfig(configuration)
token, err := config.Exchange(context.Background(), unescapedCode)
if err != nil {
return "", err
}
return token.AccessToken, nil
}
func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) {
req, err := http.NewRequest("GET", configuration.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, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return "", err
}
if content == "application/x-www-form-urlencoded" || content == "text/plain" {
values, err := url.ParseQuery(string(body))
if err != nil {
return "", err
}
username := values.Get(configuration.UserIdentifier)
return username, nil
}
var datamap map[string]interface{}
if err = json.Unmarshal(body, &datamap); err != nil {
return "", err
}
username, ok := datamap[configuration.UserIdentifier].(string)
if ok && username != "" {
return username, nil
}
if !ok {
username, ok := datamap[configuration.UserIdentifier].(float64)
if ok && username != 0 {
return fmt.Sprint(int(username)), nil
}
}
return "", &oauth2.RetrieveError{
Response: resp,
Body: body,
}
}
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
endpoint := oauth2.Endpoint{
AuthURL: configuration.AuthorizationURI,
TokenURL: configuration.AccessTokenURI,
}
return &oauth2.Config{
ClientID: configuration.ClientID,
ClientSecret: configuration.ClientSecret,
Endpoint: endpoint,
RedirectURL: configuration.RedirectURI,
Scopes: []string{configuration.Scopes},
}
}

View file

@ -984,6 +984,11 @@ type (
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
}
// OAuthService represents a service used to authenticate users using OAuth
OAuthService interface {
Authenticate(code string, configuration *OAuthSettings) (string, error)
}
// RegistryService represents a service for managing registry data
RegistryService interface {
Registry(ID RegistryID) (*Registry, error)