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

signature authentication for public links #1590

Merged
merged 1 commit into from
Mar 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
Expand All @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -679,6 +682,7 @@ func (s *service) resolveToken(ctx context.Context, token string) (string, *link
Token: token,
},
},
Sign: true,
},
)
switch {
Expand All @@ -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
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Up @@ -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() {
Expand Down
25 changes: 23 additions & 2 deletions internal/http/services/owncloud/ocdav/propfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"

"go.opencensus.io/trace"

Expand Down Expand Up @@ -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
Expand Down
35 changes: 33 additions & 2 deletions pkg/auth/manager/publicshares/publicshares.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
62 changes: 46 additions & 16 deletions pkg/cbox/publicshare/sql/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand All @@ -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
Expand Down Expand Up @@ -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 {
ishank011 marked this conversation as resolved.
Show resolved Hide resolved
publicshare.AddSignature(cs3Share, s.ShareWith)
}
shares = append(shares, cs3Share)
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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