Skip to content

Commit

Permalink
feat: Add support for header authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
oxyno-zeta committed Mar 28, 2022
1 parent 37de51a commit 6c56c5c
Show file tree
Hide file tree
Showing 22 changed files with 1,405 additions and 113 deletions.
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ down/metrics-services:
docker rm -f prometheus || true
docker rm -f grafana || true

.PHONY: down/oauth2-proxy-services
down/oauth2-proxy-services:
docker rm -f oauth2-proxy || true

.PHONY: setup/metrics-services
setup/metrics-services:
docker run --rm -d --name prometheus -v $(CURRENT_DIR)/local-resources/prometheus/prometheus.yml:/prometheus/prometheus.yml --network=host prom/prometheus:v2.18.0 --web.listen-address=:9191
Expand All @@ -140,6 +144,16 @@ setup/services: down/services
docker run -d --rm --name opa -p 8181:8181 -v $(CURRENT_DIR)/local-resources/opa/bundle.tar.gz:/bundle.tar.gz openpolicyagent/opa run --server --log-level debug --log-format text --bundle /bundle.tar.gz
docker run -d --rm --name keycloak -p 8088:8080 -e KEYCLOAK_IMPORT=/tmp/realm-export.json -v $(CURRENT_DIR)/local-resources/keycloak/realm-export.json:/tmp/realm-export.json -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:11.0.3

.PHONY: setup/oauth2-proxy-services
setup/oauth2-proxy-services: down/oauth2-proxy-services
# --skip-jwt-bearer-tokens enable the feature of accepting Authorization headers with Bearer TOKEN inside and not just only cookies
# See code: https://github.com/oauth2-proxy/oauth2-proxy/blob/f6ae15e8c37b15c7cc29332c1f070a06bc503dc7/oauthproxy.go#L260
docker run -d --rm --network=host --name oauth2-proxy quay.io/oauth2-proxy/oauth2-proxy:v7.2.1 \
--cookie-secret=SJ3QEg6Q0levwH5XAZjKKQ== --client-id=client-without-secret --email-domain="*" --provider=oidc \
--oidc-issuer-url="http://localhost:8088/auth/realms/integration" --client-secret="fake" --cookie-secure=false --cookie-name="oidc" \
--upstream="http://localhost:8080" --pass-authorization-header --pass-host-header --skip-provider-button --skip-jwt-bearer-tokens \
--skip-auth-preflight

.PHONY: setup/docs
setup/docs:
docker build -t mkdocs -f Dockerfile.docs .
Expand Down
19 changes: 19 additions & 0 deletions conf/config-example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ log:

# Authentication Providers
# authProviders:
# # Header providers
# # This authentication method should be used only with a software like [Oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) or an authentication gateway that put headers with user information inside.
# # Warning: S3-proxy won't validate headers value or anything else. It will take values as they are coming.
# header:
# oauth2-proxy:
# usernameHeader: x-forwarded-preferred-username
# emailHeader: x-forwarded-email
# groupsHeader: x-forwarded-groups
# # OIDC providers
# oidc:
# provider1:
# clientID: client-id
Expand All @@ -205,6 +214,7 @@ log:
# emailVerified: true # check email verified field from token
# # loginPath: /auth/provider1 # Override login path dynamically generated from provider key
# # callbackPath: /auth/provider1/callback # Override callback path dynamically generated from provider key
# # Basic auth providers
# basic:
# provider2:
# realm: My Basic Auth Realm
Expand Down Expand Up @@ -238,6 +248,11 @@ log:
# # NOTE: This list can be empty ([]) for authentication only and no group filter
# authorizationAccesses: # Authorization accesses : groups or email or regexp
# - group: devops_users
# # Header section for access filter
# header:
# # NOTE: This list can be empty ([]) for authentication only and no group filter
# authorizationAccesses: # Authorization accesses : groups or email or regexp
# - group: devops_users
# # Basic authentication section
# basic:
# credentials:
Expand Down Expand Up @@ -346,6 +361,10 @@ targets:
# keyRewriteList:
# - # Source represents a Regexp (golang format with group naming support)
# source: ^/(?P<one>\w+)/(?P<two>\w+)/(?P<three>\w+)?$
# # Target type: Regex or Template
# # Regex will allow to do a simple regex replace/update, like in the example
# # Template will allow to do golang template replace, like this example as "target" value: {{ regexReplaceAll "/input1(/.*)" .Key (printf "/input1/%s${1}" .User.Username) }}
# # targetType: REGEX # TEMPLATE
# # Target represents the template of the new key that will be used
# target: /$two/$one/$three/$one/
## Target custom templates
Expand Down
15 changes: 15 additions & 0 deletions docs/configuration/example.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,15 @@ log:

