Compare commits
7 Commits
6927ebf0d3
...
cb91a10192
Author | SHA1 | Date | |
---|---|---|---|
cb91a10192
|
|||
1a2130992b
|
|||
9fad610a70
|
|||
1e7ac51ca0
|
|||
8e181ccc07
|
|||
f63b0e9731
|
|||
ca0d7930b1
|
@ -6,6 +6,9 @@ ADMIN_NAME="admin"
|
||||
ADMIN_EMAIL="admin@test.net"
|
||||
ADMIN_PASSWORD="secret"
|
||||
|
||||
JWT_PRIVATE_KEY="ecdsa"
|
||||
JWT_PUBLIC_KEY="ecdsa"
|
||||
|
||||
GOOSE_DRIVER="postgres"
|
||||
GOOSE_DBSTRING=$DATABASE_URL
|
||||
GOOSE_MIGRATION_DIR="./migrations"
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -25,3 +25,6 @@ go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
# key files
|
||||
*.pem
|
||||
|
89
internal/auth/jwt.go
Normal file
89
internal/auth/jwt.go
Normal 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
21
internal/auth/routes.go
Normal 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) {
|
||||
|
||||
}
|
||||
|
@ -3,11 +3,15 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gitea.local/admin/hspguard/internal/auth"
|
||||
"gitea.local/admin/hspguard/internal/repository"
|
||||
"gitea.local/admin/hspguard/internal/web"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
@ -24,6 +28,7 @@ func (h *UserHandler) RegisterRoutes(router chi.Router, api chi.Router) {
|
||||
router.Get("/login", h.loginPage)
|
||||
router.Get("/register", h.registerPage)
|
||||
api.Post("/register", h.register)
|
||||
api.Post("/login", h.login)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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(¶ms); 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
26
scripts/generate-jwt-keys.sh
Executable 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
|
Reference in New Issue
Block a user