feat: refactor for using app config

This commit is contained in:
2025-05-25 16:22:28 +02:00
parent b95dcc6230
commit 07b9b94143
10 changed files with 120 additions and 153 deletions

View File

@ -4,71 +4,13 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/web"
"github.com/golang-jwt/jwt/v5"
)
func WriteJWKS(w http.ResponseWriter, r *http.Request) {
pubKey, err := parseBase64PublicKey("JWT_PUBLIC_KEY")
if err != nil {
web.Error(w, "failed to parse public key", http.StatusInternalServerError)
}
n := base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes())
e := base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}) // 65537 = 0x010001
jwks := map[string]interface{}{
"keys": []map[string]string{
{
"kty": "RSA",
"kid": "my-rsa-key-1",
"use": "sig",
"alg": "RS256",
"n": n,
"e": e,
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jwks)
}
func OpenIdConfiguration(w http.ResponseWriter, r *http.Request) {
type Response struct {
TokenEndpoint string `json:"token_endpoint"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
JwksURI string `json:"jwks_uri"`
Issuer string `json:"issuer"`
EndSessionEndpoint string `json:"end_session_endpoint"`
}
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
if err := encoder.Encode(Response{
TokenEndpoint: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/api/v1/oauth/token",
AuthorizationEndpoint: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/authorize",
JwksURI: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/.well-known/jwks.json",
Issuer: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app",
EndSessionEndpoint: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/api/v1/oauth/logout",
}); err != nil {
web.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}
func parseBase64PrivateKey(envVar string) (*rsa.PrivateKey, error) {
b64 := os.Getenv(envVar)
if b64 == "" {
return nil, fmt.Errorf("env var %s is empty", envVar)
}
func ParseBase64PrivateKey(b64 string) (*rsa.PrivateKey, error) {
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 key: %v", err)
@ -78,12 +20,7 @@ func parseBase64PrivateKey(envVar string) (*rsa.PrivateKey, error) {
return key.(*rsa.PrivateKey), err
}
func parseBase64PublicKey(envVar string) (*rsa.PublicKey, error) {
b64 := os.Getenv(envVar)
if b64 == "" {
return nil, fmt.Errorf("env var %s is empty", envVar)
}
func ParseBase64PublicKey(b64 string) (*rsa.PublicKey, error) {
decoded, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return nil, fmt.Errorf("failed to decode base64 key: %v", err)
@ -102,8 +39,8 @@ func parseBase64PublicKey(envVar string) (*rsa.PublicKey, error) {
return pubKey, nil
}
func SignJwtToken(claims jwt.Claims) (string, error) {
privateKey, err := parseBase64PrivateKey("JWT_PRIVATE_KEY")
func SignJwtToken(claims jwt.Claims, key string) (string, error) {
privateKey, err := ParseBase64PrivateKey(key)
if err != nil {
return "", err
}
@ -120,8 +57,8 @@ func SignJwtToken(claims jwt.Claims) (string, error) {
return s, nil
}
func VerifyToken(token string) (*jwt.Token, *types.UserClaims, error) {
publicKey, err := parseBase64PublicKey("JWT_PUBLIC_KEY")
func VerifyToken(token string, key string) (*jwt.Token, *types.UserClaims, error) {
publicKey, err := ParseBase64PublicKey(key)
if err != nil {
return nil, nil, err
}

View File

@ -6,6 +6,7 @@ import (
"net/http"
"time"
"gitea.local/admin/hspguard/internal/config"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/util"
@ -17,6 +18,7 @@ import (
type AuthHandler struct {
repo *repository.Queries
cfg *config.AppConfig
}
func NewAuthHandler(repo *repository.Queries) *AuthHandler {
@ -89,6 +91,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
accessClaims := types.UserClaims{
UserEmail: user.Email,
IsAdmin: user.IsAdmin,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "hspguard",
Subject: user.ID.String(),
@ -97,7 +100,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
},
}
accessToken, err := SignJwtToken(accessClaims)
accessToken, err := SignJwtToken(accessClaims, h.cfg.Jwt.PrivateKey)
if err != nil {
web.Error(w, fmt.Sprintf("failed to generate access token: %v", err), http.StatusBadRequest)
return
@ -105,6 +108,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
refreshClaims := types.UserClaims{
UserEmail: user.Email,
IsAdmin: user.IsAdmin,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "hspguard",
Subject: user.ID.String(),
@ -113,7 +117,7 @@ func (h *AuthHandler) login(w http.ResponseWriter, r *http.Request) {
},
}
refreshToken, err := SignJwtToken(refreshClaims)
refreshToken, err := SignJwtToken(refreshClaims, h.cfg.Jwt.PrivateKey)
if err != nil {
web.Error(w, fmt.Sprintf("failed to generate refresh token: %v", err), http.StatusBadRequest)
return

View File

@ -7,34 +7,37 @@ import (
"strings"
"gitea.local/admin/hspguard/internal/auth"
"gitea.local/admin/hspguard/internal/config"
"gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/web"
)
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
web.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
func AuthMiddleware(cfg *config.AppConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
web.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
parts := strings.Split(authHeader, "Bearer ")
if len(parts) != 2 {
web.Error(w, "invalid auth header format", http.StatusUnauthorized)
return
}
parts := strings.Split(authHeader, "Bearer ")
if len(parts) != 2 {
web.Error(w, "invalid auth header format", http.StatusUnauthorized)
return
}
tokenStr := parts[1]
token, userClaims, err := auth.VerifyToken(tokenStr)
if err != nil || !token.Valid {
http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
return
}
tokenStr := parts[1]
token, userClaims, err := auth.VerifyToken(tokenStr, cfg.Jwt.PublicKey)
if err != nil || !token.Valid {
http.Error(w, fmt.Sprintf("invalid token: %v", err), http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), types.UserIdKey, userClaims.Subject)
next.ServeHTTP(w, r.WithContext(ctx))
})
ctx := context.WithValue(r.Context(), types.UserIdKey, userClaims.Subject)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func WithSkipper(mw func(http.Handler) http.Handler, excludedPaths ...string) func(http.Handler) http.Handler {

View File

@ -10,6 +10,7 @@ import (
"time"
"gitea.local/admin/hspguard/internal/auth"
"gitea.local/admin/hspguard/internal/config"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/types"
"gitea.local/admin/hspguard/internal/util"
@ -21,11 +22,13 @@ import (
type OAuthHandler struct {
repo *repository.Queries
cfg *config.AppConfig
}
func NewOAuthHandler(repo *repository.Queries) *OAuthHandler {
func NewOAuthHandler(repo *repository.Queries, cfg *config.AppConfig) *OAuthHandler {
return &OAuthHandler{
repo,
cfg,
}
}
@ -35,6 +38,55 @@ func (h *OAuthHandler) RegisterRoutes(r chi.Router) {
r.Post("/oauth/code", h.getAuthCode)
}
func (h *OAuthHandler) WriteJWKS(w http.ResponseWriter, r *http.Request) {
pubKey, err := auth.ParseBase64PublicKey(h.cfg.Jwt.PublicKey)
if err != nil {
web.Error(w, "failed to parse public key", http.StatusInternalServerError)
}
n := base64.RawURLEncoding.EncodeToString(pubKey.N.Bytes())
e := base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}) // 65537 = 0x010001
jwks := map[string]interface{}{
"keys": []map[string]string{
{
"kty": "RSA",
"kid": "my-rsa-key-1",
"use": "sig",
"alg": "RS256",
"n": n,
"e": e,
},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jwks)
}
func OpenIdConfiguration(w http.ResponseWriter, r *http.Request) {
type Response struct {
TokenEndpoint string `json:"token_endpoint"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
JwksURI string `json:"jwks_uri"`
Issuer string `json:"issuer"`
EndSessionEndpoint string `json:"end_session_endpoint"`
}
w.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(w)
if err := encoder.Encode(Response{
TokenEndpoint: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/api/v1/oauth/token",
AuthorizationEndpoint: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/authorize",
JwksURI: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/.well-known/jwks.json",
Issuer: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app",
EndSessionEndpoint: "https://cb5f-2a00-10-5b00-c801-e955-5c68-63d0-b777.ngrok-free.app/api/v1/oauth/logout",
}); err != nil {
web.Error(w, "failed to encode response", http.StatusInternalServerError)
}
}
func (h *OAuthHandler) getAuthCode(w http.ResponseWriter, r *http.Request) {
userId, ok := util.GetRequestUserId(r.Context())
if !ok {
@ -155,7 +207,7 @@ func (h *OAuthHandler) tokenEndpoint(w http.ResponseWriter, r *http.Request) {
},
}
idToken, err := auth.SignJwtToken(claims)
idToken, err := auth.SignJwtToken(claims, h.cfg.Jwt.PrivateKey)
if err != nil {
web.Error(w, "failed to sign id token", http.StatusInternalServerError)
return

View File

@ -5,8 +5,8 @@ import (
"io"
"log"
"net/url"
"os"
"gitea.local/admin/hspguard/internal/config"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
@ -15,27 +15,9 @@ type FileStorage struct {
client *minio.Client
}
func New() *FileStorage {
endpoint := os.Getenv("MINIO_ENDPOINT")
if endpoint == "" {
log.Fatalln("MINIO_ENDPOINT env var is required")
return nil
}
accessKey := os.Getenv("MINIO_ACCESS_KEY")
if accessKey == "" {
log.Fatalln("MINIO_ACCESS_KEY env var is required")
return nil
}
secretKey := os.Getenv("MINIO_SECRET_KEY")
if secretKey == "" {
log.Fatalln("MINIO_SECRET_KEY env var is required")
return nil
}
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
func New(cfg *config.AppConfig) *FileStorage {
client, err := minio.New(cfg.Minio.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.Minio.AccessKey, cfg.Minio.SecretKey, ""),
Secure: false,
})
if err != nil {

View File

@ -4,6 +4,7 @@ import "github.com/golang-jwt/jwt/v5"
type UserClaims struct {
UserEmail string `json:"user_email"`
IsAdmin bool `json:"is_admin"`
jwt.RegisteredClaims
}

View File

@ -4,33 +4,21 @@ import (
"context"
"fmt"
"log"
"os"
"gitea.local/admin/hspguard/internal/config"
"gitea.local/admin/hspguard/internal/repository"
"gitea.local/admin/hspguard/internal/util"
"github.com/google/uuid"
)
func EnsureAdminUser(ctx context.Context, repo *repository.Queries) {
adminName := os.Getenv("ADMIN_NAME")
if adminName == "" {
adminName = "admin"
}
adminEmail := os.Getenv("ADMIN_EMAIL")
adminPassword := os.Getenv("ADMIN_PASSWORD")
if adminEmail == "" {
log.Fatalln("ERR: ADMIN_EMAIL env variable is required")
}
_, err := repo.FindUserEmail(ctx, adminEmail)
func EnsureAdminUser(ctx context.Context, cfg *config.AppConfig, repo *repository.Queries) {
_, err := repo.FindUserEmail(ctx, cfg.Admin.Email)
if err != nil {
if adminPassword == "" {
if cfg.Admin.Password == "" {
log.Fatalln("ERR: ADMIN_PASSWORD env variable is required")
}
if _, err := createAdmin(ctx, adminName, adminEmail, adminPassword, repo); err != nil {
if _, err := createAdmin(ctx, cfg.Admin.Name, cfg.Admin.Email, cfg.Admin.Password, repo); err != nil {
log.Fatalln("ERR: Failed to create admin account:", err)
}
}