feat(auth): replace .env/godotenv secret handling with config-based secrets

Secrets (JWT keys, LDAP sync password, OIDC client id/secret, cross-login
keys) are now configured directly in config.json under the auth section
where they are used. Each secret can still be supplied via its existing
environment variable, which takes precedence over the config value.

The godotenv dependency, the .env file, configs/env-template.txt and the
loadEnvironment() bootstrap step are removed. -init now writes the demo
JWT keys into config.json instead of a .env file.

Closes #283

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 3a7cb814c53f
This commit is contained in:
2026-06-17 12:28:17 +02:00
parent 07b9a57479
commit 83d04dff17
22 changed files with 151 additions and 95 deletions

View File

@@ -17,6 +17,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"sync"
"time"
@@ -115,6 +116,18 @@ type AuthConfig struct {
// Keys holds the global authentication configuration
var Keys AuthConfig
// secretFromEnv resolves a secret from the environment or config. The
// environment variable takes precedence when set and non-empty; otherwise the
// value configured in config.json is used. This lets deployments inject secrets
// via the environment (or a secret manager) while keeping config.json
// self-contained for simple setups.
func secretFromEnv(envVar, configValue string) string {
if v := os.Getenv(envVar); v != "" {
return v
}
return configValue
}
// Authentication manages all authentication methods and session handling
type Authentication struct {
sessionManager *scs.SessionManager

View File

@@ -10,7 +10,6 @@ import (
"encoding/base64"
"errors"
"net/http"
"os"
"strings"
"time"
@@ -39,6 +38,23 @@ type JWTAuthConfig struct {
// Should an existent user be updated in the DB based on the information in the token
UpdateUserOnLogin bool `json:"update-user-on-login"`
// Base64 encoded Ed25519 public key used to validate JWTs.
// Overridden by the JWT_PUBLIC_KEY environment variable when set.
PublicKey string `json:"public-key"`
// Base64 encoded Ed25519 private key used to sign JWTs.
// Overridden by the JWT_PRIVATE_KEY environment variable when set.
PrivateKey string `json:"private-key"`
// Base64 encoded Ed25519 public key for accepting externally generated JWTs.
// Overridden by the CROSS_LOGIN_JWT_PUBLIC_KEY environment variable when set.
CrossLoginPublicKey string `json:"cross-login-public-key"`
// Base64 encoded HMAC (HS256/HS512) key for accepting externally generated
// session login tokens.
// Overridden by the CROSS_LOGIN_JWT_HS512_KEY environment variable when set.
CrossLoginHS512Key string `json:"cross-login-hs512-key"`
}
type JWTAuthenticator struct {
@@ -47,9 +63,10 @@ type JWTAuthenticator struct {
}
func (ja *JWTAuthenticator) Init() error {
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
pubKey := secretFromEnv("JWT_PUBLIC_KEY", Keys.JwtConfig.PublicKey)
privKey := secretFromEnv("JWT_PRIVATE_KEY", Keys.JwtConfig.PrivateKey)
if pubKey == "" || privKey == "" {
cclog.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
cclog.Warn("JWT public/private key not configured ('public-key'/'private-key' in config or 'JWT_PUBLIC_KEY'/'JWT_PRIVATE_KEY' env): token based authentication will not work")
} else {
bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil {
@@ -121,7 +138,7 @@ func (ja *JWTAuthenticator) AuthViaJWT(
// ProvideJWT generates a new JWT that can be used for authentication
func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
if ja.privateKey == nil {
return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set")
return "", errors.New("JWT private key not configured ('private-key' in config or 'JWT_PRIVATE_KEY' env)")
}
now := time.Now()

View File

@@ -11,7 +11,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
"github.com/ClusterCockpit/cc-lib/v2/schema"
@@ -27,10 +26,11 @@ type JWTCookieSessionAuthenticator struct {
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
func (ja *JWTCookieSessionAuthenticator) Init() error {
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
pubKey := secretFromEnv("JWT_PUBLIC_KEY", Keys.JwtConfig.PublicKey)
privKey := secretFromEnv("JWT_PRIVATE_KEY", Keys.JwtConfig.PrivateKey)
if pubKey == "" || privKey == "" {
cclog.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
return errors.New("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
cclog.Warn("JWT public/private key not configured ('public-key'/'private-key' in config or 'JWT_PUBLIC_KEY'/'JWT_PRIVATE_KEY' env): token based authentication will not work")
return errors.New("JWT public/private key not configured: token based authentication will not work")
} else {
bytes, err := base64.StdEncoding.DecodeString(pubKey)
if err != nil {
@@ -47,8 +47,8 @@ func (ja *JWTCookieSessionAuthenticator) Init() error {
}
// Look for external public keys
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
if keyFound && pubKeyCrossLogin != "" {
pubKeyCrossLogin := secretFromEnv("CROSS_LOGIN_JWT_PUBLIC_KEY", Keys.JwtConfig.CrossLoginPublicKey)
if pubKeyCrossLogin != "" {
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
if err != nil {
cclog.Warn("Could not decode cross login JWT public key")
@@ -57,8 +57,8 @@ func (ja *JWTCookieSessionAuthenticator) Init() error {
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
} else {
ja.publicKeyCrossLogin = nil
cclog.Debug("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
return errors.New("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
cclog.Debug("cross login JWT public key not configured ('cross-login-public-key' in config or 'CROSS_LOGIN_JWT_PUBLIC_KEY' env): cross login token based authentication will not work")
return errors.New("cross login JWT public key not configured: cross login token based authentication will not work")
}
// Warn if other necessary settings are not configured

View File

@@ -10,7 +10,6 @@ import (
"errors"
"fmt"
"net/http"
"os"
"strings"
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
@@ -25,12 +24,12 @@ type JWTSessionAuthenticator struct {
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
func (ja *JWTSessionAuthenticator) Init() error {
pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY")
pubKey := secretFromEnv("CROSS_LOGIN_JWT_HS512_KEY", Keys.JwtConfig.CrossLoginHS512Key)
if pubKey == "" {
// Without a configured key the HMAC verification below would run against
// an empty key, which lets anyone forge a valid token. Refuse to register
// the authenticator in that case so JWT session login is simply disabled.
return errors.New("CROSS_LOGIN_JWT_HS512_KEY not set: JWT session login disabled")
return errors.New("cross login HS512 key not configured ('cross-login-hs512-key' in config or 'CROSS_LOGIN_JWT_HS512_KEY' env): JWT session login disabled")
}
bytes, err := base64.StdEncoding.DecodeString(pubKey)

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"net"
"net/http"
"os"
"strings"
"time"
@@ -33,6 +32,10 @@ type LdapConfig struct {
// Should a non-existent user be added to the DB if user exists in ldap directory
SyncUserOnLogin bool `json:"sync-user-on-login"`
UpdateUserOnLogin bool `json:"update-user-on-login"`
// Password for the LDAP admin account used for syncing (optional).
// Overridden by the LDAP_ADMIN_PASSWORD environment variable when set.
SyncPassword string `json:"sync-password"`
}
type LdapAuthenticator struct {
@@ -44,9 +47,9 @@ type LdapAuthenticator struct {
var _ Authenticator = (*LdapAuthenticator)(nil)
func (la *LdapAuthenticator) Init() error {
la.syncPassword = os.Getenv("LDAP_ADMIN_PASSWORD")
la.syncPassword = secretFromEnv("LDAP_ADMIN_PASSWORD", Keys.LdapConfig.SyncPassword)
if la.syncPassword == "" {
cclog.Warn("environment variable 'LDAP_ADMIN_PASSWORD' not set (ldap sync will not work)")
cclog.Warn("LDAP admin password not configured ('sync-password' in config or 'LDAP_ADMIN_PASSWORD' env): ldap sync will not work")
}
if Keys.LdapConfig.UserAttr != "" {

View File

@@ -12,7 +12,6 @@ import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/ClusterCockpit/cc-backend/internal/repository"
@@ -27,6 +26,14 @@ type OpenIDConfig struct {
Provider string `json:"provider"`
SyncUserOnLogin bool `json:"sync-user-on-login"`
UpdateUserOnLogin bool `json:"update-user-on-login"`
// OAuth2 client ID for the OIDC provider.
// Overridden by the OID_CLIENT_ID environment variable when set.
ClientID string `json:"client-id"`
// OAuth2 client secret for the OIDC provider.
// Overridden by the OID_CLIENT_SECRET environment variable when set.
ClientSecret string `json:"client-secret"`
}
type OIDC struct {
@@ -66,13 +73,13 @@ func NewOIDC(a *Authentication) *OIDC {
if err != nil {
cclog.Fatal(err)
}
clientID := os.Getenv("OID_CLIENT_ID")
clientID := secretFromEnv("OID_CLIENT_ID", Keys.OpenIDConfig.ClientID)
if clientID == "" {
cclog.Warn("environment variable 'OID_CLIENT_ID' not set (Open ID connect auth will not work)")
cclog.Warn("OIDC client ID not configured ('client-id' in config or 'OID_CLIENT_ID' env): Open ID connect auth will not work")
}
clientSecret := os.Getenv("OID_CLIENT_SECRET")
clientSecret := secretFromEnv("OID_CLIENT_SECRET", Keys.OpenIDConfig.ClientSecret)
if clientSecret == "" {
cclog.Warn("environment variable 'OID_CLIENT_SECRET' not set (Open ID connect auth will not work)")
cclog.Warn("OIDC client secret not configured ('client-secret' in config or 'OID_CLIENT_SECRET' env): Open ID connect auth will not work")
}
client := &oauth2.Config{

View File

@@ -34,6 +34,22 @@ var configSchema = `
"update-user-on-login": {
"description": "Should an existent user attributes in the DB be updated at login attempt with values provided in JWT.",
"type": "boolean"
},
"public-key": {
"description": "Base64 encoded Ed25519 public key used to validate JWTs. Overridden by the JWT_PUBLIC_KEY environment variable when set.",
"type": "string"
},
"private-key": {
"description": "Base64 encoded Ed25519 private key used to sign JWTs. Overridden by the JWT_PRIVATE_KEY environment variable when set.",
"type": "string"
},
"cross-login-public-key": {
"description": "Base64 encoded Ed25519 public key for accepting externally generated JWTs. Overridden by the CROSS_LOGIN_JWT_PUBLIC_KEY environment variable when set.",
"type": "string"
},
"cross-login-hs512-key": {
"description": "Base64 encoded HMAC (HS256/HS512) key for accepting externally generated session login tokens. Overridden by the CROSS_LOGIN_JWT_HS512_KEY environment variable when set.",
"type": "string"
}
},
"required": ["max-age"]
@@ -52,6 +68,14 @@ var configSchema = `
"update-user-on-login": {
"description": "Should an existent user attributes in the DB be updated at login attempt with values provided.",
"type": "boolean"
},
"client-id": {
"description": "OAuth2 client ID for the OIDC provider. Overridden by the OID_CLIENT_ID environment variable when set.",
"type": "string"
},
"client-secret": {
"description": "OAuth2 client secret for the OIDC provider. Overridden by the OID_CLIENT_SECRET environment variable when set.",
"type": "string"
}
},
"required": ["provider"]
@@ -103,6 +127,10 @@ var configSchema = `
"update-user-on-login": {
"description": "Should an existent user attributes in the DB be updated at login attempt with values from LDAP.",
"type": "boolean"
},
"sync-password": {
"description": "Password for the LDAP admin account used for syncing. Overridden by the LDAP_ADMIN_PASSWORD environment variable when set.",
"type": "string"
}
},
"required": ["url", "user-base", "search-dn", "user-bind", "user-filter"]