Files

132 lines
3.9 KiB
Go

package auth
import (
"context"
"errors"
"net/http"
"regexp"
"github.com/99designs/gqlgen/graphql/handler/transport"
"github.com/photoview/photoview/api/dataloader"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/log"
"gorm.io/gorm"
)
var ErrUnauthorized = errors.New("unauthorized")
var bearerRegex = regexp.MustCompile("^(?i)Bearer ([a-zA-Z0-9]{24})$")
const INVALID_AUTH_TOKEN = "invalid authorization token"
const INTERNAL_SERVER_ERROR = "internal server error"
// A private key for context that only this package can access. This is important
// to prevent collisions between different context uses
var userCtxKey = &contextKey{"user"}
type contextKey struct {
name string
}
// Middleware decodes the share session cookie and packs the session into context
func Middleware(db *gorm.DB) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tokenCookie, err := r.Cookie("auth-token"); err == nil {
loaders := dataloader.For(r.Context())
if loaders == nil {
log.Error(r.Context(), "Dataloader not available in HTTP context")
http.Error(w, INTERNAL_SERVER_ERROR, http.StatusInternalServerError)
return
}
user, err := loaders.UserFromAccessToken.Load(tokenCookie.Value)
// Check for dataloader errors (database failures, etc.)
if err != nil {
log.Error(r.Context(), "Error loading user from token", "error", err)
http.Error(w, INVALID_AUTH_TOKEN, http.StatusUnauthorized)
return
}
// If user is nil, the token doesn't exist or is invalid
if user == nil {
log.Error(r.Context(), "Token not found in database")
http.Error(w, INVALID_AUTH_TOKEN, http.StatusUnauthorized)
return
}
// put it in context
ctx := AddUserToContext(r.Context(), user)
// and call the next with our new context
r = r.WithContext(ctx)
} else {
log.Info(r.Context(), "Did not find auth-token cookie")
}
next.ServeHTTP(w, r)
})
}
}
func AddUserToContext(ctx context.Context, user *models.User) context.Context {
return context.WithValue(ctx, userCtxKey, user)
}
func TokenFromBearer(bearer *string) (*string, error) {
matches := bearerRegex.FindStringSubmatch(*bearer)
if len(matches) != 2 {
return nil, errors.New("invalid bearer format")
}
token := matches[1]
return &token, nil
}
// UserFromContext finds the user from the context. REQUIRES Middleware to have run.
func UserFromContext(ctx context.Context) *models.User {
raw, _ := ctx.Value(userCtxKey).(*models.User)
return raw
}
func AuthWebsocketInit() func(context.Context, transport.InitPayload) (context.Context, *transport.InitPayload, error) {
return func(ctx context.Context, initPayload transport.InitPayload) (context.Context, *transport.InitPayload, error) {
bearer, exists := initPayload["Authorization"].(string)
if !exists {
return ctx, nil, nil
}
token, err := TokenFromBearer(&bearer)
if err != nil {
log.Error(ctx, "Invalid bearer format (websocket)", "error", err)
return nil, nil, err
}
loaders := dataloader.For(ctx)
if loaders == nil {
log.Error(ctx, "Dataloader not available in websocket context")
return nil, nil, errors.New(INTERNAL_SERVER_ERROR)
}
user, err := loaders.UserFromAccessToken.Load(*token)
if err != nil {
log.Error(ctx, "Error loading user from token (websocket)", "error", err)
return nil, nil, errors.New(INVALID_AUTH_TOKEN)
}
// Check if token exists in database
if user == nil {
log.Error(ctx, "Token not found in database (websocket)")
return nil, nil, errors.New(INVALID_AUTH_TOKEN)
}
// put it in context
userCtx := context.WithValue(ctx, userCtxKey, user)
// and return it so the resolvers can see it
// Return nil for the InitPayload acknowledgment (no custom ack payload needed)
return userCtx, nil, nil
}
}