implement JWT tokens
This commit is contained in:
139
api/auth.go
139
api/auth.go
@@ -2,14 +2,26 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
|
||||
|
||||
func hashToken(token string) string {
|
||||
hash := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func Login(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseMultipartForm(64 << 10)
|
||||
if err != nil {
|
||||
@@ -36,7 +48,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sessionToken := CreateSession(username)
|
||||
sessionToken := CreateSession(user.ID)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "session_token",
|
||||
@@ -46,7 +58,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
|
||||
Secure: false,
|
||||
})
|
||||
|
||||
slog.Info("auth: login successful", "user", user.Name)
|
||||
slog.Info("auth: login successful", "userID", user.ID, "userName", user.Name)
|
||||
w.Write([]byte("Login successful"))
|
||||
}
|
||||
|
||||
@@ -58,71 +70,126 @@ func Logout(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
sessionToken := cookie.Value
|
||||
username, valid := ValidateSession(sessionToken)
|
||||
userID, valid := ValidateSession(sessionToken)
|
||||
if !valid {
|
||||
http.Error(w, "Session cookie could not be validated. You are already logged out", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dbGetUser(userID.String())
|
||||
if err != nil {
|
||||
http.Error(w, "Session cookie validated but user could not be found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
DeleteSession(sessionToken)
|
||||
|
||||
cookie.Expires = time.Now()
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
slog.Debug("auth: logout successful", "user", username)
|
||||
w.Write([]byte(username + " has been logged out"))
|
||||
slog.Debug("auth: logout successful", "userID", user.ID, "userName", user.Name)
|
||||
w.Write([]byte(fmt.Sprintf("%v has been logged out", user.Name)))
|
||||
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token uuid.UUID
|
||||
Username string
|
||||
Token string
|
||||
UserID uuid.UUID
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
func CreateSession(username string) string {
|
||||
func CreateSession(userID uuid.UUID) string {
|
||||
|
||||
expiry := time.Now().Add(7 * 24 * time.Hour)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"userID": userID.String(),
|
||||
"exp": expiry.Unix(), // 7 day token expiry
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(jwtSecret)
|
||||
if err != nil {
|
||||
slog.Error("auth: failed to create JWT", "error", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
hashedToken := hashToken(tokenString)
|
||||
session := Session{
|
||||
Token: uuid.New(),
|
||||
Username: username,
|
||||
Token: hashedToken,
|
||||
UserID: userID,
|
||||
Expiry: expiry,
|
||||
}
|
||||
dbAddSession(&session)
|
||||
slog.Debug("auth: new session created", "user", session.Username)
|
||||
return session.Token.String()
|
||||
|
||||
slog.Debug("auth: new session created", "userID", session.UserID)
|
||||
return tokenString
|
||||
}
|
||||
|
||||
func ValidateSession(sessionToken string) (string, bool) {
|
||||
tokenUUID, err := uuid.Parse(sessionToken)
|
||||
if err != nil {
|
||||
return "", false
|
||||
func ValidateSession(sessionToken string) (uuid.UUID, bool) {
|
||||
token, err := jwt.Parse(sessionToken, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
slog.Debug("auth: session token invalid, rejecting")
|
||||
return uuid.Nil, false
|
||||
}
|
||||
|
||||
session, err := dbGetSession(tokenUUID)
|
||||
if err != nil {
|
||||
return "", false
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
slog.Debug("auth: could not map claims from JWT")
|
||||
return uuid.Nil, false
|
||||
}
|
||||
slog.Debug("auth: session validated", "user", session.Username)
|
||||
return session.Username, true
|
||||
|
||||
userIDStr, ok := claims["userID"].(string)
|
||||
if !ok {
|
||||
slog.Debug("auth: userID claim is not a string")
|
||||
return uuid.Nil, false
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
slog.Debug("auth: failed to parse userID as uuid", "error", err)
|
||||
return uuid.Nil, false
|
||||
}
|
||||
|
||||
hashedToken := hashToken(sessionToken)
|
||||
|
||||
session, err := dbGetSession(hashedToken)
|
||||
if err != nil {
|
||||
slog.Debug("auth: failed to retrieve session from db", "error", err)
|
||||
return uuid.Nil, false
|
||||
}
|
||||
if time.Now().After(session.Expiry) {
|
||||
slog.Debug("auth: session is expired (or otherwise invalid) in db")
|
||||
return uuid.Nil, false
|
||||
}
|
||||
|
||||
slog.Debug("auth: session validated", "userID", session.UserID)
|
||||
return userID, true
|
||||
}
|
||||
|
||||
func DeleteSession(sessionToken string) (string, bool) {
|
||||
tokenUUID, err := uuid.Parse(sessionToken)
|
||||
func DeleteSession(sessionToken string) bool {
|
||||
|
||||
hashedToken := hashToken(sessionToken)
|
||||
|
||||
err := dbDeleteSession(hashedToken)
|
||||
if err != nil {
|
||||
return "", false
|
||||
slog.Error("auth: failed to delete session", "error", err)
|
||||
return false
|
||||
}
|
||||
|
||||
session, err := dbGetSession(tokenUUID)
|
||||
if err != nil {
|
||||
return "", false
|
||||
} else {
|
||||
dbDeleteSession(session.Token)
|
||||
}
|
||||
|
||||
slog.Debug("auth: session deleted", "user", session.Username)
|
||||
return session.Username, true
|
||||
slog.Debug("auth: session deleted", "token", hashedToken)
|
||||
return true
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
const usernameKey contextKey = "username"
|
||||
const userIDKey contextKey = "userID"
|
||||
|
||||
func SessionAuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -133,14 +200,14 @@ func SessionAuthMiddleware(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
sessionToken := cookie.Value
|
||||
username, valid := ValidateSession(sessionToken)
|
||||
userID, valid := ValidateSession(sessionToken)
|
||||
if !valid {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Add username to request context
|
||||
ctx := context.WithValue(r.Context(), usernameKey, username)
|
||||
ctx := context.WithValue(r.Context(), userIDKey, userID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user