From f6f6eb8bfad1833c89655885372062d9620e9107 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Sun, 14 Jun 2020 17:58:50 +0200 Subject: [PATCH] Prepare back-end for token password --- api/graphql/resolvers/share_token.go | 17 ++++++++--- api/routes/photos.go | 20 ++++++++++++- .../components/photoGallery/ProtectedImage.js | 28 +++++++++++++++---- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go index b20c8ab2..e06e3566 100644 --- a/api/graphql/resolvers/share_token.go +++ b/api/graphql/resolvers/share_token.go @@ -60,10 +60,15 @@ func (r *shareTokenResolver) HasPassword(ctx context.Context, obj *models.ShareT return hasPassword, nil } -func (r *queryResolver) ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) { +func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, password *string) (*models.ShareToken, error) { - row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ? AND (password = ? OR password IS NULL)", token, password) - result, err := models.NewShareTokenFromRow(row) + hashed_password, err := hashSharePassword(password) + if err != nil { + return nil, errors.Wrap(err, "Failed to hash password") + } + + row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue) + token, err := models.NewShareTokenFromRow(row) if err != nil { if err == sql.ErrNoRows { return nil, errors.New("share not found") @@ -72,7 +77,11 @@ func (r *queryResolver) ShareToken(ctx context.Context, token string, password * } } - return result, nil + if token.Password != nil && hashed_password != token.Password { + return nil, errors.New("unauthorized") + } + + return token, nil } func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) { diff --git a/api/routes/photos.go b/api/routes/photos.go index 33e1e267..1e6bd02a 100644 --- a/api/routes/photos.go +++ b/api/routes/photos.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" "github.com/viktorstrate/photoview/api/graphql/auth" "github.com/viktorstrate/photoview/api/graphql/models" @@ -60,7 +61,7 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) { return } } else { - + // Check if photo is authorized with a share token token := r.URL.Query().Get("token") if token == "" { w.WriteHeader(http.StatusForbidden) @@ -78,6 +79,23 @@ func RegisterPhotoRoutes(db *sql.DB, router *mux.Router) { return } + // Validate share token password, if set + if shareToken.Password != nil { + tokenPassword := r.Header.Get("TokenPassword") + + if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("unauthorized")) + return + } else { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("internal server error")) + return + } + } + } + if shareToken.AlbumID != nil && photo.AlbumId != *shareToken.AlbumID { // Check child albums row := db.QueryRow(` diff --git a/ui/src/components/photoGallery/ProtectedImage.js b/ui/src/components/photoGallery/ProtectedImage.js index 2111a8e5..bf2b6e93 100644 --- a/ui/src/components/photoGallery/ProtectedImage.js +++ b/ui/src/components/photoGallery/ProtectedImage.js @@ -3,13 +3,18 @@ import PropTypes from 'prop-types' let imageCache = {} -export async function fetchProtectedImage(src, { signal } = { signal: null }) { +export async function fetchProtectedImage( + src, + { signal, headers: customHeaders } = { signal: null, headers: null } +) { if (src) { if (imageCache[src]) { return imageCache[src] } - let headers = {} + let headers = { + ...customHeaders, + } if (localStorage.getItem('token')) { headers['Authorization'] = `Bearer ${localStorage.getItem('token')}` } @@ -44,16 +49,27 @@ const ProtectedImage = ({ src, ...props }) => { setImgSrc('') const imgUrl = new URL(src) + const fetchHeaders = {} + if (localStorage.getItem('token') == null) { // Get share token if not authorized - const token = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/) - if (token) { - imgUrl.searchParams.set('token', token[1]) + const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/) + if (tokenRegex) { + const token = tokenRegex[1] + imgUrl.searchParams.set('token', token) + + const tokenPassword = sessionStorage.getItem(`share-token-pw-${token}`) + if (tokenPassword) { + fetchHeaders['TokenPassword'] = tokenPassword + } } } - fetchProtectedImage(imgUrl.href, { signal: fetchController.signal }) + fetchProtectedImage(imgUrl.href, { + signal: fetchController.signal, + headers: fetchHeaders, + }) .then(newSrc => { if (!canceled) { setImgSrc(newSrc)