Nik Afiq 8a549504a4 feat(auth): implement Firebase authentication and token verification
- Added FirebaseAuth struct and TokenVerifier interface for verifying Firebase ID tokens.
- Introduced FirebaseConfig struct in config to manage Firebase credentials and project ID.
- Implemented OAuth handler for Firebase ID token verification in HTTP handlers.
- Added middleware for authenticating requests using Firebase tokens.
- Updated router to conditionally apply authentication based on configuration.
- Created tests for the new authentication middleware.
- Added request and response types for Firebase OAuth handling.
- Included a sample JSON file for testing purposes.
2025-12-10 19:05:11 +09:00

159 lines
3.3 KiB
Go

package config
import (
"encoding/base64"
"encoding/json"
"errors"
"net"
"net/url"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
type Config struct {
Addr string
GinMode string
Auth AuthConfig
Firebase FirebaseConfig
DB DBConfig
}
type AuthConfig struct {
Enabled bool
}
type FirebaseConfig struct {
ProjectID string
CredentialsJSON string
CredentialsFile string
}
type DBConfig struct {
Host string
Port string
User string
Password string
Name string
SSLMode string
MaxConns int32
AppName string
}
func Load() Config {
_ = godotenv.Load()
cfg := Config{
Addr: getenv("ADDR", ":8082"),
GinMode: getenv("GIN_MODE", "release"),
Auth: AuthConfig{
Enabled: getenvBool("AUTH_ENABLED", false),
},
Firebase: FirebaseConfig{
ProjectID: getenv("FIREBASE_PROJECT_ID", ""),
CredentialsJSON: os.Getenv("FIREBASE_CREDENTIALS_JSON"),
CredentialsFile: os.Getenv("FIREBASE_CREDENTIALS_FILE"),
},
DB: DBConfig{
Host: must("PGHOST"),
Port: must("POSTGRES_PORT"),
User: must("POSTGRES_USER"),
Password: os.Getenv("POSTGRES_PASSWORD"),
Name: must("POSTGRES_DB"),
SSLMode: getenv("PGSSLMODE", "disable"),
AppName: getenv("DB_APP_NAME", "current-api"),
},
}
if n := getenv("DB_MAX_CONNS", ""); n != "" {
if v, err := strconv.Atoi(n); err == nil && v > 0 {
cfg.DB.MaxConns = int32(v)
}
}
if cfg.Auth.Enabled {
if cfg.Firebase.ProjectID == "" {
panic("missing FIREBASE_PROJECT_ID with auth enabled")
}
if _, err := cfg.Firebase.CredentialsBytes(); err != nil {
panic("invalid firebase credentials: " + err.Error())
}
}
return cfg
}
func (d DBConfig) DSN() string {
// DATABASE_URL wins if present
if raw := os.Getenv("DATABASE_URL"); raw != "" {
return raw
}
u := &url.URL{
Scheme: "postgres",
User: url.UserPassword(d.User, d.Password),
Host: net.JoinHostPort(d.Host, d.Port),
Path: "/" + d.Name,
}
q := url.Values{}
q.Set("sslmode", d.SSLMode)
if d.AppName != "" {
q.Set("application_name", d.AppName)
}
u.RawQuery = q.Encode()
return u.String()
}
func RedactDSN(raw string) string {
u, err := url.Parse(raw)
if err != nil || u.User == nil {
return raw
}
if _, ok := u.User.Password(); ok {
u.User = url.UserPassword(u.User.Username(), "*****")
}
return u.String()
}
func getenv(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func must(k string) string {
v := os.Getenv(k)
if v == "" {
panic("missing required env: " + k)
}
return v
}
func getenvBool(k string, def bool) bool {
if v := os.Getenv(k); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
return b
}
}
return def
}
func (f FirebaseConfig) CredentialsBytes() ([]byte, error) {
if path := strings.TrimSpace(f.CredentialsFile); path != "" {
return os.ReadFile(path)
}
raw := strings.TrimSpace(f.CredentialsJSON)
if raw == "" {
return nil, errors.New("missing FIREBASE_CREDENTIALS_JSON or FIREBASE_CREDENTIALS_FILE")
}
if json.Valid([]byte(raw)) {
return []byte(raw), nil
}
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, errors.New("firebase credentials must be JSON or base64-encoded JSON")
}
if !json.Valid(decoded) {
return nil, errors.New("decoded firebase credentials are not valid JSON")
}
return decoded, nil
}