Skip to content

Commit

Permalink
refactor: Refactor authentication and authorization
Browse files Browse the repository at this point in the history
Changing signatures and decoupling authorization and authentication
  • Loading branch information
oxyno-zeta committed Jun 21, 2020
1 parent 7014542 commit 29f9168
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 306 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package middlewares
package authentication

import (
"fmt"
"net/http"

"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/authx/models"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/config"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/middlewares"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/utils"
"github.com/thoas/go-funk"
"golang.org/x/net/context"
)

var bucketRequestContextKey = &contextKey{name: "bucket-request-context"}

// nolint:whitespace
func basicAuthMiddleware(basicConfig *config.BasicAuthConfig,
basicAuthUserConfigList []*config.BasicAuthUserConfig, templateConfig *config.TemplateConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logEntry := GetLogEntry(r)
logEntry := middlewares.GetLogEntry(r)
path := r.URL.RequestURI()
// Get bucket request context from request
brctx := GetBucketRequestContext(r)
brctx := middlewares.GetBucketRequestContext(r)

// Get basic auth information
username, password, ok := r.BasicAuth()
Expand Down Expand Up @@ -63,6 +68,16 @@ func basicAuthMiddleware(basicConfig *config.BasicAuthConfig,
return
}

// Create Basic auth user
buser := &models.BasicAuthUser{Username: username}

// Add user to request context by creating a new context
ctx := context.WithValue(r.Context(), userContextKey, buser)
// Create new request with new context
r = r.WithContext(ctx)

logEntry.Info("Basic auth user %s authenticated", username)

next.ServeHTTP(w, r)
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
package middlewares
package authentication

import (
"errors"
"net/http"

"github.com/gobwas/glob"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/authx/models"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/config"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/middlewares"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/utils"
"github.com/thoas/go-funk"
"golang.org/x/net/context"
)

var errAuthMiddlewareNotSupported = errors.New("not supported")
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}

var userContextKey = &contextKey{name: "USER_CONTEXT_KEY"}
var resourceContextKey = &contextKey{name: "RESOURCE_CONTEXT_KEY"}

