Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth: add www-authenticate based on user agent #1009

Merged
merged 27 commits into from
Dec 4, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
ugly working draft
  • Loading branch information
refs committed Dec 2, 2020
commit 2910e88ba5d8af0586d25c36bd99b3a15860d24a
15 changes: 13 additions & 2 deletions proxy/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package command
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"os"
"os/signal"
Expand Down Expand Up @@ -48,8 +49,17 @@ func Server(cfg *config.Config) *cli.Command {
}
cfg.PreSignedURL.AllowedHTTPMethods = ctx.StringSlice("presignedurl-allow-method")

// When running on single binary mode the before hook from the root command won't get called. We manually
// call this before hook from ocis command, so the configuration can be loaded.
cfg.Reva.Middleware.Auth.CredentialsByUserAgent = make(map[string]string, 0)
uaw := ctx.StringSlice("proxy-user-agent-whitelist")
for _, v := range uaw {
parts := strings.Split(v, ":")
if len(parts) != 2 {
return fmt.Errorf("unexpected config value for user-agent whitelist: %v, expected format is userAgent:challenge", v)
}

cfg.Reva.Middleware.Auth.CredentialsByUserAgent[parts[0]] = parts[1]
}

return ParseConfig(ctx, cfg)
},
Action: func(c *cli.Context) error {
Expand Down Expand Up @@ -288,6 +298,7 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
middleware.EnableBasicAuth(cfg.EnableBasicAuth),
middleware.AccountsClient(accountsClient),
middleware.OIDCIss(cfg.OIDC.Issuer),
middleware.CredentialsByUserAgent(cfg.Reva.Middleware.Auth.CredentialsByUserAgent),
),
middleware.SignedURLAuth(
middleware.Logger(l),
Expand Down
11 changes: 10 additions & 1 deletion proxy/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@ var (

// Reva defines all available REVA configuration.
type Reva struct {
Address string
Address string
Middleware Middleware
}

type Middleware struct {
Auth Auth
}

type Auth struct {
CredentialsByUserAgent map[string]string
}

// Cache is a TTL cache configuration.
Expand Down
9 changes: 8 additions & 1 deletion proxy/pkg/flagset/flagset.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,15 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
EnvVars: []string{"PROXY_ENABLE_BASIC_AUTH"},
Destination: &cfg.EnableBasicAuth,
},
}

// Reva Middlewares Config
&cli.StringSliceFlag{
Name: "proxy-user-agent-whitelist", // TODO naming?
Value: cli.NewStringSlice(""),
Usage: "TODO",
EnvVars: []string{"PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT"},
},
}
}

// ListProxyWithConfig applies the config to the list commands flags.
Expand Down
2 changes: 2 additions & 0 deletions proxy/pkg/middleware/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,15 @@ func Authentication(opts ...Option) func(next http.Handler) http.Handler {
OIDCIss(options.OIDCIss),
TokenCacheSize(options.UserinfoCacheSize),
TokenCacheTTL(time.Second*time.Duration(options.UserinfoCacheTTL)),
CredentialsByUserAgent(options.CredentialsByUserAgent),
)

basic := BasicAuth(
Logger(options.Logger),
EnableBasicAuth(options.EnableBasicAuth),
AccountsClient(options.AccountsClient),
OIDCIss(options.OIDCIss),
CredentialsByUserAgent(options.CredentialsByUserAgent),
)

return func(next http.Handler) http.Handler {
Expand Down
32 changes: 29 additions & 3 deletions proxy/pkg/middleware/basic_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,47 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
if !h.isPublicLink(req) {
for i := 0; i < len(ProxyWwwAuthenticate); i++ {
if strings.Contains(req.RequestURI, fmt.Sprintf("/%v/", ProxyWwwAuthenticate[i])) {
for k, v := range options.CredentialsByUserAgent {
if strings.Contains(k, req.UserAgent()) {
w.Header().Del("Www-Authenticate")
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host))
goto OUT
}
}
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", "Basic", req.Host))
}
}
}
OUT:
next.ServeHTTP(w, req)
return
}

