mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2025-12-31 01:10:20 -06:00
With the ocdav service being able to provided signed download URLs we need the proxy to be able to verify the signatures. This should also be a first step towards phasing out the weird ocs based client side signed urls. Related Tickets: #1104
324 lines
9.3 KiB
Go
324 lines
9.3 KiB
Go
package middleware
|
|
|
|
import (
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/opencloud-eu/opencloud/pkg/log"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/config"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend"
|
|
"github.com/opencloud-eu/opencloud/services/proxy/pkg/userroles"
|
|
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
|
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
|
|
microstore "go-micro.dev/v4/store"
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
const (
|
|
_paramOCSignature = "OC-Signature"
|
|
_paramOCCredential = "OC-Credential" // #nosec G101
|
|
_paramOCDate = "OC-Date"
|
|
_paramOCExpires = "OC-Expires"
|
|
_paramOCVerb = "OC-Verb"
|
|
_paramOCAlgo = "OC-Algo"
|
|
_paramOCJWTSig = "oc-jwt-sig"
|
|
)
|
|
|
|
var (
|
|
_requiredParams = [...]string{
|
|
_paramOCSignature,
|
|
_paramOCCredential,
|
|
_paramOCDate,
|
|
_paramOCExpires,
|
|
_paramOCVerb,
|
|
}
|
|
)
|
|
|
|
// SignedURLAuthenticator is the authenticator responsible for authenticating signed URL requests.
|
|
type SignedURLAuthenticator struct {
|
|
Logger log.Logger
|
|
PreSignedURLConfig config.PreSignedURL
|
|
UserProvider backend.UserBackend
|
|
UserRoleAssigner userroles.UserRoleAssigner
|
|
Store microstore.Store
|
|
Now func() time.Time
|
|
URLVerifier signedurl.Verifier
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) shouldServeLegacy(req *http.Request) bool {
|
|
if !m.PreSignedURLConfig.Enabled {
|
|
return false
|
|
}
|
|
return req.URL.Query().Get(_paramOCSignature) != ""
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) shouldServe(req *http.Request) bool {
|
|
if m.URLVerifier == nil {
|
|
return false
|
|
}
|
|
return req.URL.Query().Get(_paramOCJWTSig) != ""
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) validate(req *http.Request) (err error) {
|
|
query := req.URL.Query()
|
|
|
|
if err := m.allRequiredParametersArePresent(query); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := m.requestMethodMatches(req.Method, query); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := m.requestMethodIsAllowed(req.Method); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = m.urlIsExpired(query); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := m.signatureIsValid(req); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) allRequiredParametersArePresent(query url.Values) (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 opencloud 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 _requiredParams {
|
|
if query.Get(p) == "" {
|
|
return fmt.Errorf("required %s parameter not found", p)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) requestMethodMatches(meth string, query url.Values) (err error) {
|
|
// check if given url query parameter OC-Verb matches given request method
|
|
if !strings.EqualFold(meth, query.Get(_paramOCVerb)) {
|
|
return errors.New("required OC-Verb parameter did not match request method")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) requestMethodIsAllowed(meth string) (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 errors.New("request method is not listed in PreSignedURLConfig AllowedHTTPMethods")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) urlIsExpired(query url.Values) (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(_paramOCDate))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
requestExpiry, err := time.ParseDuration(query.Get(_paramOCExpires) + "s")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
validTo := validFrom.Add(requestExpiry)
|
|
if !(m.Now().Before(validTo)) {
|
|
return errors.New("URL is expired")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) signatureIsValid(req *http.Request) (err error) {
|
|
c := revactx.ContextMustGetUser(req.Context())
|
|
signingKey, err := m.Store.Read(c.Id.OpaqueId)
|
|
if err != nil {
|
|
m.Logger.Error().Err(err).Msg("could not retrieve signing key")
|
|
return err
|
|
}
|
|
if len(signingKey[0].Value) == 0 {
|
|
m.Logger.Error().Err(err).Msg("signing key empty")
|
|
return err
|
|
}
|
|
u := m.buildUrlToSign(req)
|
|
computedSignature := m.createSignature(u, signingKey[0].Value)
|
|
signatureInURL := req.URL.Query().Get(_paramOCSignature)
|
|
if computedSignature == signatureInURL {
|
|
return nil
|
|
}
|
|
|
|
// try a workaround for https://github.com/owncloud/ocis/issues/10180
|
|
// Some reverse proxies might replace $ with %24 in the URL leading to a mismatch in the signature
|
|
u = strings.Replace(u, "$", "%24", 1)
|
|
computedSignature = m.createSignature(u, signingKey[0].Value)
|
|
signatureInURL = req.URL.Query().Get(_paramOCSignature)
|
|
if computedSignature == signatureInURL {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("signature mismatch: expected %s != actual %s", computedSignature, signatureInURL)
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) buildUrlToSign(req *http.Request) string {
|
|
q := req.URL.Query()
|
|
|
|
// only params required for signing
|
|
signParameters := make(url.Values)
|
|
signParameters.Add(_paramOCCredential, q.Get(_paramOCCredential))
|
|
signParameters.Add(_paramOCDate, q.Get(_paramOCDate))
|
|
signParameters.Add(_paramOCExpires, q.Get(_paramOCExpires))
|
|
signParameters.Add(_paramOCVerb, q.Get(_paramOCVerb))
|
|
|
|
// remaining query params
|
|
q.Del(_paramOCAlgo)
|
|
q.Del(_paramOCCredential)
|
|
q.Del(_paramOCDate)
|
|
q.Del(_paramOCExpires)
|
|
q.Del(_paramOCSignature)
|
|
q.Del(_paramOCVerb)
|
|
|
|
url := *req.URL
|
|
if len(q) == 0 {
|
|
url.RawQuery = signParameters.Encode()
|
|
} else {
|
|
url.RawQuery = strings.Join([]string{q.Encode(), signParameters.Encode()}, "&")
|
|
}
|
|
u := url.String()
|
|
if !url.IsAbs() {
|
|
u = "https://" + req.Host + u // TODO where do we get the scheme
|
|
}
|
|
return u
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) 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)
|
|
}
|
|
|
|
// Authenticate implements the authenticator interface to authenticate requests via signed URL auth.
|
|
func (m SignedURLAuthenticator) Authenticate(r *http.Request) (*http.Request, bool) {
|
|
switch {
|
|
case m.shouldServeLegacy(r):
|
|
return m.authenticateLegacy(r)
|
|
case m.shouldServe(r):
|
|
return m.authenticate(r)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (m SignedURLAuthenticator) authenticate(r *http.Request) (*http.Request, bool) {
|
|
u := r.URL.String()
|
|
if !r.URL.IsAbs() {
|
|
u = "https://" + r.Host + u
|
|
}
|
|
|
|
userid, err := m.URLVerifier.Verify(u)
|
|
if err != nil {
|
|
m.Logger.Error().
|
|
Err(err).
|
|
Str("authenticator", "signed_url_jwt").
|
|
Str("path", r.URL.Path).
|
|
Str("url", u).
|
|
Msg("Could not verify JWT signature")
|
|
return nil, false
|
|
}
|
|
user, _, err := m.UserProvider.GetUserByClaims(r.Context(), "userid", userid)
|
|
if err != nil {
|
|
m.Logger.Error().
|
|
Err(err).
|
|
Str("authenticator", "signed_url_jwt").
|
|
Str("path", r.URL.Path).
|
|
Msg("Could not get user by claim")
|
|
return nil, false
|
|
}
|
|
user, err = m.UserRoleAssigner.ApplyUserRole(r.Context(), user)
|
|
if err != nil {
|
|
m.Logger.Error().
|
|
Err(err).
|
|
Str("authenticator", "signed_url").
|
|
Str("path", r.URL.Path).
|
|
Msg("Could not get user by claim")
|
|
return nil, false
|
|
}
|
|
ctx := revactx.ContextSetUser(r.Context(), user)
|
|
r = r.WithContext(ctx)
|
|
m.Logger.Debug().
|
|
Str("authenticator", "signed_url").
|
|
Str("path", r.URL.Path).
|
|
Msg("successfully authenticated request")
|
|
return r, true
|
|
}
|
|
|
|
// authenticateLegacy is a helper function to authenticate requests that use the legacy
|
|
// client side signed URLs
|
|
func (m SignedURLAuthenticator) authenticateLegacy(r *http.Request) (*http.Request, bool) {
|
|
user, _, err := m.UserProvider.GetUserByClaims(r.Context(), "username", r.URL.Query().Get(_paramOCCredential))
|
|
if err != nil {
|
|
m.Logger.Error().
|
|
Err(err).
|
|
Str("authenticator", "signed_url").
|
|
Str("path", r.URL.Path).
|
|
Msg("Could not get user by claim")
|
|
return nil, false
|
|
}
|
|
|
|
user, err = m.UserRoleAssigner.ApplyUserRole(r.Context(), user)
|
|
if err != nil {
|
|
m.Logger.Error().
|
|
Err(err).
|
|
Str("authenticator", "signed_url").
|
|
Str("path", r.URL.Path).
|
|
Msg("Could not get user by claim")
|
|
return nil, false
|
|
}
|
|
|
|
ctx := revactx.ContextSetUser(r.Context(), user)
|
|
|
|
r = r.WithContext(ctx)
|
|
|
|
if err := m.validate(r); err != nil {
|
|
m.Logger.Error().
|
|
Err(err).
|
|
Str("authenticator", "signed_url").
|
|
Str("path", r.URL.Path).
|
|
Str("url", r.URL.String()).
|
|
Msg("Could not get user by claim")
|
|
return nil, false
|
|
}
|
|
|
|
m.Logger.Debug().
|
|
Str("authenticator", "signed_url").
|
|
Str("path", r.URL.Path).
|
|
Msg("successfully authenticated request")
|
|
return r, true
|
|
}
|