Skip to content

Commit

Permalink
enable jwt generate/validate/extract support with github.com/golang-j…
Browse files Browse the repository at this point in the history
…wt/jwt/v5 (#24)

* init empty jwt interface

* added utility method to generate/validate/extract jwt token

* added test code for jwt generate/validate/extract claims
  • Loading branch information
irdaislakhuafa authored Jan 15, 2024
1 parent a26d6df commit e054f7e
Show file tree
Hide file tree
Showing 4 changed files with 277 additions and 4 deletions.
79 changes: 79 additions & 0 deletions auth/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package auth

import (
"context"
"reflect"

"github.com/golang-jwt/jwt/v5"
"github.com/irdaislakhuafa/go-sdk/codes"
"github.com/irdaislakhuafa/go-sdk/errors"
)

type JWTInterface[C jwt.Claims] interface {
Generate(ctx context.Context) (string, error)
Validate(ctx context.Context, tokenString string) (*jwt.Token, error)
ExtractClaims(ctx context.Context, jwtToken *jwt.Token) (C, error)
WithSigningMethod(signingMethod jwt.SigningMethod) JWTInterface[C]
}

type jwtimpl[C jwt.Claims] struct {
secretKey []byte
claims C
signingMethod jwt.SigningMethod
}

func InitJWT[C jwt.Claims](secretKey []byte, claims C) JWTInterface[C] {
j := jwtimpl[C]{
secretKey: secretKey,
claims: claims,
signingMethod: jwt.SigningMethodHS256,
}
return &j
}

func (j *jwtimpl[C]) Generate(ctx context.Context) (string, error) {
jwtToken := jwt.NewWithClaims(j.signingMethod, j.claims)
jwtString, err := jwtToken.SignedString(j.secretKey)
if err != nil {
return "", errors.NewWithCode(codes.CodeJWTSignedStringError, "cannot signed string, %v", err.Error())
}

return jwtString, nil
}

func (j *jwtimpl[C]) Validate(ctx context.Context, tokenString string) (*jwt.Token, error) {
kind := reflect.TypeOf(j.claims).Kind()

switch kind {
case reflect.Pointer:
keyFunc := func(jwtToken *jwt.Token) (any, error) {
if _, isOk := jwtToken.Method.(*jwt.SigningMethodHMAC); !isOk {
return nil, errors.NewWithCode(codes.CodeJWTInvalidMethod, "invalid token method algoritm")
}
return j.secretKey, nil
}

jwtToken, err := jwt.ParseWithClaims(tokenString, j.claims, keyFunc)
if err != nil {
return nil, errors.NewWithCode(codes.CodeJWTParseWithClaimsError, "cannot parse token with claims, %v", err)
}

return jwtToken, nil
default:
return nil, errors.NewWithCode(codes.CodeJWTInvalidClaimsType, "claims type must be a pointer but got %v", kind.String())
}
}

func (j *jwtimpl[C]) ExtractClaims(ctx context.Context, jwtToken *jwt.Token) (C, error) {
claims, isOk := jwtToken.Claims.(C)
if !isOk {
return j.claims, errors.NewWithCode(codes.CodeJWTInvalidClaimsType, "claims type is not equals")
}

return claims, nil
}

func (j *jwtimpl[C]) WithSigningMethod(signingMethod jwt.SigningMethod) JWTInterface[C] {
j.signingMethod = signingMethod
return j
}
184 changes: 184 additions & 0 deletions auth/jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package auth

import (
"context"
"fmt"
"strings"
"testing"
"time"

"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/irdaislakhuafa/go-sdk/codes"
"github.com/irdaislakhuafa/go-sdk/errors"
"github.com/irdaislakhuafa/go-sdk/files"
)

func Test_JWT(t *testing.T) {
type Mode int

const (
MODE_GENERATE = Mode(iota + 1)
MODE_VALIDATE
MODE_EXTRACT
)

type claims struct {
UserID string
jwt.RegisteredClaims
}

type params struct {
claims claims
tokenString string
secretKey string
}

type want struct {
fn func(token string) error
}

type wantErr struct {
code codes.Code
}

type test struct {
ctx context.Context
name string
beforeFunc func(ctx context.Context, j JWTInterface[*claims], p *params)
mode Mode
params params
want want
isWantErr bool
wantErr wantErr
}

tests := []test{
{
ctx: context.Background(),
name: "generate jwt token string",
mode: MODE_GENERATE,
params: params{
claims: claims{
uuid.NewString(),
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
},
},
secretKey: "secret",
},
isWantErr: false,
want: want{
fn: func(token string) error {
if len(strings.Split(token, ".")) == 3 {
return nil
}
return errors.NewWithCode(codes.CodeJWT, "generated jwt token not valid")
},
},
wantErr: wantErr{},
},
{
ctx: context.Background(),
name: "validate jwt token string",
beforeFunc: func(ctx context.Context, j JWTInterface[*claims], p *params) {
s, _ := j.Generate(ctx)
p.tokenString = s
},
mode: MODE_VALIDATE,
params: params{
secretKey: "secret",
},
isWantErr: false,
want: want{
fn: func(token string) error {
return nil
},
},
wantErr: wantErr{},
},
{
ctx: context.Background(),
name: "extract claims jwt token string",
beforeFunc: func(ctx context.Context, j JWTInterface[*claims], p *params) {
s, _ := j.Generate(ctx)
p.tokenString = s
},
mode: MODE_EXTRACT,
params: params{
secretKey: "secret",
},
isWantErr: false,
want: want{
fn: func(token string) error {
return nil
},
},
wantErr: wantErr{},
},
}

f := files.GetCurrentMethodName()
for _, tt := range tests {
t.Run(fmt.Sprintf("%v:%v", f, tt.name), func(t *testing.T) {
jwtFunc := InitJWT([]byte(tt.params.secretKey), &tt.params.claims)

if tt.beforeFunc != nil {
tt.beforeFunc(tt.ctx, jwtFunc, &tt.params)
}

switch tt.mode {
case MODE_GENERATE:
s, err := jwtFunc.Generate(tt.ctx)
if tt.isWantErr {
if err != nil {
if code := errors.GetCode(err); code != tt.wantErr.code {
t.Fatalf("want err code is %#v but got err code %#v", tt.wantErr.code, code)
}
} else {
t.Fatalf("want err is %#v but got err %#v", tt.isWantErr, err)
}
}

if err := tt.want.fn(s); err != nil {
t.Fatalf(err.Error())
}

t.Logf("generated token: %#v", s)

case MODE_VALIDATE:
_, err := jwtFunc.Validate(tt.ctx, tt.params.tokenString)
if tt.isWantErr {
if err != nil {
if code := errors.GetCode(err); code != tt.wantErr.code {
t.Fatalf("want err code is %#v but got err code %#v", tt.wantErr.code, code)
}
} else {
t.Fatalf("want err is %#v but got err %#v", tt.isWantErr, err)
}
}
case MODE_EXTRACT:
jt, err := jwtFunc.Validate(tt.ctx, tt.params.tokenString)
if err != nil {
if code := errors.GetCode(err); code != tt.wantErr.code {
t.Fatalf("want err code is %#v but got err code %#v", tt.wantErr.code, code)
}
}

c, err := jwtFunc.ExtractClaims(tt.ctx, jt)
if tt.isWantErr {
if err != nil {
if code := errors.GetCode(err); code != tt.wantErr.code {
t.Fatalf("want err code is %#v but got err code %#v", tt.wantErr.code, code)
}
} else {
t.Fatalf("want err is %#v but got err %#v", tt.isWantErr, err)
}
}

t.Logf("claims: %#v", *c)
}
})
fmt.Println("")
}
}
11 changes: 8 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ module github.com/irdaislakhuafa/go-sdk

go 1.20

require github.com/rs/zerolog v1.31.0
require (
github.com/go-mail/gomail v2.3.1+incompatible
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
github.com/rs/zerolog v1.31.0
golang.org/x/crypto v0.17.0
)

require (
github.com/go-mail/gomail v2.3.1+incompatible // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sys v0.15.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/mail.v2 v2.3.1 // indirect
)
7 changes: 6 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/go-mail/gomail v2.3.1+incompatible h1:hPrvM8Ncd4AM/OUHssNc6vclHfPKVcV/JuIwbDkcSq8=
github.com/go-mail/gomail v2.3.1+incompatible/go.mod h1:3QIXh5Fu0sf8i95raYpBqdBzROEbW00No9d24bcbPp0=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -15,9 +19,10 @@ golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=

0 comments on commit e054f7e

Please sign in to comment.