From 5dc4a175550bd48d70d678feb747020216978d62 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Sat, 18 Apr 2026 11:35:01 +0800 Subject: [PATCH 1/2] base64 decode FMSG_API_JWT_SECRET if valid base64 --- README.md | 2 +- src/main.go | 14 +++++++++++++- src/middleware/jwt.go | 6 +++--- src/middleware/jwt_test.go | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 08885b3..8e60d43 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ fine-grained authorisation rules based on the user identity they contain. | Variable | Default | Description | | ------------------- | ------------------------ | ------------------------------------------------------- | | `FMSG_DATA_DIR` | *(required)* | Path where message data files are stored, e.g. `/opt/fmsg/data` | -| `FMSG_API_JWT_SECRET` | *(required)* | HMAC secret used to validate JWT tokens | +| `FMSG_API_JWT_SECRET` | *(required)* | HMAC secret used to validate JWT tokens. If the value is valid base64 it is decoded to bytes; otherwise the raw string is used. | | `FMSG_API_PORT` | `8000` | TCP port the HTTP server listens on | | `FMSG_ID_URL` | `http://127.0.0.1:8080` | Base URL of the fmsgid identity service | diff --git a/src/main.go b/src/main.go index cf8230e..1fc0ae6 100644 --- a/src/main.go +++ b/src/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/base64" "log" "os" @@ -20,6 +21,7 @@ func main() { // Required configuration. dataDir := mustEnv("FMSG_DATA_DIR") jwtSecret := mustEnv("FMSG_API_JWT_SECRET") + jwtKey := parseSecret(jwtSecret) // Optional configuration with defaults. port := envOrDefault("FMSG_API_PORT", "8000") @@ -35,7 +37,7 @@ func main() { log.Println("connected to PostgreSQL") // Initialise JWT middleware. - jwtMiddleware, err := middleware.SetupJWT(jwtSecret, idURL) + jwtMiddleware, err := middleware.SetupJWT(jwtKey, idURL) if err != nil { log.Fatalf("failed to initialise JWT middleware: %v", err) } @@ -88,3 +90,13 @@ func envOrDefault(key, defaultValue string) string { } return defaultValue } + +// parseSecret tries to base64-decode s. If s is valid base64 the decoded bytes +// are returned; otherwise the raw string bytes are used. +func parseSecret(s string) []byte { + b, err := base64.StdEncoding.DecodeString(s) + if err == nil { + return b + } + return []byte(s) +} diff --git a/src/middleware/jwt.go b/src/middleware/jwt.go index 0c2080a..1e904b0 100644 --- a/src/middleware/jwt.go +++ b/src/middleware/jwt.go @@ -25,9 +25,9 @@ type identityClaims struct { } // SetupJWT creates and returns a configured GinJWTMiddleware. -// secret is the HMAC secret used to validate tokens. +// key is the HMAC secret bytes used to validate tokens. // idURL is the base URL of the fmsgid service used to validate user addresses. -func SetupJWT(secret string, idURL string) (*jwt.GinJWTMiddleware, error) { +func SetupJWT(key []byte, idURL string) (*jwt.GinJWTMiddleware, error) { // Set the global TimeFunc used by golang-jwt/v4 when validating iat/nbf/exp // inside MapClaims.Valid(). gin-jwt's own TimeFunc field does not affect this // path; only the package-level variable does. @@ -35,7 +35,7 @@ func SetupJWT(secret string, idURL string) (*jwt.GinJWTMiddleware, error) { mw, err := jwt.New(&jwt.GinJWTMiddleware{ Realm: "fmsg", - Key: []byte(secret), + Key: key, Timeout: 24 * time.Hour, MaxRefresh: 24 * time.Hour, IdentityKey: IdentityKey, diff --git a/src/middleware/jwt_test.go b/src/middleware/jwt_test.go index 9093154..1b210ca 100644 --- a/src/middleware/jwt_test.go +++ b/src/middleware/jwt_test.go @@ -44,7 +44,7 @@ func TestIATClockSkew(t *testing.T) { const secret = "test-secret-for-iat-skew" // SetupJWT sets jwtv4.TimeFunc as a side-effect. - if _, err := SetupJWT(secret, "http://127.0.0.1:0"); err != nil { + if _, err := SetupJWT([]byte(secret), "http://127.0.0.1:0"); err != nil { t.Fatalf("SetupJWT: %v", err) } From a1e8efb6173a7124894d1cb15389b29055cc6f81 Mon Sep 17 00:00:00 2001 From: Mark Mennell Date: Sat, 18 Apr 2026 11:39:41 +0800 Subject: [PATCH 2/2] only if starts with base64: --- README.md | 2 +- src/main.go | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8e60d43..4e2b89a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ fine-grained authorisation rules based on the user identity they contain. | Variable | Default | Description | | ------------------- | ------------------------ | ------------------------------------------------------- | | `FMSG_DATA_DIR` | *(required)* | Path where message data files are stored, e.g. `/opt/fmsg/data` | -| `FMSG_API_JWT_SECRET` | *(required)* | HMAC secret used to validate JWT tokens. If the value is valid base64 it is decoded to bytes; otherwise the raw string is used. | +| `FMSG_API_JWT_SECRET` | *(required)* | HMAC secret used to validate JWT tokens. Prefix with `base64:` to supply a base64-encoded key (e.g. `base64:c2VjcmV0`); otherwise the raw string is used. | | `FMSG_API_PORT` | `8000` | TCP port the HTTP server listens on | | `FMSG_ID_URL` | `http://127.0.0.1:8080` | Base URL of the fmsgid identity service | diff --git a/src/main.go b/src/main.go index 1fc0ae6..8142bb2 100644 --- a/src/main.go +++ b/src/main.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "log" "os" + "strings" "github.com/gin-gonic/gin" "github.com/joho/godotenv" @@ -91,11 +92,16 @@ func envOrDefault(key, defaultValue string) string { return defaultValue } -// parseSecret tries to base64-decode s. If s is valid base64 the decoded bytes -// are returned; otherwise the raw string bytes are used. +// parseSecret returns the HMAC key bytes for the given secret string. +// If s begins with "base64:" the remainder is base64-decoded; otherwise the +// raw string bytes are used. func parseSecret(s string) []byte { - b, err := base64.StdEncoding.DecodeString(s) - if err == nil { + const prefix = "base64:" + if strings.HasPrefix(s, prefix) { + b, err := base64.StdEncoding.DecodeString(s[len(prefix):]) + if err != nil { + log.Fatalf("FMSG_API_JWT_SECRET has base64: prefix but is not valid base64: %v", err) + } return b } return []byte(s)