// AuthMiddleware will redirect authentication to basic auth or OIDC depending on request path and resources declared
func AuthMiddleware(cfg *config.Config, resources []*config.Resource) func(http.Handler) http.Handler {
var errAuthenticationMiddlewareNotSupported = errors.New("authentication not supported")

// AuthenticationMiddleware will redirect authentication to basic auth or OIDC depending on request path and resources declared
func AuthenticationMiddleware(cfg *config.Config, resources []*config.Resource) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logEntry := GetLogEntry(r)
logEntry := middlewares.GetLogEntry(r)

// Check if resources are empty
if len(resources) == 0 {
// In this case, continue without authentication
logEntry.Info("no resource declared => skip authentication")
next.ServeHTTP(w, r)
return
}

// Get request data
requestURI := r.URL.RequestURI()
httpMethod := r.Method

// Get bucket request context
brctx := GetBucketRequestContext(r)
logEntry.Info(requestURI)
brctx := middlewares.GetBucketRequestContext(r)
// Find resource
res, err := findResource(resources, requestURI, httpMethod)
if err != nil {
Expand All @@ -37,15 +60,9 @@ func AuthMiddleware(cfg *config.Config, resources []*config.Resource) func(http.

// Check if resource isn't found
if res == nil {
// Check if resources are empty
if len(resources) == 0 {
// In this case, continue without authentication
next.ServeHTTP(w, r)
return
}
// In this case, resource isn't found because not path not declared
// So access is forbidden
logEntry.Errorf("no resource found for path %s => Forbidden access", requestURI)
logEntry.Errorf("no resource found for path %s and method %s => Forbidden access", requestURI, httpMethod)
// Check if bucket request context doesn't exist to use local default files
if brctx == nil {
utils.HandleForbidden(logEntry, w, cfg.Templates, requestURI)
Expand All @@ -57,26 +74,34 @@ func AuthMiddleware(cfg *config.Config, resources []*config.Resource) func(http.

// Resource found case

// Add resource to request context in order to keep it ready for authorization
ctx := context.WithValue(r.Context(), resourceContextKey, res)
// Create new request with new context
r = r.WithContext(ctx)

// Check if OIDC is enabled
if res.OIDC != nil {
oidcAuthorizationMiddleware(cfg.AuthProviders.OIDC[res.Provider], cfg.Templates, res.OIDC.AuthorizationAccesses)(next).ServeHTTP(w, r)
logEntry.Debug("authentication with oidc detected")
oidcAuthorizationMiddleware(cfg.AuthProviders.OIDC[res.Provider], cfg.Templates)(next).ServeHTTP(w, r)
return
}

// Check if Basic auth is enabled
if res.Basic != nil {
logEntry.Debug("authentication with basic auth detected")
basicAuthMiddleware(cfg.AuthProviders.Basic[res.Provider], res.Basic.Credentials, cfg.Templates)(next).ServeHTTP(w, r)
return
}

// Last case must be whitelist
if *res.WhiteList {
logEntry.Debug("authentication skipped because resource is whitelisted")
next.ServeHTTP(w, r)
return
}

// Error, this case shouldn't arrive
err = errAuthMiddlewareNotSupported
err = errAuthenticationMiddlewareNotSupported
logEntry.Error(err)
// Check if bucket request context doesn't exist to use local default files
if brctx == nil {
Expand All @@ -88,6 +113,18 @@ func AuthMiddleware(cfg *config.Config, resources []*config.Resource) func(http.
}
}

// GetAuthenticatedUser will get authenticated user in context
func GetAuthenticatedUser(req *http.Request) models.GenericUser {
res, _ := req.Context().Value(userContextKey).(models.GenericUser)
return res
}

// GetRequestResource will get request resource in context
func GetRequestResource(req *http.Request) *config.Resource {
res, _ := req.Context().Value(resourceContextKey).(*config.Resource)
return res
}

func findResource(resL []*config.Resource, requestURI string, httpMethod string) (*config.Resource, error) {
for i := 0; i < len(resL); i++ {
res := resL[i]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// +build unit

package middlewares
package authentication

import (
"reflect"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package middlewares
package authentication

import (
"errors"
Expand All @@ -10,10 +10,11 @@ import (

oidc "github.com/coreos/go-oidc"
"github.com/go-chi/chi"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/authx/models"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/config"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/log"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/middlewares"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/server/utils"
"github.com/thoas/go-funk"

"golang.org/x/net/context"
"golang.org/x/oauth2"
Expand Down Expand Up @@ -65,7 +66,7 @@ func OIDCEndpoints(oidcCfg *config.OIDCAuthConfig, tplConfig *config.TemplateCon

mux.HandleFunc(oidcCfg.LoginPath, func(w http.ResponseWriter, r *http.Request) {
// Get logger from request
logEntry := GetLogEntry(r)
logEntry := middlewares.GetLogEntry(r)
// Parse query params from request
qs := r.URL.Query()
// Get redirect query from query params
Expand Down Expand Up @@ -98,7 +99,7 @@ func OIDCEndpoints(oidcCfg *config.OIDCAuthConfig, tplConfig *config.TemplateCon

mux.HandleFunc(mainRedirectURLObject.Path, func(w http.ResponseWriter, r *http.Request) {
// Get logger from request
logEntry := GetLogEntry(r)
logEntry := middlewares.GetLogEntry(r)

// ! In this particular case, no bucket request context because mounted in general and not per target

Expand Down Expand Up @@ -184,15 +185,14 @@ func OIDCEndpoints(oidcCfg *config.OIDCAuthConfig, tplConfig *config.TemplateCon
func oidcAuthorizationMiddleware(
oidcAuthCfg *config.OIDCAuthConfig,
tplConfig *config.TemplateConfig,
authorizationAccesses []*config.OIDCAuthorizationAccess,
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get logger from request
logEntry := GetLogEntry(r)
logEntry := middlewares.GetLogEntry(r)
path := r.URL.RequestURI()
// Get bucket request context from request
brctx := GetBucketRequestContext(r)
brctx := middlewares.GetBucketRequestContext(r)

// Get JWT Token from header or cookie
jwtContent, err := getJWTToken(logEntry, r, oidcAuthCfg.CookieName)
Expand Down Expand Up @@ -278,19 +278,18 @@ func oidcAuthorizationMiddleware(
}
}

// Check if authorized
if !isAuthorized(groups, email, authorizationAccesses) {
logEntry.Errorf("Forbidden user %s", email)
// Check if bucket request context doesn't exist to use local default files
if brctx == nil {
utils.HandleForbidden(logEntry, w, tplConfig, path)
} else {
brctx.HandleForbidden(path)
}
return
// Create Basic auth user
ouser := &models.OIDCUser{
Email: email,
Groups: groups,
}

logEntry.Infof("User authorized and authenticated: %s", email)
// Add user to request context by creating a new context
ctx := context.WithValue(r.Context(), userContextKey, ouser)
// Create new request with new context
r = r.WithContext(ctx)

logEntry.Infof("OIDC User authenticated: %s", email)

// Next
next.ServeHTTP(w, r)
Expand Down Expand Up @@ -370,52 +369,6 @@ func getJWTToken(logEntry log.Logger, r *http.Request, cookieName string) (strin
return "", nil
}

func isAuthorized(groups []string, email string, authorizationAccesses []*config.OIDCAuthorizationAccess) bool {
// Check if there is a list of groups or email
if len(authorizationAccesses) == 0 {
// No group or email => consider this as authentication only required => ok
return true
}

// Loop over groups and email
for _, item := range authorizationAccesses {
if item.Regexp {
// Regex case
// Check group case
if item.Group != "" {
for _, grp := range groups {
// Try matching for group regexp
if item.GroupRegexp.MatchString(grp) {
return true
}
}
}

// Check email case
if item.Email != "" && item.EmailRegexp.MatchString(email) {
return true
}
} else {
// Not a regex case

// Check group case
if item.Group != "" {
result := funk.Contains(groups, item.Group)
if result {
return true
}
}
// Check email case
if item.Email != "" && item.Email == email {
return true
}
}
}

// Not found case
return false
}

// IsValidRedirect checks whether the redirect URL is whitelisted
func isValidRedirect(redirect string) bool {
return strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://")
Expand Down
Loading

0 comments on commit 29f9168

Please sign in to comment.