Skip to content

Commit 13d4979

Browse files
committed
initial commit
0 parents  commit 13d4979

File tree

15 files changed

+1435
-0
lines changed

15 files changed

+1435
-0
lines changed

.gitignore

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binaries and coverage output
9+
*.test
10+
*.out
11+
12+
# Build directories
13+
/bin/
14+
/obj/
15+
16+
# Coverage folder
17+
/coverage/*
18+
!/coverage/.gitkeep
19+
20+
# Dependency directory (if you vendor your modules)
21+
/vendor/
22+
23+
# Go workspace file (if you use go.work)
24+
/go.work
25+
26+
# IDE/editor directories and files
27+
.vscode/
28+
.idea/
29+
*.iml
30+
31+
# macOS
32+
.DS_Store
33+
34+
# Logs and temporary files
35+
*.log
36+
*.tmp
37+
38+
# Editor swap/backup files
39+
*~
40+
*.swp

LINCENCE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# JWT Manager Golang Library
2+
3+
A simple, secure, and easy-to-use Golang library for generating and validating JWT tokens with custom claims, full test coverage, and mock support.
4+
5+
---
6+
7+
## Description
8+
9+
This library provides:
10+
11+
- **JWT generation** using HMAC-SHA256, with customizable expiration and refresh thresholds.
12+
- **Methods** to validate signature, check expiration (`IsOnTime`), determine if a token needs refresh (`TokenNeedsRefresh`), and decode payload.
13+
- **100% unit and feature tests**.
14+
- A **mock implementation** (`mock/JwtManagerMock`) to simplify testing your own code.
15+
16+
## Installation
17+
18+
```bash
19+
# Add the library as a dependency
20+
go get github.com/not-empty/jwt-manager-go-lib
21+
```
22+
23+
## Usage
24+
25+
Import the package and initialize the manager:
26+
27+
```go
28+
import (
29+
"fmt"
30+
"log"
31+
32+
"github.com/not-empty/jwt-manager-go-lib/jwt_manager"
33+
)
34+
35+
func main() {
36+
// secret, issuer, expire (sec), renew threshold (sec)
37+
mgr := jwt_manager.NewJwtManager("my-secret-key", "my-app", 3600, 1800)
38+
39+
// generate token with custom claims
40+
custom := map[string]interface{}{"role": "admin", "org": "example"}
41+
token := mgr.Generate("audience123", "subject123", custom)
42+
fmt.Println("Token:", token)
43+
44+
// validate signature
45+
ok, err := mgr.IsValid(token)
46+
if err != nil {
47+
log.Fatal(err)
48+
}
49+
fmt.Println("Valid signature?", ok)
50+
51+
// check expiration
52+
onTime, _ := mgr.IsOnTime(token)
53+
fmt.Println("On time?", onTime)
54+
55+
// check refresh threshold
56+
needs, _ := mgr.TokenNeedsRefresh(token)
57+
fmt.Println("Needs refresh?", needs)
58+
59+
// decode payload
60+
payload, _ := mgr.DecodePayload(token)
61+
fmt.Printf("Payload: %+v\n", payload)
62+
}
63+
```
64+
65+
### Example
66+
67+
See [`./example/jwt_example.go`](example/jwt_example.go) for a full runnable demonstration of all public methods.
68+
69+
## Running Tests
70+
71+
- **Unit & feature tests**: run the script:
72+
```bash
73+
./test.sh
74+
```
75+
- **Coverage report**: open the generated HTML:
76+
```bash
77+
open ./test/coverage-unit.html
78+
```
79+
80+
## Mocking
81+
82+
A mock implementation is provided in `mock/JwtManagerMock`. To see it in action, run:
83+
84+
```bash
85+
./mock.sh
86+
```
87+
88+
Use `mock.JwtManagerMock` in your tests to override any behavior of the JWT manager. See [`./mock/jwt_manager_mock_test.go`](mock/jwt_manager_mock_test.go) for usage examples.
89+
90+
## License
91+
92+
This project is licensed under the GNU General Public License v3.0. See the [LICENSE](LICENSE) file for details.
93+
94+
## Contributing
95+
96+
Contribuite by submitting issues and pull requests. Feedback and improvements are always welcome.
97+
98+
---
99+
100+
Not Empty Foundation - Free codes, full minds

coverage/.gitkeep

Whitespace-only changes.

docker-compose.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
version: '3.8'
2+
3+
services:
4+
jwt-manager-go-lib:
5+
container_name: jwt-manager-go-lib
6+
build:
7+
context: .
8+
dockerfile: ops/Dockerfile-dev
9+
volumes:
10+
- .:/app
11+
ulimits:
12+
nofile:
13+
soft: "65536"
14+
hard: "65536"
15+

example/jwt_manager_example.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"time"
7+
8+
"github.com/not-empty/jwt-manager-go-lib"
9+
)
10+
11+
func main() {
12+
// Initialize the JWT manager
13+
// Arguments: secret key, issuer/context, expiration (seconds), renew threshold (seconds)
14+
mgr := jwt_manager.NewJwtManager("my-secret-key", "my-app", 1, 1)
15+
16+
// Generate a token with custom claims
17+
customClaims := map[string]interface{}{"role": "admin", "env": "prod"}
18+
token := mgr.Generate("user123", "subject123", customClaims)
19+
fmt.Println("Generated JWT:", token)
20+
21+
// Validate signature
22+
valid, err := mgr.IsValid(token)
23+
if err != nil {
24+
log.Fatalf("Signature validation error: %v", err)
25+
}
26+
fmt.Println("Signature valid?", valid)
27+
28+
// Check expiration
29+
onTime, err := mgr.IsOnTime(token)
30+
if err != nil {
31+
log.Fatalf("Expiration check error: %v", err)
32+
}
33+
fmt.Println("Token on time?", onTime)
34+
35+
// Check if token needs refresh (after renew threshold)
36+
needsRefresh, err := mgr.TokenNeedsRefresh(token)
37+
if err != nil {
38+
log.Fatalf("Refresh check error: %v", err)
39+
}
40+
fmt.Println("Token needs refresh?", needsRefresh)
41+
42+
// Decode and print payload
43+
payload, err := mgr.DecodePayload(token)
44+
if err != nil {
45+
log.Fatalf("Decode payload error: %v", err)
46+
}
47+
fmt.Println("Payload contents:")
48+
for k, v := range payload {
49+
fmt.Printf(" %s: %v\n", k, v)
50+
}
51+
52+
// Demonstrate expiration by simulating a future timestamp
53+
fmt.Println("\nSimulating expiry check after expiration period...")
54+
// Sleep past expiration (for demonstration, shorten sleep in real use)
55+
time.Sleep(2 * time.Second)
56+
expires, err := mgr.IsOnTime(token)
57+
fmt.Println("On time after wait?", expires, "error:", err)
58+
}

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/not-empty/jwt-manager-go-lib
2+
3+
go 1.24
4+
5+
require github.com/stretchr/testify v1.10.0
6+
7+
require (
8+
github.com/davecgh/go-spew v1.1.1 // indirect
9+
github.com/pmezard/go-difflib v1.0.0 // indirect
10+
gopkg.in/yaml.v3 v3.0.1 // indirect
11+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
6+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
8+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
9+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

jwt_manager.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package jwt_manager
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/base64"
7+
"encoding/json"
8+
"errors"
9+
"strings"
10+
"time"
11+
)
12+
13+
type Manager interface {
14+
Generate(audience, subject string, custom map[string]interface{}) string
15+
IsValid(token string) (bool, error)
16+
IsOnTime(token string) (bool, error)
17+
TokenNeedsRefresh(token string) (bool, error)
18+
DecodePayload(token string) (map[string]interface{}, error)
19+
}
20+
21+
var _ Manager = (*JwtManager)(nil)
22+
23+
type JwtManager struct {
24+
AppSecret string
25+
Context string
26+
Expire int64
27+
Renew int64
28+
algorithm string
29+
tokenType string
30+
}
31+
32+
func NewJwtManager(secret, context string, expire, renew int64) Manager {
33+
return &JwtManager{
34+
AppSecret: secret,
35+
Context: context,
36+
Expire: expire,
37+
Renew: renew,
38+
algorithm: "HS256",
39+
tokenType: "JWT",
40+
}
41+
}
42+
43+
func (j *JwtManager) GetHeader() string {
44+
header := map[string]string{
45+
"alg": j.algorithm,
46+
"typ": j.tokenType,
47+
}
48+
data, _ := json.Marshal(header)
49+
return Base64UrlEncode(data)
50+
}
51+
52+
func (j *JwtManager) getPayload(audience, subject string, custom map[string]interface{}) string {
53+
now := time.Now().Unix()
54+
payload := map[string]interface{}{
55+
"aud": audience,
56+
"exp": now + j.Expire,
57+
"iat": now,
58+
"iss": j.Context,
59+
"sub": subject,
60+
}
61+
for k, v := range custom {
62+
payload[k] = v
63+
}
64+
data, _ := json.Marshal(payload)
65+
return Base64UrlEncode(data)
66+
}
67+
68+
func (j *JwtManager) GetSignature(header, payload string) string {
69+
h := hmac.New(sha256.New, []byte(j.AppSecret))
70+
h.Write([]byte(header + "." + payload))
71+
return Base64UrlEncode(h.Sum(nil))
72+
}
73+
74+
func (j *JwtManager) Generate(audience, subject string, custom map[string]interface{}) string {
75+
header := j.GetHeader()
76+
payload := j.getPayload(audience, subject, custom)
77+
signature := j.GetSignature(header, payload)
78+
return header + "." + payload + "." + signature
79+
}
80+
81+
func (j *JwtManager) IsValid(token string) (bool, error) {
82+
parts := strings.Split(token, ".")
83+
if len(parts) != 3 {
84+
return false, errors.New("invalid JWT format")
85+
}
86+
expectedSig := j.GetSignature(parts[0], parts[1])
87+
if parts[2] != expectedSig && parts[2] != expectedSig+"=" {
88+
return false, errors.New("invalid JWT signature")
89+
}
90+
return true, nil
91+
}
92+
93+
func (j *JwtManager) IsOnTime(token string) (bool, error) {
94+
payload, err := j.DecodePayload(token)
95+
if err != nil {
96+
return false, errors.New("Illegal base64")
97+
}
98+
exp, ok := payload["exp"].(float64)
99+
if !ok || int64(exp) < time.Now().Unix() {
100+
return false, errors.New("JWT expired")
101+
}
102+
return true, nil
103+
}
104+
105+
func (j *JwtManager) TokenNeedsRefresh(token string) (bool, error) {
106+
payload, err := j.DecodePayload(token)
107+
if err != nil {
108+
return false, errors.New("Illegal base64")
109+
}
110+
iat, ok := payload["iat"].(float64)
111+
if !ok {
112+
return false, errors.New("invalid JWT payload: missing iat")
113+
}
114+
if time.Now().Unix() > int64(iat)+j.Renew {
115+
return true, nil
116+
}
117+
return false, nil
118+
}
119+
120+
func (j *JwtManager) DecodePayload(token string) (map[string]interface{}, error) {
121+
parts := strings.Split(token, ".")
122+
if len(parts) != 3 {
123+
return nil, errors.New("invalid token format")
124+
}
125+
payloadJson, err := Base64UrlDecode(parts[1])
126+
if err != nil {
127+
return nil, errors.New("Illegal base64")
128+
}
129+
var payload map[string]interface{}
130+
err = json.Unmarshal([]byte(payloadJson), &payload)
131+
if err != nil {
132+
return nil, errors.New("Illegal payload")
133+
}
134+
return payload, nil
135+
}
136+
137+
func Base64UrlEncode(data []byte) string {
138+
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data)
139+
}
140+
141+
func Base64UrlDecode(data string) (string, error) {
142+
decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(data)
143+
return string(decoded), err
144+
}

0 commit comments

Comments
 (0)