Lock bootstrap admin token until password is changed.
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:
+20
-8
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user