Skip to content

Commit

Permalink
signature authentication for public links (#1590)
Browse files Browse the repository at this point in the history
  • Loading branch information
C0rby authored Mar 26, 2021
1 parent 23edd54 commit e8a00d9
Showing 13 changed files with 245 additions and 53 deletions.
8 changes: 8 additions & 0 deletions changelog/unreleased/public-link-signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: Add signature authentication for public links

Implemented signature authentication for public links in addition to the existing password authentication.
This allows web clients to efficiently download files from password protected public shares.

https://github.com/cs3org/cs3apis/issues/110
https://github.com/cs3org/reva/pull/1590

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ require (
github.com/cheggaaa/pb v1.0.29
github.com/coreos/go-oidc v2.2.1+incompatible
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e
github.com/cs3org/go-cs3apis v0.0.0-20210322124405-872bbbf14d0b
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59
github.com/go-ldap/ldap/v3 v3.2.4
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -144,6 +144,8 @@ github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJff
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
github.com/cs3org/go-cs3apis v0.0.0-20210322124405-872bbbf14d0b h1:80DK9Yufaj1YJ0fPb6x1WZfijHWA+CMstq3MEZs/8To=
github.com/cs3org/go-cs3apis v0.0.0-20210322124405-872bbbf14d0b/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535 h1:555D8A3ddKqb4OyK9v5mdphw2zDLWKGXOkcnf1RQwTA=
github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Original file line number Diff line number Diff line change
@@ -149,7 +149,7 @@ func (s *service) GetPublicShareByToken(ctx context.Context, req *link.GetPublic
log.Debug().Msg("getting public share by token")

// there are 2 passes here, and the second request has no password
found, err := s.sm.GetPublicShareByToken(ctx, req.GetToken(), req.GetPassword())
found, err := s.sm.GetPublicShareByToken(ctx, req.GetToken(), req.GetAuthentication(), req.GetSign())
switch v := err.(type) {
case nil:
return &link.GetPublicShareByTokenResponse{
@@ -180,7 +180,7 @@ func (s *service) GetPublicShare(ctx context.Context, req *link.GetPublicShareRe
log.Error().Msg("error getting user from context")
}

found, err := s.sm.GetPublicShare(ctx, u, req.Ref)
found, err := s.sm.GetPublicShare(ctx, u, req.Ref, req.GetSign())
if err != nil {
return nil, err
}
@@ -196,7 +196,7 @@ func (s *service) ListPublicShares(ctx context.Context, req *link.ListPublicShar
log.Info().Str("publicshareprovider", "list").Msg("list public share")
user, _ := user.ContextGetUser(ctx)

shares, err := s.sm.ListPublicShares(ctx, user, req.Filters, &provider.ResourceInfo{})
shares, err := s.sm.ListPublicShares(ctx, user, req.Filters, &provider.ResourceInfo{}, req.GetSign())
if err != nil {
log.Err(err).Msg("error listing shares")
return &link.ListPublicSharesResponse{
Original file line number Diff line number Diff line change
@@ -552,6 +552,9 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer
for i := range listContainerR.Infos {
filterPermissions(listContainerR.Infos[i].PermissionSet, ls.GetPermissions().Permissions)
listContainerR.Infos[i].Path = path.Join(s.mountPath, "/", tkn, relativePath, path.Base(listContainerR.Infos[i].Path))
if err := addShare(listContainerR.Infos[i], ls); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("share", ls).Interface("info", listContainerR.Infos[i]).Msg("error when adding share")
}
}

return listContainerR, nil
@@ -679,6 +682,7 @@ func (s *service) resolveToken(ctx context.Context, token string) (string, *link
Token: token,
},
},
Sign: true,
},
)
switch {
@@ -697,6 +701,5 @@ func (s *service) resolveToken(ctx context.Context, token string) (string, *link
case pathRes.Status.Code != rpc.Code_CODE_OK:
return "", nil, pathRes.Status, nil
}

return pathRes.Path, publicShareResponse.GetShare(), nil, nil
}
40 changes: 33 additions & 7 deletions internal/http/services/owncloud/ocdav/dav.go
Original file line number Diff line number Diff line change
@@ -171,16 +171,22 @@ func (h *DavHandler) Handler(s *svc) http.Handler {
w.WriteHeader(http.StatusNotFound)
}

_, pass, _ := r.BasicAuth()
var res *gatewayv1beta1.AuthenticateResponse
token, _ := router.ShiftPath(r.URL.Path)

authenticateRequest := gatewayv1beta1.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: pass,
if _, pass, ok := r.BasicAuth(); ok {
res, err = handleBasicAuth(r.Context(), c, token, pass)
} else {
q := r.URL.Query()
sig := q.Get("signature")
expiration := q.Get("expiration")
// We restrict the pre-signed urls to downloads.
if sig != "" && expiration != "" && r.Method != http.MethodGet {
w.WriteHeader(http.StatusUnauthorized)
return
}
res, err = handleSignatureAuth(r.Context(), c, token, sig, expiration)
}

res, err := c.Authenticate(r.Context(), &authenticateRequest)
switch {
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
@@ -247,3 +253,23 @@ func getTokenStatInfo(ctx context.Context, client gatewayv1beta1.GatewayAPIClien
Spec: &provider.Reference_Path{Path: path.Join("/public", token)},
}})
}

func handleBasicAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token, pw string) (*gatewayv1beta1.AuthenticateResponse, error) {
authenticateRequest := gatewayv1beta1.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: "password|" + pw,
}

return c.Authenticate(ctx, &authenticateRequest)
}

func handleSignatureAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token, sig, expiration string) (*gatewayv1beta1.AuthenticateResponse, error) {
authenticateRequest := gatewayv1beta1.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: "signature|" + sig + "|" + expiration,
}

return c.Authenticate(ctx, &authenticateRequest)
}
1 change: 1 addition & 0 deletions internal/http/services/owncloud/ocdav/ocdav.go
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ type Config struct {
GatewaySvc string `mapstructure:"gatewaysvc"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
PublicURL string `mapstructure:"public_url"`
}

func (c *Config) init() {
25 changes: 23 additions & 2 deletions internal/http/services/owncloud/ocdav/propfind.go
Original file line number Diff line number Diff line change
@@ -26,9 +26,11 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"

"go.opencensus.io/trace"

@@ -660,11 +662,30 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
} else {
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-display-name", ""))
}
case "downloadURL": // desktop
if isPublic && md.Type == provider.ResourceType_RESOURCE_TYPE_FILE {
var path string
if !ls.PasswordProtected {
path = md.Path
} else {
expiration := time.Unix(int64(ls.Signature.SignatureExpiration.Seconds), int64(ls.Signature.SignatureExpiration.Nanos))
var sb strings.Builder

sb.WriteString(md.Path)
sb.WriteString("?signature=")
sb.WriteString(ls.Signature.Signature)
sb.WriteString("&expiration=")
sb.WriteString(url.QueryEscape(expiration.Format(time.RFC3339)))

path = sb.String()
}
propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:downloadURL", s.c.PublicURL+baseURI+path))
} else {
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:"+pf.Prop[i].Local, ""))
}
case "privatelink": // phoenix only
// <oc:privatelink>https://phoenix.owncloud.com/f/9</oc:privatelink>
fallthrough
case "downloadUrl": // desktop
fallthrough
case "dDC": // desktop
fallthrough
case "data-fingerprint": // desktop
35 changes: 33 additions & 2 deletions pkg/auth/manager/publicshares/publicshares.go
Original file line number Diff line number Diff line change
@@ -20,11 +20,14 @@ package publicshares

import (
"context"
"strings"
"time"

user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
userprovider "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/auth"
"github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/errtypes"
@@ -72,9 +75,37 @@ func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user
return nil, err
}

var auth *link.PublicShareAuthentication
if strings.HasPrefix(secret, "password|") {
secret = strings.TrimPrefix(secret, "password|")
auth = &link.PublicShareAuthentication{
Spec: &link.PublicShareAuthentication_Password{
Password: secret,
},
}
} else if strings.HasPrefix(secret, "signature|") {
secret = strings.TrimPrefix(secret, "signature|")
parts := strings.Split(secret, "|")
sig, expiration := parts[0], parts[1]
exp, _ := time.Parse(time.RFC3339, expiration)

auth = &link.PublicShareAuthentication{
Spec: &link.PublicShareAuthentication_Signature{
Signature: &link.ShareSignature{
Signature: sig,
SignatureExpiration: &typesv1beta1.Timestamp{
Seconds: uint64(exp.UnixNano() / 1000000000),
Nanos: uint32(exp.UnixNano() % 1000000000),
},
},
},
}
}

publicShareResponse, err := gwConn.GetPublicShareByToken(ctx, &link.GetPublicShareByTokenRequest{
Token: token,
Password: secret,
Token: token,
Authentication: auth,
Sign: true,
})
switch {
case err != nil:
62 changes: 46 additions & 16 deletions pkg/cbox/publicshare/sql/sql.go
Original file line number Diff line number Diff line change
@@ -247,42 +247,43 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link
return nil, err
}

return m.GetPublicShare(ctx, u, req.Ref)
return m.GetPublicShare(ctx, u, req.Ref, false)
}

func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, error) {
func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) {
s := conversions.DBShare{Token: token}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?"
if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(token)
return nil, "", errtypes.NotFound(token)
}
return nil, err
return nil, "", err
}
return conversions.ConvertToCS3PublicShare(s), nil
return conversions.ConvertToCS3PublicShare(s), s.ShareWith, nil
}

func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, error) {
func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) {
uid := conversions.FormatUserID(u.Id)
s := conversions.DBShare{ID: id.OpaqueId}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions FROM oc_share WHERE share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)"
if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions); err != nil {
if err == sql.ErrNoRows {
return nil, errtypes.NotFound(id.OpaqueId)
return nil, "", errtypes.NotFound(id.OpaqueId)
}
return nil, err
return nil, "", err
}
return conversions.ConvertToCS3PublicShare(s), nil
return conversions.ConvertToCS3PublicShare(s), s.ShareWith, nil
}

func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) (*link.PublicShare, error) {
func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) {
var s *link.PublicShare
var pw string
var err error
switch {
case ref.GetId() != nil:
s, err = m.getByID(ctx, ref.GetId(), u)
s, pw, err = m.getByID(ctx, ref.GetId(), u)
case ref.GetToken() != "":
s, err = m.getByToken(ctx, ref.GetToken(), u)
s, pw, err = m.getByToken(ctx, ref.GetToken(), u)
default:
err = errtypes.NotFound(ref.String())
}
@@ -297,10 +298,14 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu
return nil, errtypes.NotFound(ref.String())
}

if s.PasswordProtected && sign {
publicshare.AddSignature(s, pw)
}

return s, nil
}

func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo) ([]*link.PublicShare, error) {
func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) {
uid := conversions.FormatUserID(u.Id)
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (uid_owner=? or uid_initiator=?) AND (share_type=?)"
var filterQuery string
@@ -348,6 +353,9 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []
if expired(cs3Share) {
_ = m.cleanupExpiredShares()
} else {
if cs3Share.PasswordProtected && sign {
publicshare.AddSignature(cs3Share, s.ShareWith)
}
shares = append(shares, cs3Share)
}
}
@@ -393,7 +401,7 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link
return nil
}

func (m *manager) GetPublicShareByToken(ctx context.Context, token, password string) (*link.PublicShare, error) {
func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) {
s := conversions.DBShare{Token: token}
query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?"
if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil {
@@ -402,13 +410,18 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token, password str
}
return nil, err
}
cs3Share := conversions.ConvertToCS3PublicShare(s)
if s.ShareWith != "" {
if check := checkPasswordHash(password, s.ShareWith); !check {
if !authenticate(cs3Share, s.ShareWith, auth) {
// if check := checkPasswordHash(auth.Password, s.ShareWith); !check {
return nil, errtypes.InvalidCredentials(token)
}

if sign {
publicshare.AddSignature(cs3Share, s.ShareWith)
}
}

cs3Share := conversions.ConvertToCS3PublicShare(s)
if expired(cs3Share) {
if err := m.cleanupExpiredShares(); err != nil {
return nil, err
@@ -455,3 +468,20 @@ func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "1|")), []byte(password))
return err == nil
}

func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool {
switch {
case auth.GetPassword() != "":
return checkPasswordHash(auth.GetPassword(), pw)
case auth.GetSignature() != nil:
sig := auth.GetSignature()
now := time.Now()
expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos()))
if now.After(expiration) {
return false
}
s := publicshare.CreateSignature(share.Token, pw, expiration)
return sig.GetSignature() == s
}
return false
}
Loading

0 comments on commit e8a00d9

Please sign in to comment.