w.Header().Del("Www-Authenticate")

account, ok := h.getAccount(req)

// touch is a user agent locking guard, when touched changes to true it indicates the User-Agent on the
// request is configured to support only one challenge, it it remains untouched, there are no considera-
// tions and we should write all available authentication challenges to the response.
touch := false

if !ok {
for i := 0; i < len(ProxyWwwAuthenticate); i++ {
if strings.Contains(req.RequestURI, fmt.Sprintf("/%v/", ProxyWwwAuthenticate[i])) {
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", "Basic", req.Host))
// if the request is bound to a user agent the locked write Www-Authenticate for such user
for k, v := range options.CredentialsByUserAgent {
if strings.Contains(k, req.UserAgent()) {
w.Header().Del("Www-Authenticate")
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host))
touch = true
break
}
}

// if the request is not bound to any user agent, write all available challenges
if !touch {
writeSupportedAuthenticateHeader(w, req)
}

w.WriteHeader(http.StatusUnauthorized)
return
}
Expand Down
15 changes: 8 additions & 7 deletions proxy/pkg/middleware/oidc_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,19 @@ func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
// there is no bearer token on the request,
if !h.shouldServe(req) {
// oidc supported but token not present, add header and handover to the next middleware.

// TODO for this logic to work and we don't return superfluous Www-Authenticate headers we would need to
// add Www-Authenticate only on selected endpoints, because Reva won't cleanup already written headers.
// this means that requests such as:
// curl -v -k -u admin:admin -H "depth: 0" -X PROPFIND https://localhost:9200/remote.php/dav/files | xmllint --format -
// even when succeeding, will contain a Www-Authenticate header.

for i := 0; i < len(ProxyWwwAuthenticate); i++ {
if strings.Contains(req.RequestURI, fmt.Sprintf("/%v/", ProxyWwwAuthenticate[i])) {
for k, v := range options.CredentialsByUserAgent {
if strings.Contains(k, req.UserAgent()) {
w.Header().Del("Www-Authenticate")
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host))
goto OUT
}
}
w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", "Bearer", req.Host))
}
}
OUT:
next.ServeHTTP(w, req)
return
}
Expand Down
9 changes: 9 additions & 0 deletions proxy/pkg/middleware/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Options struct {
UserinfoCacheSize int
// UserinfoCacheTTL sets the max cache duration for the userinfo cache, intended for the oidc_auth middleware
UserinfoCacheTTL time.Duration
// CredentialsByUserAgent sets the auth challenges on a per user-agent basis
CredentialsByUserAgent map[string]string
}

// newOptions initializes the available default options.
Expand Down Expand Up @@ -108,6 +110,13 @@ func OIDCIss(iss string) Option {
}
}

// CredentialsByUserAgent sets UserAgentChallenges.
func CredentialsByUserAgent(v map[string]string) Option {
return func(o *Options) {
o.CredentialsByUserAgent = v
}
}

// RevaGatewayClient provides a function to set the the reva gateway service client option.
func RevaGatewayClient(gc gateway.GatewayAPIClient) Option {
return func(o *Options) {
Expand Down
2 changes: 1 addition & 1 deletion storage/pkg/command/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func Frontend(cfg *config.Config) *cli.Command {
for _, v := range uaw {
parts := strings.Split(v, ":")
if len(parts) != 2 {
return fmt.Errorf("unexpected config value for user-agent whitelist: %v, expected format is user-agent:challenge", v) // TODO wording + error wrapping?
return fmt.Errorf("unexpected config value for user-agent whitelist: %v, expected format is user-agent:challenge", v)
}

cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent[parts[0]] = parts[1]
Expand Down
2 changes: 1 addition & 1 deletion storage/pkg/flagset/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func FrontendWithConfig(cfg *config.Config) []cli.Flag {
Destination: &cfg.Reva.UploadHTTPMethodOverride,
},

// Middlewares
// Reva Middlewares Config
&cli.StringSliceFlag{
Name: "user-agent-whitelist", // TODO naming?
Value: cli.NewStringSlice("test"),
Expand Down