package service import ( "strings" "testing" "time" "github.com/golang-jwt/jwt/v5" "hy2xs-admin/dao" "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" ) func newJwtTestDB(t *testing.T) []byte { t.Helper() newTestDB(t) secret := "test-jwt-secret-not-used-anywhere-else-0123456789" if err := dao.UpsertConfigValue(constant.JwtSecret, secret); err != nil { t.Fatalf("не удалось записать %s: %v", constant.JwtSecret, err) } return []byte(secret) } func testAccount() bo.AccountBo { return bo.AccountBo{Id: 1, Username: "hy2xsadmin", Roles: []string{"admin"}, TokenVersion: 1} } func TestGenAndParseTokenRoundTrip(t *testing.T) { newJwtTestDB(t) token, err := GenToken(testAccount()) if err != nil { t.Fatalf("GenToken: %v", err) } claims, err := ParseToken(token) if err != nil { t.Fatalf("ParseToken: %v", err) } if claims.Admin.Id != 1 || claims.Admin.Username != "hy2xsadmin" { t.Fatalf("claims не совпадают: %+v", claims.Admin) } if claims.Admin.TokenVersion != 1 { t.Fatalf("token_version потерян: %d", claims.Admin.TokenVersion) } if claims.ExpiresAt == nil { t.Fatal("токен выписан без срока жизни") } } // Алгоритм подписи задаётся сервером, а не токеном. // // Прежний keyfunc возвращал ключ, не глядя на token.Method: набор допустимых // алгоритмов фактически определял недоверенный вход. Здесь проверяется, что // токен, подписанный другим HMAC-алгоритмом тем же ключом, отклоняется. func TestParseTokenRejectsOtherSigningMethod(t *testing.T) { secret := newJwtTestDB(t) now := time.Now() claims := MyClaims{ Admin: testAccount(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), IssuedAt: jwt.NewNumericDate(now), Issuer: tokenIssuer, }, } signed, err := jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString(secret) if err != nil { t.Fatalf("не удалось подписать тестовый токен: %v", err) } if _, err := ParseToken(signed); err == nil { t.Fatal("токен, подписанный HS512, принят при разрешённом только HS256") } } // Токен без exp не должен считаться бессрочно валидным. func TestParseTokenRejectsTokenWithoutExpiration(t *testing.T) { secret := newJwtTestDB(t) claims := MyClaims{ Admin: testAccount(), RegisteredClaims: jwt.RegisteredClaims{ IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: tokenIssuer, }, } signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret) if err != nil { t.Fatalf("не удалось подписать тестовый токен: %v", err) } if _, err := ParseToken(signed); err == nil { t.Fatal("токен без exp принят") } } func TestParseTokenRejectsExpiredToken(t *testing.T) { secret := newJwtTestDB(t) past := time.Now().Add(-2 * time.Hour) claims := MyClaims{ Admin: testAccount(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(past.Add(time.Hour)), IssuedAt: jwt.NewNumericDate(past), Issuer: tokenIssuer, }, } signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret) if err != nil { t.Fatalf("не удалось подписать тестовый токен: %v", err) } _, err = ParseToken(signed) if err == nil { t.Fatal("истёкший токен принят") } if err.Error() != constant.TokenExpiredError { t.Errorf("истечение срока должно сообщаться отдельно, получено: %v", err) } } // Токен, выписанный не нами, не должен разбираться как наш даже при совпадении // ключа: issuer входит в проверку. func TestParseTokenRejectsForeignIssuer(t *testing.T) { secret := newJwtTestDB(t) now := time.Now() claims := MyClaims{ Admin: testAccount(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), IssuedAt: jwt.NewNumericDate(now), Issuer: "some-other-service", }, } signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret) if err != nil { t.Fatalf("не удалось подписать тестовый токен: %v", err) } if _, err := ParseToken(signed); err == nil { t.Fatal("токен с чужим issuer принят") } } func TestParseTokenRejectsWrongSecret(t *testing.T) { newJwtTestDB(t) now := time.Now() claims := MyClaims{ Admin: testAccount(), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)), IssuedAt: jwt.NewNumericDate(now), Issuer: tokenIssuer, }, } signed, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte("совершенно другой ключ подписи")) if err != nil { t.Fatalf("не удалось подписать тестовый токен: %v", err) } if _, err := ParseToken(signed); err == nil { t.Fatal("токен, подписанный чужим ключом, принят") } } func TestParseTokenRejectsGarbage(t *testing.T) { newJwtTestDB(t) for _, value := range []string{ "", "not-a-token", "a.b.c", strings.Repeat("A", 500), "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJhZG1pbiI6eyJpZCI6MX19.", } { if _, err := ParseToken(value); err == nil { t.Errorf("мусорный токен %q принят", value) } } } // Пустой JWT_SECRET — повреждённое состояние, а не «ключ нулевой длины». // Раньше значение разыменовывалось без проверки, и подпись выполнялась пустым // ключом: такой токен подделывает кто угодно. func TestGenTokenRefusesEmptySecret(t *testing.T) { newTestDB(t) if err := dao.UpsertConfigValue(constant.JwtSecret, ""); err != nil { t.Fatalf("не удалось очистить %s: %v", constant.JwtSecret, err) } if _, err := GenToken(testAccount()); err == nil { t.Fatal("токен подписан пустым ключом") } if _, err := ParseToken("whatever"); err == nil { t.Fatal("разбор токена с пустым ключом не отклонён") } }