Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions esignet-service/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
# Copy this file to .env and adjust values for your environment:
# cp .env.example .env
#
# make.sh loads .env automatically when present.
# Precedence: VAR=VALUE on the command line > .env > shell environment > defaults.
# make.sh loads .env into the environment before starting the service, so these
# values apply to `go run`, the binary, and tests. For Docker/CI, pass the same
# variables through your usual mechanism. Variables already set in the real
# environment always take precedence over .env.
# ─────────────────────────────────────────────────────────────────────────────

# ── HTTP / ThunderID engine ───────────────────────────────────────────────────
Expand Down Expand Up @@ -53,14 +55,14 @@ OIDC_UI_ERROR_PATH=/error

# ── PostgreSQL (client management persistence) ────────────────────────────────
# Option A: full DSN (takes precedence over individual vars)
# POSTGRES_URL=postgres://esignet:secret@localhost:5432/mosip_esignet?sslmode=disable
# DATABASE_URL=postgres://esignet:secret@localhost:5432/mosip_esignet?sslmode=disable

# Option B: individual connection params
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=mosip_esignet
DATABASE_USERNAME=esignet
# DB_DBUSER_PASSWORD=secret
# DATABASE_PASSWORD=secret

# Connection pool tuning (optional — defaults shown)
# DB_MAX_OPEN_CONNS=25
Expand Down
12 changes: 7 additions & 5 deletions esignet-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,14 @@ The checked-in `go.mod` `replace` directive pins a Thunder backend fork until th
### Quick start (development)

```bash
cp .env.example .env # fill in DATABASE_* / DB_DBUSER_PASSWORD and REDIS_* at minimum
cp .env.example .env # fill in DATABASE_* and REDIS_* at minimum
./make.sh run
```

Copy `.env.example` to `.env` to override defaults, or pass overrides on the command line (`./make.sh run PORT=9090`).

`make.sh` loads `.env` from the working directory into the environment before starting the service, so it applies to `go run`, the built binary, and tests. The service itself only reads variables already present in the environment; for Docker/CI, supply them through your usual mechanism. Real environment variables already set always take precedence over `.env`.

### Binary

