mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-04 03:09:33 -06:00
Configureable via: PROXY_ACCOUNT_BACKEND_TYPE=cs3 PROXY_ACCOUNT_BACKEND_TYPE=accounts (default) By using a backend which implements the CS3 user-api (currently provided by reva/storage) it is possible to bypass the ocis-accounts service and for example use ldap directly. Hides user and auth related communication behind a facade (user/backend) to minimize logic-duplication across middlewares. Allows to switich the account backend from accounts to cs3. Co-authored-by: Jörn Friedrich Dreyer <jfd@butonic.de>
213 lines
6.2 KiB
Go
213 lines
6.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
revauser "github.com/cs3org/reva/pkg/user"
|
|
"github.com/owncloud/ocis/proxy/pkg/user/backend"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/owncloud/ocis/ocis-pkg/log"
|
|
"github.com/owncloud/ocis/proxy/pkg/config"
|
|
store "github.com/owncloud/ocis/store/pkg/proto/v0"
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
// SignedURLAuth provides a middleware to check access secured by a signed URL.
|
|
func SignedURLAuth(optionSetters ...Option) func(next http.Handler) http.Handler {
|
|
options := newOptions(optionSetters...)
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
return &signedURLAuth{
|
|
next: next,
|
|
logger: options.Logger,
|
|
preSignedURLConfig: options.PreSignedURLConfig,
|
|
store: options.Store,
|
|
userProvider: options.UserProvider,
|
|
}
|
|
}
|
|
}
|
|
|
|
type signedURLAuth struct {
|
|
next http.Handler
|
|
logger log.Logger
|
|
preSignedURLConfig config.PreSignedURL
|
|
userProvider backend.UserBackend
|
|
store store.StoreService
|
|
}
|
|
|
|
func (m signedURLAuth) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
if !m.shouldServe(req) {
|
|
m.next.ServeHTTP(w, req)
|
|
return
|
|
}
|
|
|
|
user, err := m.userProvider.GetUserByClaims(req.Context(), "username", req.URL.Query().Get("OC-Credential"), true)
|
|
if err != nil {
|
|
m.logger.Error().Err(err).Msg("Could not get user by claim")
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}
|
|
|
|
ctx := revauser.ContextSetUser(req.Context(), user)
|
|
|
|
req = req.WithContext(ctx)
|
|
|
|
if err := m.validate(req); err != nil {
|
|
http.Error(w, "Invalid url signature", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
m.next.ServeHTTP(w, req)
|
|
}
|
|
|
|
func (m signedURLAuth) shouldServe(req *http.Request) bool {
|
|
if !m.preSignedURLConfig.Enabled {
|
|
return false
|
|
}
|
|
return req.URL.Query().Get("OC-Signature") != ""
|
|
}
|
|
|
|
func (m signedURLAuth) validate(req *http.Request) (err error) {
|
|
query := req.URL.Query()
|
|
|
|
if ok, err := m.allRequiredParametersArePresent(query); !ok {
|
|
return err
|
|
}
|
|
|
|
if ok, err := m.requestMethodMatches(req.Method, query); !ok {
|
|
return err
|
|
}
|
|
|
|
if ok, err := m.requestMethodIsAllowed(req.Method); !ok {
|
|
return err
|
|
}
|
|
|
|
if expired, err := m.urlIsExpired(query, time.Now); expired {
|
|
return err
|
|
}
|
|
|
|
if ok, err := m.signatureIsValid(req); !ok {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m signedURLAuth) allRequiredParametersArePresent(query url.Values) (ok bool, err error) {
|
|
// check if required query parameters exist in given request query parameters
|
|
// OC-Signature - the computed signature - server will verify the request upon this REQUIRED
|
|
// 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
|
|
for _, p := range []string{
|
|
"OC-Signature",
|
|
"OC-Credential",
|
|
"OC-Date",
|
|
"OC-Expires",
|
|
"OC-Verb",
|
|
} {
|
|
if query.Get(p) == "" {
|
|
return false, fmt.Errorf("required %s parameter not found", p)
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (m signedURLAuth) requestMethodMatches(meth string, query url.Values) (ok bool, err error) {
|
|
// check if given url query parameter OC-Verb matches given request method
|
|
if !strings.EqualFold(meth, query.Get("OC-Verb")) {
|
|
return false, errors.New("required OC-Verb parameter did not match request method")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (m signedURLAuth) requestMethodIsAllowed(meth string) (ok bool, err error) {
|
|
// check if given request method is allowed
|
|
methodIsAllowed := false
|
|
for _, am := range m.preSignedURLConfig.AllowedHTTPMethods {
|
|
if strings.EqualFold(meth, am) {
|
|
methodIsAllowed = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !methodIsAllowed {
|
|
return false, errors.New("request method is not listed in PreSignedURLConfig AllowedHTTPMethods")
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
func (m signedURLAuth) urlIsExpired(query url.Values, now func() time.Time) (expired bool, err error) {
|
|
// check if url is expired by checking if given date (OC-Date) + expires in seconds (OC-Expires) is after now
|
|
validFrom, err := time.Parse(time.RFC3339, query.Get("OC-Date"))
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
requestExpiry, err := time.ParseDuration(query.Get("OC-Expires") + "s")
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
|
|
validTo := validFrom.Add(requestExpiry)
|
|
|
|
return !(now().After(validFrom) && now().Before(validTo)), nil
|
|
}
|
|
|
|
func (m signedURLAuth) signatureIsValid(req *http.Request) (ok bool, err error) {
|
|
u := revauser.ContextMustGetUser(req.Context())
|
|
signingKey, err := m.getSigningKey(req.Context(), u.Id.OpaqueId)
|
|
if err != nil {
|
|
m.logger.Error().Err(err).Msg("could not retrieve signing key")
|
|
return false, err
|
|
}
|
|
if len(signingKey) == 0 {
|
|
m.logger.Error().Err(err).Msg("signing key empty")
|
|
return false, err
|
|
}
|
|
q := req.URL.Query()
|
|
signature := q.Get("OC-Signature")
|
|
q.Del("OC-Signature")
|
|
req.URL.RawQuery = q.Encode()
|
|
url := req.URL.String()
|
|
if !req.URL.IsAbs() {
|
|
url = "https://" + req.Host + url // TODO where do we get the scheme from
|
|
}
|
|
|
|
return m.createSignature(url, signingKey) == signature, nil
|
|
}
|
|
|
|
func (m signedURLAuth) 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)
|
|
}
|
|
|
|
func (m signedURLAuth) getSigningKey(ctx context.Context, ocisID string) ([]byte, error) {
|
|
res, err := m.store.Read(ctx, &store.ReadRequest{
|
|
Options: &store.ReadOptions{
|
|
Database: "proxy",
|
|
Table: "signing-keys",
|
|
},
|
|
Key: ocisID,
|
|
})
|
|
if err != nil || len(res.Records) < 1 {
|
|
return []byte{}, err
|
|
}
|
|
|
|
return res.Records[0].Value, nil
|
|
}
|