# Authentication Providers
# authProviders:
# # Header providers
# # This authentication method should be used only with a software like [Oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) or an authentication gateway that put headers with user information inside.
# # Warning: S3-proxy won't validate headers value or anything else. It will take values as they are coming.
# header:
# oauth2-proxy:
# usernameHeader: x-forwarded-preferred-username
# emailHeader: x-forwarded-email
# groupsHeader: x-forwarded-groups
# # OIDC providers
# oidc:
# provider1:
# clientID: client-id
Expand All @@ -215,6 +224,7 @@ log:
# emailVerified: true # check email verified field from token
# # loginPath: /auth/provider1 # Override login path dynamically generated from provider key
# # callbackPath: /auth/provider1/callback # Override callback path dynamically generated from provider key
# # Basic auth providers
# basic:
# provider2:
# realm: My Basic Auth Realm
Expand Down Expand Up @@ -248,6 +258,11 @@ log:
# # NOTE: This list can be empty ([]) for authentication only and no group filter
# authorizationAccesses: # Authorization accesses : groups or email or regexp
# - group: devops_users
# # Header section for access filter
# header:
# # NOTE: This list can be empty ([]) for authentication only and no group filter
# authorizationAccesses: # Authorization accesses : groups or email or regexp
# - group: devops_users
# # Basic authentication section
# basic:
# credentials:
Expand Down
53 changes: 35 additions & 18 deletions docs/configuration/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ You can see a full example in the [Example section](./example.md)
| internalServer | [ServerConfiguration](#serverconfiguration) | No | None | Internal Server configurations |
| template | [TemplateConfiguration](#templateconfiguration) | No | None | Template configurations |
| targets | Map[String][targetconfiguration](#targetconfiguration) | No | None | Targets configuration. Map key will be considered as the target name. (This will used in urls and list of targets.) |
| authProviders | [AuthProvidersConfiguration](#authProvidersconfiguration) | No | None | Authentication providers configuration |
| authProviders | [AuthProvidersConfiguration](#authprovidersconfiguration) | No | None | Authentication providers configuration |
| listTargets | [ListTargetsConfiguration](#listtargetsconfiguration) | No | None | List targets feature configuration |

## LogConfiguration
Expand Down Expand Up @@ -307,10 +307,26 @@ You can found more information [here](../feature-guide/webhooks.md) about webhoo

## AuthProvidersConfiguration

| Key | Type | Required | Default | Description |
| ----- | ------------------------------------------------------------ | -------- | ------- | ------------------------------------------------- |
| basic | [map[string]BasicAuthConfiguration](#basicauthconfiguration) | No | None | Basic Auth configuration and key as provider name |
| oidc | [map[string]OIDCAuthConfiguration](#oidcauthconfiguration) | No | None | OIDC Auth configuration and key as provider name |
| Key | Type | Required | Default | Description |
| ------ | -------------------------------------------------------------- | -------- | ------- | -------------------------------------------------- |
| basic | [map[string]BasicAuthConfiguration](#basicauthconfiguration) | No | None | Basic Auth configuration and key as provider name |
| oidc | [map[string]OIDCAuthConfiguration](#oidcauthconfiguration) | No | None | OIDC Auth configuration and key as provider name |
| header | [map[string]HeaderAuthConfiguration](#headerauthconfiguration) | No | None | Header Auth configuration and key as provider name |

## HeaderAuthConfiguration

This authentication method should be used only with a software like [Oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy) or an authentication gateway that put headers with user information inside.

<!-- prettier-ignore-start -->
!!! Warning
S3-proxy won't validate headers value or anything else. It will take values as they are coming.
<!-- prettier-ignore-end -->

| Key | Type | Required | Default | Description |
| -------------- | -------- | -------- | ------- | ---------------------------------------------------------------------------- |
| usernameHeader | String | Yes | None | Username header |
| emailHeader | String | Yes | None | Email header |
| groupsHeader | [String] | No | `""` | Groups header. Note: Value must be a list of groups separated by comas (`,`) |

## OIDCAuthConfiguration

Expand Down Expand Up @@ -338,20 +354,21 @@ You can found more information [here](../feature-guide/webhooks.md) about webhoo

## Resource

| Key | Type | Required | Default | Description |
| --------- | ------------------------------- | ----------------------------------- | ------- | ------------------------------------------------------------ |
| path | String | Yes | None | Path or matching path (e.g.: `/*`) |
| methods | [String] | No | `[GET]` | HTTP methods allowed (Allowed values `GET`, `PUT`, `DELETE`) |
| whiteList | Boolean | Required without oidc or basic | None | Is this path in white list ? E.g.: No authentication |
| oidc | [ResourceOIDC](#resourceoidc) | Required without whitelist or oidc | None | OIDC configuration authorization |
| basic | [ResourceBasic](#resourcebasic) | Required without whitelist or basic | None | Basic auth configuration |
| Key | Type | Required | Default | Description |
| --------- | ----------------------------------------- | ------------------------------------------- | ------- | ------------------------------------------------------------ |
| path | String | Yes | None | Path or matching path (e.g.: `/*`) |
| methods | [String] | No | `[GET]` | HTTP methods allowed (Allowed values `GET`, `PUT`, `DELETE`) |
| whiteList | Boolean | Required without oidc or basic | None | Is this path in white list ? E.g.: No authentication |
| basic | [ResourceBasic](#resourcebasic) | Required without whitelist, oidc or header | None | Basic auth configuration |
| oidc | [ResourceHeaderOIDC](#resourceheaderoidc) | Required without whitelist, basic or header | None | OIDC configuration authorization |
| header | [ResourceHeaderOIDC](#resourceheaderoidc) | Required without whitelist, oidc or basic | None | Header configuration authorization |

## ResourceOIDC
## ResourceHeaderOIDC

| Key | Type | Required | Default | Description |
| ---------------------- | --------------------------------------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| authorizationAccesses | [[OIDCAuthorizationAccesses]](#oidcauthorizationaccesses) | No | None | Authorization accesses matrix by group or email. If not set, authenticated users will be authorized (no group or email validation will be performed if authorizationOPAServer isn't set). |
| authorizationOPAServer | [OPAServerAuthorization](#opaserverauthorization) | No | None | Authorization through an OPA (Open Policy Agent) server |
| Key | Type | Required | Default | Description |
| ---------------------- | --------------------------------------------------------------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| authorizationAccesses | [[HeaderOIDCAuthorizationAccesses]](#headeroidcauthorizationaccesses) | No | None | Authorization accesses matrix by group or email. If not set, authenticated users will be authorized (no group or email validation will be performed if authorizationOPAServer isn't set). |
| authorizationOPAServer | [OPAServerAuthorization](#opaserverauthorization) | No | None | Authorization through an OPA (Open Policy Agent) server |

## OPAServerAuthorization

Expand All @@ -360,7 +377,7 @@ You can found more information [here](../feature-guide/webhooks.md) about webhoo
| url | String | Yes | None | URL of the OPA server including the data path (see the dedicated section for [OPA](../feature-guide/opa.md)) |
| tags | Map[String]String | No | `{}` | Data that will be added as tags in the OPA input data (see the dedicated section for [OPA](../feature-guide/opa.md)) |

## OIDCAuthorizationAccesses
## HeaderOIDCAuthorizationAccesses

| Key | Type | Required | Default | Description |
| ------ | ------- | ---------------------- | ------- | ---------------------------------------------- |
Expand Down
10 changes: 8 additions & 2 deletions local-resources/keycloak/realm-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,10 @@
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "565f78f2-a706-41cd-a1a0-431d7df29443",
"redirectUris": ["http://localhost:8080/auth/provider1/callback"],
"redirectUris": [
"http://localhost:8080/auth/provider1/callback",
"http://localhost:4180/*"
],
"webOrigins": ["http://localhost:8080"],
"notBefore": 0,
"bearerOnly": false,
Expand Down Expand Up @@ -785,7 +788,10 @@
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"secret": "**********",
"redirectUris": ["http://localhost:8080/auth/provider2/callback"],
"redirectUris": [
"http://localhost:8080/auth/provider2/callback",
"http://localhost:4180/*"
],
"webOrigins": ["http://localhost:8080"],
"notBefore": 0,
"bearerOnly": false,
Expand Down
2 changes: 1 addition & 1 deletion pkg/s3-proxy/authx/authentication/basic-auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ func (s *service) basicAuthMiddleware(res *config.Resource) func(http.Handler) h
return
}

logEntry.Info("Basic auth user %s authenticated", buser.GetIdentifier())
logEntry.Infof("Basic auth user %s authenticated", buser.GetIdentifier())
s.metricsCl.IncAuthenticated("basic-auth", res.Provider)

next.ServeHTTP(w, r)
Expand Down
84 changes: 84 additions & 0 deletions pkg/s3-proxy/authx/authentication/header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package authentication

import (
"net/http"
"strings"

"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/authx/models"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/bucket"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/config"
"github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/log"
responsehandler "github.com/oxyno-zeta/s3-proxy/pkg/s3-proxy/response-handler"
"github.com/pkg/errors"
)

func (s *service) headerAuthMiddleware(res *config.Resource) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get header configuration
headerAuthCfg := s.cfg.AuthProviders.Header[res.Provider]
// Get logger from request
logEntry := log.GetLoggerFromContext(r.Context())
// Get bucket request context from request
brctx := bucket.GetBucketRequestContextFromContext(r.Context())
// Get response handler
resHan := responsehandler.GetResponseHandlerFromContext(r.Context())

// Get Email header value
emailHValue := r.Header.Get(headerAuthCfg.EmailHeader)
// Get Username header value
usernameHValue := r.Header.Get(headerAuthCfg.UsernameHeader)
// Get groups header value
groupsHValue := r.Header.Get(headerAuthCfg.GroupsHeader)

// Check if email or username aren't set
if emailHValue == "" || usernameHValue == "" {
// Initialize error
var err error
// Switch
switch {
case emailHValue == "":
err = errors.New("cannot find email value from header")
case usernameHValue == "":
err = errors.New("cannot find username value from header")
}

// Check if bucket request context doesn't exist to use local default files
if brctx == nil {
responsehandler.GeneralInternalServerError(r, w, s.cfgManager, err)
} else {
resHan.InternalServerError(brctx.LoadFileContent, err)
}

return
}

// Initialize groups
var groups []string
// Check if groups is set
if groupsHValue != "" {
groups = strings.Split(groupsHValue, ",")
}

// Create Header auth user
huser := &models.HeaderUser{
Username: usernameHValue,
Email: emailHValue,
Groups: groups,
}

// Add user to request context by creating a new context
ctx := models.SetAuthenticatedUserInContext(r.Context(), huser)
// Create new request with new context
r = r.WithContext(ctx)

// Update response handler to have the latest context values
resHan.UpdateRequestAndResponse(r, w)

logEntry.Infof("Header auth user %s authenticated", huser.GetIdentifier())
s.metricsCl.IncAuthenticated("header-auth", res.Provider)

next.ServeHTTP(w, r)
})
}
}
10 changes: 9 additions & 1 deletion pkg/s3-proxy/authx/authentication/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,15 @@ func (s *service) Middleware(resources []*config.Resource) func(http.Handler) ht
// Check if OIDC is enabled
if res.OIDC != nil {
logEntry.Debug("authentication with oidc detected")
s.oidcAuthorizationMiddleware(res)(next).ServeHTTP(w, r)
s.oidcAuthMiddleware(res)(next).ServeHTTP(w, r)

return
}

// Check if Header auth is enabled
if res.Header != nil {
logEntry.Debug("authentication with header detected")
s.headerAuthMiddleware(res)(next).ServeHTTP(w, r)

return
}
Expand Down
Loading

0 comments on commit 6c56c5c

Please sign in to comment.