diff --git a/README.md b/README.md index 08885b3..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 | +| `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 cf8230e..8142bb2 100644 --- a/src/main.go +++ b/src/main.go @@ -2,8 +2,10 @@ package main import ( "context" + "encoding/base64" "log" "os" + "strings" "github.com/gin-gonic/gin" "github.com/joho/godotenv" @@ -20,6 +22,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 +38,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 +91,18 @@ func envOrDefault(key, defaultValue string) string { } return defaultValue } + +// 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 { + 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) +} 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) }