autobrr/internal/http/auth.go

172 lines
4.5 KiB
Go

package http
import (
"context"
"encoding/json"
"net/http"
"github.com/autobrr/autobrr/internal/domain"
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
"github.com/rs/zerolog"
)
type authService interface {
GetUserCount(ctx context.Context) (int, error)
Login(ctx context.Context, username, password string) (*domain.User, error)
CreateUser(ctx context.Context, username, password string) error
}
type authHandler struct {
log zerolog.Logger
encoder encoder
config *domain.Config
service authService
cookieStore *sessions.CookieStore
}
func newAuthHandler(encoder encoder, log zerolog.Logger, config *domain.Config, cookieStore *sessions.CookieStore, service authService) *authHandler {
return &authHandler{
log: log,
encoder: encoder,
config: config,
service: service,
cookieStore: cookieStore,
}
}
func (h authHandler) Routes(r chi.Router) {
r.Post("/login", h.login)
r.Post("/logout", h.logout)
r.Post("/onboard", h.onboard)
r.Get("/onboard", h.canOnboard)
r.Get("/validate", h.validate)
}
func (h authHandler) login(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
data domain.User
)
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error
h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest)
return
}
h.cookieStore.Options.HttpOnly = true
h.cookieStore.Options.SameSite = http.SameSiteLaxMode
h.cookieStore.Options.Path = h.config.BaseURL
// autobrr does not support serving on TLS / https, so this is only available behind reverse proxy
// if forwarded protocol is https then set cookie secure
// SameSite Strict can only be set with a secure cookie. So we overwrite it here if possible.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
fwdProto := r.Header.Get("X-Forwarded-Proto")
if fwdProto == "https" {
h.cookieStore.Options.Secure = true
h.cookieStore.Options.SameSite = http.SameSiteStrictMode
}
session, _ := h.cookieStore.Get(r, "user_session")
_, err := h.service.Login(ctx, data.Username, data.Password)
if err != nil {
h.log.Error().Err(err).Msgf("invalid login [%s] from: %s", ReadUserIP(r))
h.encoder.StatusResponse(ctx, w, nil, http.StatusUnauthorized)
return
}
// Set user as authenticated
session.Values["authenticated"] = true
session.Save(r, w)
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
}
func (h authHandler) logout(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := h.cookieStore.Get(r, "user_session")
// Revoke users authentication
session.Values["authenticated"] = false
session.Save(r, w)
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
}
func (h authHandler) onboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := h.cookieStore.Get(r, "user_session")
// Don't proceed if user is authenticated
if _, ok := session.Values["authenticated"].(bool); ok {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
var data domain.User
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
// encode error
h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest)
return
}
err := h.service.CreateUser(ctx, data.Username, data.Password)
if err != nil {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// send empty response as ok
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
}
func (h authHandler) canOnboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userCount, err := h.service.GetUserCount(ctx)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if userCount > 0 {
// send 503 service onboarding unavailable
http.Error(w, "Onboarding unavailable", http.StatusForbidden)
return
}
// send empty response as ok
// (client can proceed with redirection to onboarding page)
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
}
func (h authHandler) validate(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := h.cookieStore.Get(r, "user_session")
// Check if user is authenticated
if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
http.Error(w, "Forbidden", http.StatusUnauthorized)
return
}
// send empty response as ok
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
}
func ReadUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
}
return IPAddress
}