Skip to content

Commit

Permalink
Merge pull request #92 from owncloud/pre-signed-url-configuration
Browse files Browse the repository at this point in the history
Pre signed url configuration
  • Loading branch information
C0rby authored Aug 20, 2020
2 parents a51b3c8 + 1070c61 commit 84cadfb
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 21 deletions.
7 changes: 7 additions & 0 deletions changelog/unreleased/pre-signed-url-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: add configuration options for the pre-signed url middleware

Added an option to define allowed http methods for pre-signed url requests.
This is useful since we only want clients to GET resources and don't upload anything with presigned requests.

https://github.com/owncloud/ocis-proxy/issues/91
https://github.com/owncloud/product/issues/150
2 changes: 2 additions & 0 deletions pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func Server(cfg *config.Config) *cli.Command {
if cfg.HTTP.Root != "/" {
cfg.HTTP.Root = strings.TrimSuffix(cfg.HTTP.Root, "/")
}
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.
Expand Down Expand Up @@ -251,6 +252,7 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic
psMW := middleware.PresignedURL(
middleware.Logger(l),
middleware.Store(storepb.NewStoreService("com.owncloud.api.store", grpc.NewClient())),
middleware.PreSignedURLConfig(cfg.PreSignedURL),
)

// TODO this won't work with a registry other than mdns. Look into Micro's client initialization.
Expand Down
6 changes: 6 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type Config struct {
TokenManager TokenManager
PolicySelector *PolicySelector `mapstructure:"policy_selector"`
Reva Reva
PreSignedURL PreSignedURL
}

// OIDC is the config for the OpenID-Connect middleware. If set the proxy will try to authenticate every request
Expand All @@ -115,6 +116,11 @@ type TokenManager struct {
JWTSecret string
}

// PreSignedURL is the config for the presigned url middleware
type PreSignedURL struct {
AllowedHTTPMethods []string
}

// MigrationSelectorConf is the config for the migration-selector
type MigrationSelectorConf struct {
AccFoundPolicy string `mapstructure:"acc_found_policy"`
Expand Down
6 changes: 6 additions & 0 deletions pkg/flagset/flagset.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag {
EnvVars: []string{"PROXY_OIDC_INSECURE"},
Destination: &cfg.OIDC.Insecure,
},
&cli.StringSliceFlag{
Name: "presignedurl-allow-method",
Value: cli.NewStringSlice("GET"),
Usage: "--presignedurl-allow-method GET [--presignedurl-allow-method POST]",
EnvVars: []string{"PRESIGNEDURL_ALLOWED_METHODS"},
},
}

}
9 changes: 9 additions & 0 deletions pkg/middleware/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type Options struct {
RevaGatewayClient gateway.GatewayAPIClient
// Store for persisting data
Store storepb.StoreService
// PreSignedURLConfig to configure the middleware
PreSignedURLConfig config.PreSignedURL
}

// newOptions initializes the available default options.
Expand Down Expand Up @@ -99,3 +101,10 @@ func Store(sc storepb.StoreService) Option {
o.Store = sc
}
}

// PreSignedURLConfig provides a function to set the PreSignedURL config
func PreSignedURLConfig(cfg config.PreSignedURL) Option {
return func(o *Options) {
o.PreSignedURLConfig = cfg
}
}
75 changes: 54 additions & 21 deletions pkg/middleware/presigned_url.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ import (

"github.com/owncloud/ocis-pkg/v2/log"
ocisoidc "github.com/owncloud/ocis-pkg/v2/oidc"
"github.com/owncloud/ocis-proxy/pkg/config"
storepb "github.com/owncloud/ocis-store/pkg/proto/v0"
"golang.org/x/crypto/pbkdf2"
)

const (
iterations = 10000
keyLen = 32
)