```bash
Expand All @@ -75,7 +77,7 @@ export PORT=8088
export MOSIP_ESIGNET_HOST=http://127.0.0.1:8088
export DATABASE_HOST=localhost
export DATABASE_USERNAME=esignet
export DB_DBUSER_PASSWORD=secret
export DATABASE_PASSWORD=secret
export DATABASE_NAME=mosip_esignet
export REDIS_HOST=localhost
export AUTHN_PROVIDER=mosip
Expand Down Expand Up @@ -139,12 +141,12 @@ Authorize redirects are sent to the Thunder gate client:

| Variable | Default | Purpose |
|----------|---------|---------|
| `POSTGRES_URL` | _(empty)_ | Full DSN — takes precedence if set |
| `DATABASE_URL` | _(empty)_ | Full DSN — takes precedence if set |
| `DATABASE_HOST` | `localhost` | |
| `DATABASE_PORT` | `5432` | |
| `DATABASE_NAME` | `mosip_esignet` | |
| `DATABASE_USERNAME` | `postgres` | |
| `DB_DBUSER_PASSWORD` | _(empty)_ | |
| `DATABASE_PASSWORD` | _(empty)_ | |
| `DB_MAX_OPEN_CONNS` | `25` | Max open connections |
| `DB_MAX_IDLE_CONNS` | `5` | Max idle connections |
| `DB_CONN_MAX_LIFETIME_SECS` | `300` | Connection lifetime |
Expand Down Expand Up @@ -339,7 +341,7 @@ curl -s http://127.0.0.1:8088/health
docker run --rm -p 8088:8088 \
-e MOSIP_ESIGNET_HOST=http://127.0.0.1:8088 \
-e CRYPTO_ENCRYPTION_KEY=your-64-char-hex-key \
-e POSTGRES_URL=postgres://esignet:secret@host.docker.internal:5432/mosip_esignet?sslmode=disable \
-e DATABASE_URL=postgres://esignet:secret@host.docker.internal:5432/mosip_esignet?sslmode=disable \
-e REDIS_URL=redis://host.docker.internal:6379/0 \
-e AUTHN_PROVIDER=mosip \
esignet:latest
Expand Down
1 change: 1 addition & 0 deletions esignet-service/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.26

require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/lib/pq v1.10.9
github.com/redis/go-redis/v9 v9.18.0
github.com/stretchr/testify v1.11.1
Expand Down
2 changes: 2 additions & 0 deletions esignet-service/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
181 changes: 94 additions & 87 deletions esignet-service/internal/config/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
package config

import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/kelseyhightower/envconfig"
engineconfig "github.com/thunder-id/thunderid/pkg/thunderidengine/config"
"gopkg.in/yaml.v3"
)

const (
defaultPort = 8088
defaultDataDir = "./data"
appConfigFileName = "deployment.yaml"
defaultGatePort = 3000
Expand Down Expand Up @@ -49,7 +49,27 @@ type AppConfig struct {
Consent engineconfig.ConsentConfig `yaml:"consent"`
}

// LoadAppConfig loads the application configuration from the default data directory.
// appSpec is the environment-variable layout for core application settings.
// Issuer carries no default tag because its fallback is derived from the
// resolved Port at load time.
type appSpec struct {
Identifier string `envconfig:"NAMESPACE" default:"esignet"`
Port int `envconfig:"PORT" default:"8088"`
Issuer string `envconfig:"MOSIP_ESIGNET_HOST"`
DataDir string `envconfig:"DATA_DIR" default:"./data"`
Provider string `envconfig:"AUTHN_PROVIDER" default:"mock"`
AuthFlowID string `envconfig:"AUTH_FLOW_ID" default:"flow-esignet"`
ThemeID string `envconfig:"THEME_ID" default:"theme-esignet"`
LayoutID string `envconfig:"LAYOUT_ID" default:"layout-esignet"`
EncryptionKey string `envconfig:"CRYPTO_ENCRYPTION_KEY" required:"true"`
}

// LoadAppConfig loads the application configuration from the default data
// directory and overlays environment-derived settings. It returns an error
// (and a nil config) if the file cannot be read or any environment variable
// cannot be parsed into its target type, so an invalid configuration fails
// startup rather than being silently coerced or mistaken for a usable
// zero-value struct.
func LoadAppConfig() (*AppConfig, error) {
path := filepath.Join(defaultDataDir, appConfigFileName)
data, err := os.ReadFile(path)
Expand All @@ -65,28 +85,46 @@ func LoadAppConfig() (*AppConfig, error) {
return nil, fmt.Errorf("parse %s: %w", path, err)
}

applyDefaults(&cfg)
if err := applyDefaults(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}

func applyDefaults(cfg *AppConfig) {
cfg.Identifier = envOrDefault("NAMESPACE", "esignet")
cfg.Port = envIntOrDefault("PORT", defaultPort)
cfg.Issuer = envOrDefault("MOSIP_ESIGNET_HOST", fmt.Sprintf("http://127.0.0.1:%d", cfg.Port))
cfg.DataDir = envOrDefault("DATA_DIR", defaultDataDir)
func applyDefaults(cfg *AppConfig) error {
var s appSpec
if err := envconfig.Process("", &s); err != nil {
return fmt.Errorf("loading app config: %w", err)
}

cfg.Identifier = s.Identifier
cfg.Port = s.Port
cfg.Issuer = s.Issuer
if cfg.Issuer == "" {
cfg.Issuer = fmt.Sprintf("http://127.0.0.1:%d", cfg.Port)
}
cfg.DataDir = s.DataDir
yamlDB := cfg.DB
cfg.DB = loadDB()
db, err := loadDB()
if err != nil {
return err
}
cfg.DB = *db
if !hasDBEnvConfig() && yamlDB.DSN != "" {
cfg.DB.DSN = yamlDB.DSN
}
if yamlDB.Pool.MaxOpenConns > 0 {
cfg.DB.Pool = yamlDB.Pool
}
cfg.Redis = loadRedis()
cfg.Provider = envOrDefault("AUTHN_PROVIDER", "mock")
cfg.LayoutID = envOrDefault("LAYOUT_ID", "layout-esignet")
cfg.ThemeID = envOrDefault("THEME_ID", "theme-esignet")
cfg.AuthFlowID = envOrDefault("AUTH_FLOW_ID", "flow-esignet")
redisCfg, err := loadRedis()
if err != nil {
return err
}
cfg.Redis = *redisCfg
cfg.Provider = s.Provider
cfg.LayoutID = s.LayoutID
cfg.ThemeID = s.ThemeID
cfg.AuthFlowID = s.AuthFlowID

cfg.Server.Port = cfg.Port
cfg.Server.Identifier = cfg.Identifier
Expand All @@ -98,10 +136,7 @@ func applyDefaults(cfg *AppConfig) {
cfg.JWT.PreferredKeyID = "default-key"
cfg.JWT.ValidityPeriod = 3600

cfg.EncryptionConfig.Key = envOrDefault("CRYPTO_ENCRYPTION_KEY", "")
if cfg.EncryptionConfig.Key == "" {
panic("CRYPTO_ENCRYPTION_KEY must be set")
}
cfg.EncryptionConfig.Key = s.EncryptionKey

cfg.Cache.Disabled = false
cfg.Cache.Type = "redis"
Expand Down Expand Up @@ -140,88 +175,60 @@ func applyDefaults(cfg *AppConfig) {
cfg.KeyConfig.CertFile = defaultSigningCertPath
cfg.KeyConfig.KeyFile = defaultSigningKeyPath
cfg.KeyConfig.ID = "default-key"

return nil
}

// overrideSpec is the environment-variable layout for optional overrides of
// gate-client and OAuth lifetime settings. Zero values mean "not set" and
// leave the corresponding default in place.
type overrideSpec struct {
UIScheme string `envconfig:"OIDC_UI_SCHEME"`
UIHostname string `envconfig:"OIDC_UI_HOSTNAME"`
UIPort int `envconfig:"OIDC_UI_PORT"`
UILoginPath string `envconfig:"OIDC_UI_LOGIN_PATH"`
UIErrorPath string `envconfig:"OIDC_UI_ERROR_PATH"`

AuthCodeLifetimeSecs int64 `envconfig:"OAUTH_AUTH_CODE_LIFETIME_SECONDS"`
PARExpirySecs int64 `envconfig:"OAUTH_PAR_EXPIRY_SECONDS"`
AccessTokenLifetimeSecs int64 `envconfig:"OAUTH_ACCESS_TOKEN_LIFETIME_SECONDS"`
}

// ApplyEnvOverrides overlays environment and application settings onto cfg.
// Env vars take precedence over values from app.yaml.
func ApplyEnvOverrides(cfg *AppConfig) error {
if v := os.Getenv("OIDC_UI_SCHEME"); v != "" {
cfg.GateClient.Scheme = v
}
if v := os.Getenv("OIDC_UI_HOSTNAME"); v != "" {
cfg.GateClient.Hostname = v
}
if v := os.Getenv("OIDC_UI_PORT"); v != "" {
port, err := strconv.Atoi(v)
if err != nil {
return fmt.Errorf("invalid OIDC_UI_PORT: %w", err)
}
if port < 1 || port > 65535 {
return fmt.Errorf("invalid OIDC_UI_PORT: port must be between 1 and 65535")
}
cfg.GateClient.Port = port
}
if v := os.Getenv("OIDC_UI_LOGIN_PATH"); v != "" {
cfg.GateClient.LoginPath = v
}
if v := os.Getenv("OIDC_UI_ERROR_PATH"); v != "" {
cfg.GateClient.ErrorPath = v
var s overrideSpec
if err := envconfig.Process("", &s); err != nil {
return fmt.Errorf("loading env overrides: %w", err)
}

if v := os.Getenv("OAUTH_AUTH_CODE_LIFETIME_SECONDS"); v != "" {
secs, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("invalid OAUTH_AUTH_CODE_LIFETIME_SECONDS: %w", err)
}
if secs > 0 {
cfg.OAuth.AuthorizationCode.ValidityPeriod = secs
}
if s.UIScheme != "" {
cfg.GateClient.Scheme = s.UIScheme
}
if v := os.Getenv("OAUTH_PAR_EXPIRY_SECONDS"); v != "" {
secs, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("invalid OAUTH_PAR_EXPIRY_SECONDS: %w", err)
}
if secs > 0 {
cfg.OAuth.PAR.ExpiresIn = secs
}
if s.UIHostname != "" {
cfg.GateClient.Hostname = s.UIHostname
}
if v := os.Getenv("OAUTH_ACCESS_TOKEN_LIFETIME_SECONDS"); v != "" {
secs, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return fmt.Errorf("invalid OAUTH_ACCESS_TOKEN_LIFETIME_SECONDS: %w", err)
}
if secs > 0 {
cfg.JWT.ValidityPeriod = secs
if s.UIPort != 0 {
if s.UIPort < 1 || s.UIPort > 65535 {
return errors.New("invalid OIDC_UI_PORT: port must be between 1 and 65535")
}
cfg.GateClient.Port = s.UIPort
}
return nil
}

func envOrDefault(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
if s.UILoginPath != "" {
cfg.GateClient.LoginPath = s.UILoginPath
}
if s.UIErrorPath != "" {
cfg.GateClient.ErrorPath = s.UIErrorPath
}
return fallback
}

func envIntOrDefault(key string, fallback int) int {
raw := os.Getenv(key)
if raw == "" {
return fallback
if s.AuthCodeLifetimeSecs > 0 {
cfg.OAuth.AuthorizationCode.ValidityPeriod = s.AuthCodeLifetimeSecs
}
n, err := strconv.Atoi(raw)
if err != nil {
return fallback
if s.PARExpirySecs > 0 {
cfg.OAuth.PAR.ExpiresIn = s.PARExpirySecs
}
return n
}

func envBool(key string) bool {
switch strings.ToLower(strings.TrimSpace(os.Getenv(key))) {
case "1", "true", "yes", "on":
return true
default:
return false
if s.AccessTokenLifetimeSecs > 0 {
cfg.JWT.ValidityPeriod = s.AccessTokenLifetimeSecs
}
return nil
}
Loading
Loading