Skip to content

Commit

Permalink
signature authentication for public links
Browse files Browse the repository at this point in the history
Implemented the mechanism proposed here: cs3org/cs3apis#110.
The signature authentication is limited to downloads.
  • Loading branch information
David Christofas committed Mar 26, 2021
1 parent fb410ea commit 775cfc0
Show file tree
Hide file tree
Showing 13 changed files with 263 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
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
16 changes: 13 additions & 3 deletions internal/grpc/services/publicshareprovider/publicshareprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package publicshareprovider
import (
"context"
"fmt"
"time"

link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
Expand Down Expand Up @@ -148,8 +149,17 @@ func (s *service) GetPublicShareByToken(ctx context.Context, req *link.GetPublic
log := appctx.GetLogger(ctx)
log.Debug().Msg("getting public share by token")

sig := req.GetAuthentication().GetSignature()
auth := publicshare.Authentication{
Password: req.GetAuthentication().GetPassword(),
Signature: publicshare.Signature{
Value: sig.GetSignature(),
Expiration: time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos())),
},
}

// 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(), auth, req.GetSign())
switch v := err.(type) {
case nil:
return &link.GetPublicShareByTokenResponse{
Expand Down Expand Up @@ -180,7 +190,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 +206,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 {
// We restrict the pre-signed urls to downloads.
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusUnauthorized)
return
}
q := r.URL.Query()
sig := q.Get("signature")
expiration := q.Get("expiration")
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
60 changes: 44 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 {
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 publicshare.Authentication, 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,18 @@ 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 publicshare.Authentication) bool {
switch {
case auth.Password != "":
return checkPasswordHash(auth.Password, pw)
case auth.Signature != publicshare.Signature{}:
now := time.Now()
if now.After(auth.Signature.Expiration) {
return false
}
sig := publicshare.CreateSignature(share.Token, pw, auth.Signature.Expiration)
return auth.Signature.Value == sig
}
return false
}
Loading

0 comments on commit 775cfc0

Please sign in to comment.