Compare commits

..

7 Commits

Author SHA1 Message Date
cb91a10192 feat: auth handler init 2025-05-19 10:13:25 +02:00
1a2130992b feat: ignore .pem files 2025-05-19 09:19:28 +02:00
9fad610a70 fix: keys generation 2025-05-19 09:19:24 +02:00
1e7ac51ca0 feat: login post handler 2025-05-19 09:19:18 +02:00
8e181ccc07 feat: sign and verify token func 2025-05-19 09:19:10 +02:00
f63b0e9731 feat: env jwt variables 2025-05-19 08:19:10 +02:00
ca0d7930b1 feat: generate key pair for jwyt 2025-05-19 08:19:05 +02:00
6 changed files with 207 additions and 0 deletions

View File

@ -6,6 +6,9 @@ ADMIN_NAME="admin"
ADMIN_EMAIL="admin@test.net" ADMIN_EMAIL="admin@test.net"
ADMIN_PASSWORD="secret" ADMIN_PASSWORD="secret"
JWT_PRIVATE_KEY="ecdsa"
JWT_PUBLIC_KEY="ecdsa"
GOOSE_DRIVER="postgres" GOOSE_DRIVER="postgres"
GOOSE_DBSTRING=$DATABASE_URL GOOSE_DBSTRING=$DATABASE_URL
GOOSE_MIGRATION_DIR="./migrations" GOOSE_MIGRATION_DIR="./migrations"

3
.gitignore vendored
View File

@ -25,3 +25,6 @@ go.work.sum
# env file # env file
.env .env
# key files
*.pem

89
internal/auth/jwt.go Normal file
View File

@ -0,0 +1,89 @@
package auth
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
"fmt"
"os"
"github.com/golang-jwt/jwt/v5"
)
func parseBase64PrivateKey(envVar string) (*ecdsa.PrivateKey, error) {
b64 := os.Getenv(envVar)
if b64 == "" {
return nil, fmt.Errorf("env var %s is empty", envVar)
}
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 key: %v", err)
}
return x509.ParseECPrivateKey(decoded)
}
func parseBase64PublicKey(envVar string) (*ecdsa.PublicKey, error) {
b64 := os.Getenv(envVar)
if b64 == "" {
return nil, fmt.Errorf("env var %s is empty", envVar)
}
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 key: %v", err)
}
pubInterface, err := x509.ParsePKIXPublicKey(decoded)
if err != nil {
return nil, fmt.Errorf("failed to parse public key: %v", err)
}
pubKey, ok := pubInterface.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an ECDSA public key")
}
return pubKey, nil
}
func SignJwtToken(claims jwt.Claims) (string, error) {
privateKey, err := parseBase64PrivateKey("JWT_PRIVATE_KEY")
if err != nil {
return "", err
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
s, err := token.SignedString(privateKey)
if err != nil {
return "", err
}
return s, nil
}
func VerifyToken(token string, claims jwt.Claims) (*jwt.Token, error) {
publicKey, err := parseBase64PublicKey("JWT_PUBLIC_KEY")
if err != nil {
return nil, err
}
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
if _, ok := t.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return publicKey, nil
})
if err != nil {
return nil, fmt.Errorf("invalid token: %w", err)
}
if !parsed.Valid {
return nil, fmt.Errorf("token is not valid")
}
return parsed, nil
}

21
internal/auth/routes.go Normal file
View File

@ -0,0 +1,21 @@
package auth
import (
"gitea.local/admin/hspguard/internal/repository"
"github.com/go-chi/chi/v5"
)
type AuthHandler struct {
repo *repository.Queries
}
func NewAuthHandler(repo *repository.Queries) *AuthHandler {
return &AuthHandler{
repo: repo,
}
}
func (h *AuthHandler) RegisterRoutes(router chi.Router, api chi.Router) {
}

View File

@ -3,11 +3,15 @@ package user
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"time"
"gitea.local/admin/hspguard/internal/auth"
"gitea.local/admin/hspguard/internal/repository" "gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/web" "gitea.local/admin/hspguard/internal/web"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
) )
type UserHandler struct { type UserHandler struct {
@ -24,6 +28,7 @@ func (h *UserHandler) RegisterRoutes(router chi.Router, api chi.Router) {
router.Get("/login", h.loginPage) router.Get("/login", h.loginPage)
router.Get("/register", h.registerPage) router.Get("/register", h.registerPage)
api.Post("/register", h.register) api.Post("/register", h.register)
api.Post("/login", h.login)
} }
func (h *UserHandler) loginPage(w http.ResponseWriter, r *http.Request) { func (h *UserHandler) loginPage(w http.ResponseWriter, r *http.Request) {
@ -92,3 +97,63 @@ func (h *UserHandler) register(w http.ResponseWriter, r *http.Request) {
web.Error(w, "failed to encode response", http.StatusInternalServerError) web.Error(w, "failed to encode response", http.StatusInternalServerError)
} }
} }
type LoginParams struct {
Email string `json:"email"`
Password string `json:"password"`
}
type UserClaims struct {
UserID string `json:"user_id"`
// Role
jwt.RegisteredClaims
}
func (h *UserHandler) login(w http.ResponseWriter, r *http.Request) {
var params LoginParams
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&params); err != nil {
web.Error(w, "failed to parse request body", http.StatusBadRequest)
return
}
if params.Email == "" || params.Password == "" {
web.Error(w, "missing required fields", http.StatusBadRequest)
return
}
user, err := h.repo.FindUserEmail(context.Background(), params.Email)
if err != nil {
web.Error(w, "user with provided email does not exists", http.StatusBadRequest)
return
}
claims := UserClaims{
UserID: user.ID.String(),
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "hspguard",
Subject: user.Email,
IssuedAt: jwt.NewNumericDate(time.Now()),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
}
token, err := auth.SignJwtToken(claims)
if err != nil {
web.Error(w, fmt.Sprintf("failed to generate access token: %v", err), http.StatusBadRequest)
return
}
encoder := json.NewEncoder(w)
type Response struct {
Token string `json:"token"`
}
if err := encoder.Encode(Response{
Token: token,
}); err != nil {
web.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}

26
scripts/generate-jwt-keys.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
# Generate private key
# openssl ecparam -genkey -name prime256v1 -noout -out ec256-private.pem
# openssl ec -in ec256-private.pem -outform DER | base64 -w 0
# Extract public key
# openssl ec -in ec256-private.pem -pubout -out ec256-public.pem
# openssl ec -in ec256-private.pem -pubout -outform DER | base64 -w 0
# Generate private key
openssl ecparam -genkey -name prime256v1 -noout -out ec256-private.pem
# Extract public key
openssl ec -in ec256-private.pem -pubout -out ec256-public.pem
echo ""
echo "Private Key (DER base64):"
openssl ec -in ec256-private.pem -outform DER | base64 -w 0
echo "
--------------------------------"
echo ""
echo "Public Key (DER base64):"
openssl ec -in ec256-private.pem -pubout -outform DER | base64 -w 0