Files
projectreshoot/internal/middleware/authentication.go
2026-01-02 19:50:10 +11:00

144 lines
3.8 KiB
Go

package middleware
import (
"context"
"database/sql"
"net/http"
"time"
"projectreshoot/internal/config"
"projectreshoot/internal/handler"
"projectreshoot/internal/models"
"projectreshoot/pkg/contexts"
"git.haelnorr.com/h/golib/cookies"
"git.haelnorr.com/h/golib/hlog"
"git.haelnorr.com/h/golib/jwt"
"github.com/pkg/errors"
)
// Attempt to use a valid refresh token to generate a new token pair
func refreshAuthTokens(
config *config.Config,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
req *http.Request,
ref *jwt.RefreshToken,
) (*models.User, error) {
user, err := models.GetUserFromID(tx, ref.SUB)
if err != nil {
return nil, errors.Wrap(err, "models.GetUser")
}
rememberMe := map[string]bool{
"session": false,
"exp": true,
}[ref.TTL]
// Set fresh to true because new tokens coming from refresh request
err = jwt.SetTokenCookies(w, req, tokenGen, user.ID, false, rememberMe, config.SSL)
if err != nil {
return nil, errors.Wrap(err, "cookies.SetTokenCookies")
}
// New tokens sent, revoke the used refresh token
err = ref.Revoke(tx)
if err != nil {
return nil, errors.Wrap(err, "ref.Revoke")
}
// Return the authorized user
return user, nil
}
// Check the cookies for token strings and attempt to authenticate them
func getAuthenticatedUser(
config *config.Config,
tokenGen *jwt.TokenGenerator,
tx *sql.Tx,
w http.ResponseWriter,
r *http.Request,
) (*contexts.AuthenticatedUser, error) {
// Get token strings from cookies
atStr, rtStr := jwt.GetTokenCookies(r)
if atStr == "" && rtStr == "" {
return nil, errors.New("No token strings provided")
}
// Attempt to parse the access token
aT, err := tokenGen.ValidateAccess(tx, atStr)
if err != nil {
// Access token invalid, attempt to parse refresh token
rT, err := tokenGen.ValidateRefresh(tx, rtStr)
if err != nil {
return nil, errors.Wrap(err, "tokenGen.ValidateRefresh")
}
// Refresh token valid, attempt to get a new token pair
user, err := refreshAuthTokens(config, tokenGen, tx, w, r, rT)
if err != nil {
return nil, errors.Wrap(err, "refreshAuthTokens")
}
// New token pair sent, return the authorized user
authUser := contexts.AuthenticatedUser{
User: user,
Fresh: time.Now().Unix(),
}
return &authUser, nil
}
// Access token valid
user, err := models.GetUserFromID(tx, aT.SUB)
if err != nil {
return nil, errors.Wrap(err, "models.GetUser")
}
authUser := contexts.AuthenticatedUser{
User: user,
Fresh: aT.Fresh,
}
return &authUser, nil
}
// Attempt to authenticate the user and add their account details
// to the request context
func Authentication(
logger *hlog.Logger,
config *config.Config,
conn *sql.DB,
tokenGen *jwt.TokenGenerator,
next http.Handler,
) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/static/css/output.css" ||
r.URL.Path == "/static/favicon.ico" {
next.ServeHTTP(w, r)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()
// Start the transaction
tx, err := conn.BeginTx(ctx, nil)
if err != nil {
// Failed to start transaction, skip auth
logger.Warn().Err(err).
Msg("Skipping Auth - unable to start a transaction")
handler.ErrorPage(http.StatusServiceUnavailable, w, r)
return
}
user, err := getAuthenticatedUser(config, tokenGen, tx, w, r)
if err != nil {
tx.Rollback()
// User auth failed, delete the cookies to avoid repeat requests
cookies.DeleteCookie(w, "access", "/")
cookies.DeleteCookie(w, "refresh", "/")
logger.Debug().
Str("remote_addr", r.RemoteAddr).
Err(err).
Msg("Failed to authenticate user")
next.ServeHTTP(w, r)
return
}
tx.Commit()
uctx := contexts.SetUser(r.Context(), user)
newReq := r.WithContext(uctx)
next.ServeHTTP(w, newReq)
})
}