1
Fork 0

Use cookie based auth for shares with password

This commit is contained in:
viktorstrate 2020-07-13 17:51:53 +02:00
parent f669812efb
commit 21f66b9e62
11 changed files with 75 additions and 83 deletions

View File

@ -2,6 +2,7 @@ package routes
import ( import (
"database/sql" "database/sql"
"fmt"
"net/http" "net/http"
"github.com/viktorstrate/photoview/api/graphql/auth" "github.com/viktorstrate/photoview/api/graphql/auth"
@ -39,7 +40,12 @@ func authenticateMedia(media *models.Media, db *sql.DB, r *http.Request) (succes
// Validate share token password, if set // Validate share token password, if set
if shareToken.Password != nil { if shareToken.Password != nil {
tokenPassword := r.Header.Get("TokenPassword") tokenPasswordCookie, err := r.Cookie(fmt.Sprintf("share-token-pw-%s", shareToken.Value))
if err != nil {
return false, "unauthorized", http.StatusForbidden, nil
}
// tokenPassword := r.Header.Get("TokenPassword")
tokenPassword := tokenPasswordCookie.Value
if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword { if err == bcrypt.ErrMismatchedHashAndPassword {

View File

@ -37,7 +37,6 @@ func RegisterVideoRoutes(db *sql.DB, router *mux.Router) {
w.Write([]byte("internal server error")) w.Write([]byte("internal server error"))
} }
// TODO: Make sure user is authorized to access video
if success, response, status, err := authenticateMedia(media, db, r); !success { if success, response, status, err := authenticateMedia(media, db, r); !success {
if err != nil { if err != nil {
log.Printf("WARN: error authenticating video: %s\n", err) log.Printf("WARN: error authenticating video: %s\n", err)

View File

@ -2,7 +2,10 @@ import React, { useContext, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import Layout from '../../Layout' import Layout from '../../Layout'
import ProtectedImage from '../../components/photoGallery/ProtectedImage' import {
ProtectedImage,
ProtectedVideo,
} from '../../components/photoGallery/ProtectedMedia'
import { SidebarContext } from '../../components/sidebar/Sidebar' import { SidebarContext } from '../../components/sidebar/Sidebar'
import MediaSidebar from '../../components/sidebar/MediaSidebar' import MediaSidebar from '../../components/sidebar/MediaSidebar'
@ -12,7 +15,7 @@ const DisplayPhoto = styled(ProtectedImage)`
object-fit: contain; object-fit: contain;
` `
const DisplayVideo = styled.video` const DisplayVideo = styled(ProtectedVideo)`
width: 100%; width: 100%;
max-height: calc(80vh); max-height: calc(80vh);
` `
@ -29,16 +32,7 @@ const MediaView = ({ media }) => {
} }
if (media.type == 'video') { if (media.type == 'video') {
return ( return <DisplayVideo media={media} />
<DisplayVideo
controls
key={media.id}
crossorigin="use-credentials"
poster={media.thumbnail.url}
>
<source src={media.videoWeb.url} type="video/mp4" />
</DisplayVideo>
)
} }
throw new Error(`Unsupported media type: ${media.type}`) throw new Error(`Unsupported media type: ${media.type}`)

View File

@ -16,6 +16,7 @@ import {
Icon, Icon,
Message, Message,
} from 'semantic-ui-react' } from 'semantic-ui-react'
import { saveSharePassword, getSharePassword } from '../../authentication'
const shareTokenQuery = gql` const shareTokenQuery = gql`
query SharePageToken($token: String!, $password: String) { query SharePageToken($token: String!, $password: String) {
@ -97,7 +98,7 @@ const AuthorizedTokenRoute = ({ match }) => {
const { loading, error, data } = useQuery(shareTokenQuery, { const { loading, error, data } = useQuery(shareTokenQuery, {
variables: { variables: {
token, token,
password: sessionStorage.getItem(`share-token-pw-${token}`), password: getSharePassword(token),
}, },
}) })
@ -185,7 +186,7 @@ const TokenRoute = ({ match }) => {
{ {
variables: { variables: {
token: match.params.token, token: match.params.token,
password: sessionStorage.getItem(`share-token-pw-${token}`), password: getSharePassword(match.params.token),
}, },
} }
) )
@ -208,8 +209,8 @@ const TokenRoute = ({ match }) => {
<ProtectedTokenEnterPassword <ProtectedTokenEnterPassword
match={match} match={match}
refetchWithPassword={password => { refetchWithPassword={password => {
sessionStorage.setItem(`share-token-pw-${token}`, password) saveSharePassword(token, password)
refetch({ variables: { password: password } }) refetch({ variables: { password } })
}} }}
loading={loading} loading={loading}
/> />

View File

@ -5,10 +5,21 @@ export function saveTokenCookie(token) {
} }
export function clearTokenCookie() { export function clearTokenCookie() {
document.cookie = 'auth-token= ;max-age=0' document.cookie = 'auth-token= ;max-age=0 ;path=/ ;sameSite=Lax'
} }
export function authToken() { export function authToken() {
const match = document.cookie.match(/auth-token=([\d\w]+)/) const match = document.cookie.match(/auth-token=([\d\w]+)/)
return match && match[1] return match && match[1]
} }
export function saveSharePassword(shareToken, password) {
document.cookie = `share-token-pw-${shareToken}=${password} ;path=/ ;sameSite=Lax`
}
export function getSharePassword(shareToken) {
const match = document.cookie.match(
`share-token-pw-${shareToken}=([\\d\\w]+)`
)
return match && match[1]
}

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import ProtectedImage from '../photoGallery/ProtectedImage' import { ProtectedImage } from '../photoGallery/ProtectedMedia'
const AlbumBoxLink = styled(Link)` const AlbumBoxLink = styled(Link)`
width: 240px; width: 240px;

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
import { useLazyQuery } from '@apollo/react-hooks' import { useLazyQuery } from '@apollo/react-hooks'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import debounce from '../../debounce' import debounce from '../../debounce'
import ProtectedImage from '../photoGallery/ProtectedImage' import { ProtectedImage } from '../photoGallery/ProtectedMedia'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
const Container = styled.div` const Container = styled.div`

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import LazyLoad from 'react-lazyload' import LazyLoad from 'react-lazyload'
import { Icon } from 'semantic-ui-react' import { Icon } from 'semantic-ui-react'
import ProtectedImage from './ProtectedImage' import { ProtectedImage } from './ProtectedMedia'
const markFavoriteMutation = gql` const markFavoriteMutation = gql`
mutation markMediaFavorite($mediaId: Int!, $favorite: Boolean!) { mutation markMediaFavorite($mediaId: Int!, $favorite: Boolean!) {

View File

@ -1,43 +1,26 @@
import React, { useEffect, useState } from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
// let imageCache = {} const getProtectedUrl = url => {
const imgUrl = new URL(url)
// export async function fetchProtectedImage( if (localStorage.getItem('token') == null) {
// src, // Get share token if not authorized
// { signal, headers: customHeaders } = { signal: null, headers: null }
// ) {
// if (src) {
// if (imageCache[src]) {
// return imageCache[src]
// }
// let headers = { const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
// ...customHeaders, if (tokenRegex) {
// } const token = tokenRegex[1]
// if (localStorage.getItem('token')) { imgUrl.searchParams.set('token', token)
// headers['Authorization'] = `Bearer ${localStorage.getItem('token')}` }
// } }
// let image = await fetch(src, { return imgUrl.href
// headers, }
// signal,
// })
// image = await image.blob()
// const url = URL.createObjectURL(image)
// // eslint-disable-next-line require-atomic-updates
// imageCache[src] = url
// return url
// }
// }
/** /**
* An image that needs a authorization header to load * An image that needs authorization to load
*/ */
const ProtectedImage = ({ src, ...props }) => { export const ProtectedImage = ({ src, ...props }) => {
// const [imgSrc, setImgSrc] = useState(null) // const [imgSrc, setImgSrc] = useState(null)
// useEffect(() => { // useEffect(() => {
@ -85,11 +68,29 @@ const ProtectedImage = ({ src, ...props }) => {
// } // }
// }, [src]) // }, [src])
return <img {...props} src={src} crossOrigin="use-credentials" /> return (
<img {...props} src={getProtectedUrl(src)} crossOrigin="use-credentials" />
)
} }
ProtectedImage.propTypes = { ProtectedImage.propTypes = {
src: PropTypes.string, src: PropTypes.string.isRequired,
} }
export default ProtectedImage export const ProtectedVideo = ({ media, ...props }) => {
return (
<video
{...props}
controls
key={media.id}
crossOrigin="use-credentials"
poster={getProtectedUrl(media.thumbnail.url)}
>
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video>
)
}
ProtectedVideo.propTypes = {
media: PropTypes.object.isRequired,
}

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import styled from 'styled-components' import styled from 'styled-components'
import ProtectedImage from '../ProtectedImage' import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'
const StyledPhoto = styled(ProtectedImage)` const StyledPhoto = styled(ProtectedImage)`
position: absolute; position: absolute;
@ -13,7 +13,7 @@ const StyledPhoto = styled(ProtectedImage)`
object-position: center; object-position: center;
` `
const StyledVideo = styled.video` const StyledVideo = styled(ProtectedVideo)`
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -39,18 +39,7 @@ const PresentMedia = ({ media, imageLoaded, ...otherProps }) => {
} }
if (media.type == 'video') { if (media.type == 'video') {
return ( return <StyledVideo media={media} />
<div {...otherProps}>
<StyledVideo
controls
key={media.id}
crossorigin="use-credentials"
poster={media.thumbnail.url}
>
<source src={media.videoWeb.url} type="video/mp4" />
</StyledVideo>
</div>
)
} }
throw new Error(`Unknown media type '${media.type}'`) throw new Error(`Unknown media type '${media.type}'`)

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
import { useLazyQuery } from 'react-apollo' import { useLazyQuery } from 'react-apollo'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
import ProtectedImage from '../photoGallery/ProtectedImage' import { ProtectedImage, ProtectedVideo } from '../photoGallery/ProtectedMedia'
import SidebarShare from './Sharing' import SidebarShare from './Sharing'
import SidebarDownload from './SidebarDownload' import SidebarDownload from './SidebarDownload'
import { authToken } from '../../authentication' import { authToken } from '../../authentication'
@ -73,7 +73,7 @@ const PreviewImage = styled(ProtectedImage)`
object-fit: contain; object-fit: contain;
` `
const PreviewVideo = styled.video` const PreviewVideo = styled(ProtectedVideo)`
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -87,16 +87,7 @@ const PreviewMedia = ({ media, previewImage }) => {
} }
if (media.type == 'video') { if (media.type == 'video') {
return ( return <PreviewVideo media={media} />
<PreviewVideo
controls
key={media.id}
crossorigin="use-credentials"
poster={media.thumbnail.url}
>
<source src={media.videoWeb.url} type="video/mp4" />
</PreviewVideo>
)
} }
throw new Error('Unknown media type') throw new Error('Unknown media type')