Skip to content

Commit faaa651

Browse files
committed
init
0 parents  commit faaa651

14 files changed

+676
-0
lines changed

.github/workflows/tests.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Unit Tests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- "*"
8+
paths-ignore:
9+
- "**.md"
10+
pull_request:
11+
branches:
12+
- "*"
13+
paths-ignore:
14+
- "**.md"
15+
16+
jobs:
17+
tests:
18+
name: Unit Tests
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Set up Go
22+
uses: actions/setup-go@v2
23+
with:
24+
go-version: ^1.18
25+
- name: Check out code
26+
uses: actions/checkout@v2
27+
- name: Run Tests
28+
run: go test -v -timeout 30s -covermode atomic ./...

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, built with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool, specifically when used with LiteIDE
12+
*.out
13+
14+
# Dependency directories (remove the comment below to include it)
15+
# vendor/
16+
17+
.env

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 Ringo Hoffmann
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# jwt
2+
3+
This is a very simplistic implementation of JWT using hashing algorithms like `HS256` or `HS512` and taking advantage of Go 1.18 generic type parameters for parsing claim objects.
4+
5+
This package is very much inspired and influenced by [robbert229's JWT implementation](https://github.com/robbert229/jwt). [Here](https://github.com/robbert229/jwt/blob/master/LICENSE) you can find the projects License.
6+
7+
## Usage
8+
9+
```go
10+
const signingSecret = "3U5o3Z#XqfLpr3pjGknwWa^u6)CCo&&G"
11+
12+
algorithm := jwt.NewHmacSha512([]byte(signingSecret))
13+
handler := jwt.NewHandler[Claims](algorithm)
14+
15+
claims := new(Claims)
16+
claims.UserID = "221905671296253953"
17+
claims.Iss = "jwt example"
18+
claims.SetIat()
19+
claims.SetExpDuration(15 * time.Minute)
20+
claims.SetNbfTime(time.Now())
21+
22+
token, err := handler.EncodeAndSign(*claims)
23+
if err != nil {
24+
log.Fatalf("Token generation failed: %s", err.Error())
25+
}
26+
27+
log.Printf("Token generated: %s", token)
28+
29+
recoveredClaims, err := handler.DecodeAndValidate(token)
30+
if err != nil {
31+
log.Fatalf("Token validation failed: %s", err.Error())
32+
}
33+
34+
log.Printf("Recovered claims: %+v", recoveredClaims)
35+
```
36+
37+
Go to [examples](examples) to see the full example.

algorithms.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package jwt
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"crypto/sha512"
7+
"hash"
8+
)
9+
10+
// IAlgorithm describes a hash algorithm used to
11+
// generate hash sums from given data.
12+
type IAlgorithm interface {
13+
// Name returns the name of the algorithm.
14+
// The name must be all uppercase.
15+
Name() string
16+
// Sum takes any data and returns the hashed
17+
// sum of the given data.
18+
Sum(data []byte) ([]byte, error)
19+
}
20+
21+
type Algorithm struct {
22+
name string
23+
hasher hash.Hash
24+
}
25+
26+
func (t Algorithm) Name() string {
27+
return t.name
28+
}
29+
30+
func (t Algorithm) Sum(data []byte) ([]byte, error) {
31+
_, err := t.hasher.Write([]byte(data))
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
sum := t.hasher.Sum(nil)
37+
t.hasher.Reset()
38+
39+
return sum, nil
40+
}
41+
42+
// NewAlgorithmWithKey returns a new algorithm using the
43+
// given hash implementation with the given name and
44+
// a key used to sign the hash with.
45+
func NewAlgorithmWithKey(name string, hasher func() hash.Hash, key []byte) Algorithm {
46+
return Algorithm{
47+
name: name,
48+
hasher: hmac.New(hasher, key),
49+
}
50+
}
51+
52+
// NewHmacSha256 returns a new algorithm using HS256.
53+
func NewHmacSha256(key []byte) Algorithm {
54+
return NewAlgorithmWithKey("HS256", sha256.New, key)
55+
}
56+
57+
// NewHmacSha512 returns a new algorithm using HS512.
58+
func NewHmacSha512(key []byte) Algorithm {
59+
return NewAlgorithmWithKey("HS512", sha512.New, key)
60+
}

