Lock bootstrap admin token until password is changed.
Format / gofmt (push) Successful in 7s
CI / Build (push) Successful in 14s
Format / gofmt (pull_request) Successful in 7s
CI / Build (pull_request) Successful in 13s
CI / Go Tests (push) Successful in 50s
CI / Go Tests (pull_request) Successful in 48s

Closes #2: bootstrap JWTs cannot mutate admin APIs except change-password, production requires ADMIN_PASSWORD on first boot, admin binds loopback.
This commit is contained in:
s1d3sw1ped_bot
2026-09-01 03:54:09 +00:00
parent 6aabb76c16
commit 13070a275d
13 changed files with 445 additions and 40 deletions
+20 -8
View File
@@ -22,10 +22,11 @@ var (
// Claims for our JWT (minimal, matching original style).
type Claims struct {
UserID int `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Roles []string `json:"roles"`
UserID int `json:"user_id"`
Email string `json:"email"`
Name string `json:"name"`
Roles []string `json:"roles"`
Bootstrap bool `json:"bootstrap,omitempty"`
jwt.RegisteredClaims
}
@@ -95,15 +96,26 @@ func NewJWTManager(secret string) *JWTManager {
// GenerateToken creates a JWT for the given user (1 hour expiry like typical).
func (m *JWTManager) GenerateToken(userID int, email, name string, roles []string) (string, error) {
return m.signToken(userID, email, name, roles, false)
}
// GenerateBootstrapToken issues a JWT that cannot call mutating admin APIs
// until the default password has been changed.
func (m *JWTManager) GenerateBootstrapToken(userID int, email, name string, roles []string) (string, error) {
return m.signToken(userID, email, name, roles, true)
}
func (m *JWTManager) signToken(userID int, email, name string, roles []string, bootstrap bool) (string, error) {
now := time.Now()
if roles == nil {
roles = []string{}
}
claims := Claims{
UserID: userID,
Email: email,
Name: name,
Roles: roles,
UserID: userID,
Email: email,
Name: name,
Roles: roles,
Bootstrap: bootstrap,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
IssuedAt: jwt.NewNumericDate(now),
+26
View File
@@ -32,6 +32,32 @@ func TestJWTManager_GenerateValidate_Roundtrip(t *testing.T) {
}
}
func TestJWTManager_BootstrapClaim(t *testing.T) {
m := NewJWTManager("test-secret-123")
token, err := m.GenerateBootstrapToken(1, "admin.com", "Admin", []string{"admin"})
if err != nil {
t.Fatal(err)
}
claims, err := m.ValidateToken(token)
if err != nil {
t.Fatal(err)
}
if !claims.Bootstrap {
t.Error("bootstrap token missing claim")
}
full, err := m.GenerateToken(1, "admin.com", "Admin", []string{"admin"})
if err != nil {
t.Fatal(err)
}
fullClaims, err := m.ValidateToken(full)
if err != nil {
t.Fatal(err)
}
if fullClaims.Bootstrap {
t.Error("normal token should not be bootstrap")
}
}
func TestJWTManager_EmptySecretRandomRoundtrip(t *testing.T) {
m := NewJWTManager("") // empty -> random in-memory secret
token, _ := m.GenerateToken(1, "a@b", "A", nil)