// PresignedURL provides a middleware to check access secured by a presigned URL.
func PresignedURL(opts ...Option) func(next http.Handler) http.Handler {
opt := newOptions(opts...)
l := opt.Logger
cfg := opt.PreSignedURLConfig

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isSignedRequest(r) {
if signedRequestIsValid(l, r, opt.Store) {
if signedRequestIsValid(l, r, opt.Store, cfg) {
// use openid claims to let the account_uuid middleware do a lookup by username
claims := ocisoidc.StandardClaims{
OcisID: r.URL.Query().Get("OC-Credential"),
Expand All @@ -47,33 +54,56 @@ func isSignedRequest(r *http.Request) bool {
return r.URL.Query().Get("OC-Signature") != ""
}

func signedRequestIsValid(l log.Logger, r *http.Request, s storepb.StoreService) bool {
// cheap checks first
func signedRequestIsValid(l log.Logger, r *http.Request, s storepb.StoreService, cfg config.PreSignedURL) bool {
// TODO OC-Algorithm - defined the used algo (e.g. sha256 or sha512 - we should agree on one default algo and make this parameter optional)
// TODO OC-Verb - defines for which http verb the request is valid - defaults to GET OPTIONAL

return allRequiredParametersArePresent(r) &&
requestMethodMatches(r) &&
requestMethodIsAllowed(r.Method, cfg.AllowedHTTPMethods) &&
!urlIsExpired(r, time.Now) &&
signatureIsValid(l, r, s)
}

func allRequiredParametersArePresent(r *http.Request) bool {
// OC-Credential - defines the user scope (shall we use the owncloud user id here - this might leak internal data ....) REQUIRED
// OC-Date - defined the date the url was signed (ISO 8601 UTC) REQUIRED
// OC-Expires - defines the expiry interval in seconds (between 1 and 604800 = 7 days) REQUIRED
// TODO OC-Verb - defines for which http verb the request is valid - defaults to GET OPTIONAL
// OC-Signature - the computed signature - server will verify the request upon this REQUIRED
if r.URL.Query().Get("OC-Signature") == "" || r.URL.Query().Get("OC-Credential") == "" || r.URL.Query().Get("OC-Date") == "" || r.URL.Query().Get("OC-Expires") == "" || r.URL.Query().Get("OC-Verb") == "" {
return false
}
return r.URL.Query().Get("OC-Signature") != "" &&
r.URL.Query().Get("OC-Credential") != "" &&
r.URL.Query().Get("OC-Date") != "" &&
r.URL.Query().Get("OC-Expires") != "" &&
r.URL.Query().Get("OC-Verb") != ""
}

if !strings.EqualFold(r.Method, r.URL.Query().Get("OC-Verb")) {
return false
}
func requestMethodMatches(r *http.Request) bool {
return strings.EqualFold(r.Method, r.URL.Query().Get("OC-Verb"))
}

if t, err := time.Parse(time.RFC3339, r.URL.Query().Get("OC-Date")); err != nil {
return false
} else if expires, err := time.ParseDuration(r.URL.Query().Get("OC-Expires") + "s"); err != nil {
return false
} else {
t.Add(expires)
if t.After(time.Now()) {
return false
func requestMethodIsAllowed(m string, allowedMethods []string) bool {
for _, allowed := range allowedMethods {
if strings.EqualFold(m, allowed) {
return true
}
}
return false
}

func urlIsExpired(r *http.Request, now func() time.Time) bool {
t, err := time.Parse(time.RFC3339, r.URL.Query().Get("OC-Date"))
if err != nil {
return true
}
expires, err := time.ParseDuration(r.URL.Query().Get("OC-Expires") + "s")
if err != nil {
return true
}
t.Add(expires)
return t.After(now())
}

func signatureIsValid(l log.Logger, r *http.Request, s storepb.StoreService) bool {
signingKey, err := getSigningKey(r.Context(), s, r.URL.Query().Get("OC-Credential"))
if err != nil {
l.Error().Err(err).Msg("could not retrieve signing key")
Expand All @@ -92,14 +122,17 @@ func signedRequestIsValid(l log.Logger, r *http.Request, s storepb.StoreService)
if !r.URL.IsAbs() {
url = "https://" + r.Host + url // TODO where do we get the scheme from
}
return createSignature(url, signingKey) == signature
}

func createSignature(url string, signingKey []byte) string {
// the oc10 signature check: $hash = \hash_pbkdf2("sha512", $url, $signingKey, 10000, 64, false);
// - sets the length of the output string to 64
// - sets raw output to false -> if raw_output is FALSE length corresponds to twice the byte-length of the derived key (as every byte of the key is returned as two hexits).
// TODO change to length 128 in oc10?
// fo golangs pbkdf2.Key we need to use 32 because it will be encoded into 64 hexits later
hash := pbkdf2.Key([]byte(url), signingKey, 10000, 32, sha512.New)

return hex.EncodeToString(hash) == signature
hash := pbkdf2.Key([]byte(url), signingKey, iterations, keyLen, sha512.New)
return hex.EncodeToString(hash)
}

func getSigningKey(ctx context.Context, s storepb.StoreService, credential string) ([]byte, error) {
Expand Down
121 changes: 121 additions & 0 deletions pkg/middleware/presigned_url_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package middleware

import (
"net/http/httptest"
"testing"
"time"
)

func TestIsSignedRequest(t *testing.T) {
tests := []struct {
url string
expected bool
}{
{"https://example.com/example.jpg", false},
{"https://example.com/example.jpg?OC-Signature=something", true},
}

for _, tt := range tests {
r := httptest.NewRequest("", tt.url, nil)
result := isSignedRequest(r)
if result != tt.expected {
t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result)
}
}
}

func TestAllRequiredParametersPresent(t *testing.T) {
baseURL := "https://example.com/example.jpg?"
tests := []struct {
params string
expected bool
}{
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Expires=something&OC-Verb=something", true},
{"OC-Credential=something&OC-Date=something&OC-Expires=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Date=something&OC-Expires=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Credential=something&OC-Expires=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Verb=something", false},
{"OC-Signature=something&OC-Credential=something&OC-Date=something&OC-Expires=something", false},
}
for _, tt := range tests {
r := httptest.NewRequest("", baseURL+tt.params, nil)
result := allRequiredParametersArePresent(r)
if result != tt.expected {
t.Errorf("with %s expected %t got %t", tt.params, tt.expected, result)
}
}
}

func TestRequestMethodMatches(t *testing.T) {
tests := []struct {
method string
url string
expected bool
}{
{"GET", "https://example.com/example.jpg?OC-Verb=GET", true},
{"GET", "https://example.com/example.jpg?OC-Verb=get", true},
{"POST", "https://example.com/example.jpg?OC-Verb=GET", false},
}

for _, tt := range tests {
r := httptest.NewRequest(tt.method, tt.url, nil)
result := requestMethodMatches(r)
if result != tt.expected {
t.Errorf("with method %s and url %s expected %t got %t", tt.method, tt.url, tt.expected, result)
}
}
}

func TestRequestMethodIsAllowed(t *testing.T) {
tests := []struct {
method string
allowed []string
expected bool
}{
{"GET", []string{}, false},
{"GET", []string{"POST"}, false},
{"GET", []string{"GET"}, true},
{"GET", []string{"get"}, true},
{"GET", []string{"POST", "GET"}, true},
}

for _, tt := range tests {
result := requestMethodIsAllowed(tt.method, tt.allowed)
if result != tt.expected {
t.Errorf("with method %s and allowed methods %s expected %t got %t", tt.method, tt.allowed, tt.expected, result)
}
}
}

func TestUrlIsExpired(t *testing.T) {
nowFunc := func() time.Time {
t, _ := time.Parse(time.RFC3339, "2020-08-19T15:12:43.478Z")
return t
}

tests := []struct {
url string
expected bool
}{
{"http://example.com/example.jpg?OC-Date=2020-08-19T15:02:43.478Z&OC-Expires=1200", false},
{"http://example.com/example.jpg?OC-Date=invalid&OC-Expires=1200", true},
{"http://example.com/example.jpg?OC-Date=2020-08-19T15:02:43.478Z&OC-Expires=invalid", true},
}

for _, tt := range tests {
r := httptest.NewRequest("", tt.url, nil)
result := urlIsExpired(r, nowFunc)
if result != tt.expected {
t.Errorf("with %s expected %t got %t", tt.url, tt.expected, result)
}
}
}

func TestCreateSignature(t *testing.T) {
expected := "27d2ebea381384af3179235114801dcd00f91e46f99fca72575301cf3948101d"
s := createSignature("something", []byte("somerandomkey"))

if s != expected {
t.Fail()
}
}

0 comments on commit 84cadfb

Please sign in to comment.