claims.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package jwt
2+
3+
import "time"
4+
5+
// IValidateExp describes an implementation to check the
6+
// claims 'exp' value against the given current time.
7+
type IValidateExp interface {
8+
ValidateExp(now time.Time) bool
9+
}
10+
11+
// IValidateNbf describes an implementation to check the
12+
// claims 'nbf' value against the given current time.
13+
type IValidateNbf interface {
14+
ValidateNbf(now time.Time) bool
15+
}
16+
17+
// PublicClaims contains general public clains as
18+
// specified in RFC7519, Section 4.1.
19+
//
20+
// This struct also implements IValidateExp and
21+
// IValidateNbf to validate the timings of the
22+
// claims.
23+
//
24+
// You can simply extend these claims by your custom
25+
// ones by setting the PublicClaims as an anonymous
26+
// field in your claims model.
27+
// Example:
28+
// type MyClains struct {
29+
// PublicClaims
30+
//
31+
// UserID string `json:"uid"`
32+
// }
33+
//
34+
// claims := new(MyClains)
35+
// claims.UserID = "123"
36+
// claims.SetExpDuration(15 * time.Minute)
37+
//
38+
// Reference:
39+
// https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1
40+
type PublicClaims struct {
41+
Iss string `json:"iss,omitempty"` // Issuer
42+
Sub string `json:"sub,omitempty"` // Subject
43+
Aud string `json:"aud,omitempty"` // Audience
44+
Exp int64 `json:"exp,omitempty"` // UNIX Expiration Time
45+
Nbf int64 `json:"nbf,omitempty"` // UNIX Not Before Time
46+
Iat int64 `json:"iat,omitempty"` // UNIX Issued At Time
47+
Jti string `json:"jti,omitempty"` // JWT ID
48+
}
49+
50+
func (t PublicClaims) ValidateExp(now time.Time) bool {
51+
if t.Exp == 0 {
52+
return true
53+
}
54+
55+
return now.Before(time.Unix(t.Exp, 0))
56+
}
57+
58+
func (t PublicClaims) ValidateNbf(now time.Time) bool {
59+
if t.Nbf == 0 {
60+
return true
61+
}
62+
63+
return now.After(time.Unix(t.Nbf, 0))
64+
}
65+
66+
// SetExpTime sets 'exp' to the given time.
67+
func (t *PublicClaims) SetExpTime(tm time.Time) {
68+
t.Exp = tm.Unix()
69+
}
70+
71+
// SetExpDuration sets 'exp' to the time in the given duration.
72+
func (t *PublicClaims) SetExpDuration(duration time.Duration) {
73+
t.SetExpTime(time.Now().Add(duration))
74+
}
75+
76+
// SetNbfTime sets 'nbf' to the given time.
77+
func (t *PublicClaims) SetNbfTime(tm time.Time) {
78+
t.Nbf = tm.Unix()
79+
}
80+
81+
// SetNbfDuration sets 'nbf' to the time in the given duration.
82+
func (t *PublicClaims) SetNbfDuration(duration time.Duration) {
83+
t.SetNbfTime(time.Now().Add(duration))
84+
}
85+
86+
// SetIat sets 'iat' to the current time.
87+
//
88+
// You can also pass a custom time to be set.
89+
func (t *PublicClaims) SetIat(tm ...time.Time) {
90+
var st time.Time
91+
if len(tm) != 0 {
92+
st = tm[0]
93+
} else {
94+
st = time.Now()
95+
}
96+
t.Iat = st.Unix()
97+
}

claims_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package jwt
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestValidateExp(t *testing.T) {
9+
var claims PublicClaims
10+
now := time.Now()
11+
12+
claims.Exp = 0
13+
if !claims.ValidateExp(now) {
14+
t.Fatal("ValidateExp returned false when empty")
15+
}
16+
17+
claims.Exp = now.Add(1 * time.Minute).Unix()
18+
if !claims.ValidateExp(now) {
19+
t.Fatal("ValidateExp returned false")
20+
}
21+
22+
claims.Exp = now.Add(-1 * time.Minute).Unix()
23+
if claims.ValidateExp(now) {
24+
t.Fatal("ValidateExp returned true")
25+
}
26+
}
27+
28+
func TestValidateNbf(t *testing.T) {
29+
var claims PublicClaims
30+
now := time.Now()
31+
32+
claims.Nbf = 0
33+
if !claims.ValidateNbf(now) {
34+
t.Fatal("ValidateNbf returned false when empty")
35+
}
36+
37+
claims.Nbf = now.Add(1 * time.Minute).Unix()
38+
if claims.ValidateNbf(now) {
39+
t.Fatal("ValidateNbf returned true")
40+
}
41+
42+
claims.Nbf = now.Add(-1 * time.Minute).Unix()
43+
if !claims.ValidateNbf(now) {
44+
t.Fatal("ValidateNbf returned false")
45+
}
46+
}

encoding.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package jwt
2+
3+
import (
4+
"encoding/base64"
5+
"encoding/json"
6+
)
7+
8+
func b64JsonEncode(payload any) (string, error) {
9+
data, err := json.Marshal(payload)
10+
if err != nil {
11+
return "", err
12+
}
13+
return base64.RawURLEncoding.EncodeToString(data), nil
14+
}
15+
16+
func b64JsonDecode(data string, v any) error {
17+
raw, err := base64.RawStdEncoding.DecodeString(data)
18+
if err != nil {
19+
return err
20+
}
21+
return json.Unmarshal(raw, v)
22+
}

errors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package jwt
2+
3+
import "errors"
4+
5+
var (
6+
ErrInvalidTokenFormat = errors.New("invalid token format")
7+
ErrInvalidSignature = errors.New("invalid signature")
8+
ErrTokenExpired = errors.New("token has expired (invalid exp)")
9+
ErrNotValidYet = errors.New("token is not valid yet (invalid nbf)")
10+
)

example/example.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"time"
6+
7+
"github.com/zekrotja/jwt"
8+
)
9+
10+
type Claims struct {
11+
jwt.PublicClaims
12+
13+
UserID string `json:"uid"`
14+
}
15+
16+
func main() {
17+
const signingSecret = "3U5o3Z#XqfLpr3pjGknwWa^u6)CCo&&G"
18+
19+
algorithm := jwt.NewHmacSha512([]byte(signingSecret))
20+
handler := jwt.NewHandler[Claims](algorithm)
21+
22+
claims := new(Claims)
23+
claims.UserID = "221905671296253953"
24+
claims.Iss = "jwt example"
25+
claims.SetIat()
26+
claims.SetExpDuration(15 * time.Minute)
27+
claims.SetNbfTime(time.Now())
28+
29+
token, err := handler.EncodeAndSign(*claims)
30+
if err != nil {
31+
log.Fatalf("Token generation failed: %s", err.Error())
32+
}
33+
34+
log.Printf("Token generated: %s", token)
35+
36+
recoveredClaims, err := handler.DecodeAndValidate(token)
37+
if err != nil {
38+
log.Fatalf("Token validation failed: %s", err.Error())
39+
}
40+
41+
log.Printf("Recovered claims: %+v", recoveredClaims)
42+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/zekrotja/jwt
2+
3+
go 1.18

0 commit comments

Comments
 (0)