diff --git a/jwt/jwt.go b/jwt/jwt.go index 22ab8f0..ac12ad6 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "strings" + "time" "git.daebt.dev/auth/algo" ) @@ -24,6 +25,11 @@ var ( // ErrNotJWTType = errors.New("token of not JWT type") ErrAlgorithmMismatch = errors.New("token is signed by another algorithm") + // + Err_1 = errors.New("заданного владельца(ов) не существует") + Err_2 = errors.New("заданного aud не существует") + ErrExpired = errors.New("token expired") + ErrNotActivated = errors.New("token yet not activated") ) type Token struct { @@ -80,7 +86,7 @@ func (t *Token) decodeSegment(str string, val any) error { return json.Unmarshal(buf, val) } -func (t *Token) Verify(a algo.Algorithm) error { +func (t *Token) Verify(a algo.Algorithm, o ...VerifyOption) error { if a == nil { return ErrAlgorithmNil } @@ -91,6 +97,28 @@ func (t *Token) Verify(a algo.Algorithm) error { return ErrAlgorithmMismatch } + if val, err := t.Payload.GetExpirationTime(); err != nil && !errors.Is(err, ErrKeyNotExist) { + return err + } else if !errors.Is(err, ErrKeyNotExist) { + if time.Now().After(val) { + return ErrExpired + } + } + + if val, err := t.Payload.GetNotBefore(); err != nil && !errors.Is(err, ErrKeyNotExist) { + return err + } else if !errors.Is(err, ErrKeyNotExist) { + if time.Now().Before(val) { + return ErrNotActivated + } + } + + for _, f := range o { + if err := f(t); err != nil { + return err + } + } + return a.Verify(t.body, t.sign) } diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index 35ccfac..5d4f351 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -1,6 +1,7 @@ package jwt_test import ( + "errors" "fmt" "testing" "time" @@ -47,89 +48,92 @@ func TestCreate(t *testing.T) { tm := time.Now() - var data = [][]jwt.Option{ + var data = []*struct { + o []jwt.Option + v []jwt.VerifyOption + e error + }{ { - jwt.WithHeaderKeyId([]byte("019451f1-f789-72a6-836d-3bc6146ad76a")), - jwt.WithIssuer("https://git.daebt.dev"), - jwt.WithAudience("https://0.example.com"), - jwt.WithSubject("example:0"), + []jwt.Option{ + jwt.WithIssuer("https://git.daebt.dev"), + }, + nil, + nil, }, { - jwt.WithHeaderKeyId([]byte("019451f1-f789-72a6-836d-3bc6146ad76a")), - jwt.WithIssuer("https://git.daebt.dev"), - jwt.WithAudience("https://1.example.com"), - jwt.WithSubject("example:1"), - jwt.WithIssuedAt(tm), - jwt.WithExpirationTime(tm.Add(time.Hour)), + []jwt.Option{ + jwt.WithIssuer("https://git.daebt.dev"), + jwt.WithIssuedAt(tm), + jwt.WithExpirationTime(tm.Add(time.Hour)), + }, + nil, + nil, }, { - jwt.WithHeaderKeyId([]byte("019451f1-f789-72a6-2911-3bc6146ad76a")), - jwt.WithIssuer("https://git.daebt.dev"), - jwt.WithAudience("https://2.example.com"), - jwt.WithSubject("example:2"), - jwt.WithIssuedAt(tm), - jwt.WithNotBefore(tm), + []jwt.Option{ + jwt.WithIssuer("https://git.daebt.dev"), + jwt.WithIssuedAt(tm), + jwt.WithExpirationTime(tm.Add(-time.Hour)), + }, + nil, + jwt.ErrExpired, }, { - jwt.WithHeaderKeyId([]byte("019451f1-f789-72a6-2911-3bc6146ad76a")), - jwt.WithIssuer("https://git.daebt.dev"), - jwt.WithAudience("https://3.example.com"), - jwt.WithSubject("example:3"), - jwt.WithIssuedAt(tm), - jwt.WithNotBefore(tm.Add(time.Minute)), + []jwt.Option{ + jwt.WithIssuer("https://git.daebt.dev"), + jwt.WithIssuedAt(tm), + jwt.WithNotBefore(tm.Add(time.Hour)), + }, + nil, + jwt.ErrNotActivated, }, { - jwt.WithHeaderKeyId([]byte("019451f1-f789-72a6-2911-3bc6146ad76a")), - jwt.WithIssuer("https://git.daebt.dev"), - jwt.WithAudience("https://4.example.com"), - jwt.WithSubject("example:4"), - jwt.WithIssuedAt(tm), - jwt.WithNotBefore(tm.Add(time.Minute)), - jwt.WithExpirationTime(tm.Add(time.Hour)), + []jwt.Option{ + jwt.WithIssuer("https://git.daebt.dev"), + jwt.WithIssuedAt(tm), + }, + []jwt.VerifyOption{ + jwt.WithVerifyIssuer("https://git.daebt.dev"), + }, + nil, + }, + { + []jwt.Option{ + jwt.WithIssuedAt(tm), + }, + []jwt.VerifyOption{ + jwt.WithVerifyIssuer(), + }, + jwt.ErrKeyNotExist, + }, + { + []jwt.Option{ + jwt.WithIssuer("https://git.daebt.dev"), + jwt.WithIssuedAt(tm), + }, + []jwt.VerifyOption{ + jwt.WithVerifyIssuer(), + }, + nil, }, } - for _, v := range data { - // t.Run(fmt.Sprint(i), func(t *testing.T) { - val, err := jwt.New(v...).Sign(alg) - if err != nil { - t.Fatal(err.Error()) - } - // t.Log(val) - fmt.Printf(`"%s",`, val) - // }) + for i, v := range data { + t.Run(fmt.Sprint(i), func(t *testing.T) { + val, err := jwt.New(v.o...).Sign(alg) + if err != nil { + t.Fatal(err.Error()) + } + + tkn, err := jwt.Parse(val) + if err != nil { + t.Fatal(err.Error()) + } + + if err := tkn.Verify(alg, v.v...); !errors.Is(err, v.e) { + t.Fatal(err.Error()) + } + }) } } - -// func TestJwt(t *testing.T) { -// alg, err := rs.NewRS256( -// rs.WithGenerateKey(2048), -// ) -// if err != nil { -// t.Fatal(err.Error()) -// } - -// tkn := New( -// WithSubject("main-jwt-token"), -// ) - -// str, err := tkn.Sign(alg) -// if err != nil { -// t.Fatal(err.Error()) -// } - -// tkn, err = Parse(str) -// if err != nil { -// t.Fatal(err.Error()) -// } - -// if err := tkn.Verify(alg); err != nil { -// t.Fatal(err.Error()) -// } - -// tkn.Payload.Range(func(key string, val any) bool { -// t.Log(key, val) -// return true -// }) -// } diff --git a/jwt/option.go b/jwt/option.go index 9a61413..8e115ec 100644 --- a/jwt/option.go +++ b/jwt/option.go @@ -39,21 +39,22 @@ func WithAudience(aud ...string) Option { // WithExpirationTime устанавливает время истечения срока действия, по истечении которого JWT НЕ ДОЛЖЕН быть принят к обработке func WithExpirationTime(exp time.Time) Option { return func(t *Token) { - t.Payload.AppendArg("exp", exp) + + t.Payload.AppendArg("exp", exp.Unix()) } } // WithNotBefore устанавливает время, до которого JWT НЕ ДОЛЖЕН быть принят к обработке func WithNotBefore(nbf time.Time) Option { return func(t *Token) { - t.Payload.AppendArg("nbf", nbf) + t.Payload.AppendArg("nbf", nbf.Unix()) } } // WithIssuedAt устанавливает время, когда был создан JWT func WithIssuedAt(iat time.Time) Option { return func(t *Token) { - t.Payload.AppendArg("iat", iat) + t.Payload.AppendArg("iat", iat.Unix()) } } @@ -88,3 +89,50 @@ func WithHeaderKeyId(kid []byte) Option { t.Header.AppendArg("kid", base64.RawURLEncoding.EncodeToString(kid)) } } + +type VerifyOption func(*Token) error + +// WithVerifyIssuer проверяет существуют ли заданные владельцы +func WithVerifyIssuer(iss ...string) VerifyOption { + return func(t *Token) error { + val, err := t.Payload.GetIssuer() + if err != nil { + return err + } + + if len(iss) == 0 { + return nil + } + + for _, v := range iss { + if v == val { + return nil + } + } + + return Err_1 + } +} + +func WithVerifyAudience(aud ...string) VerifyOption { + return func(t *Token) error { + val, err := t.Payload.GetAudience() + if err != nil { + return err + } + + if len(aud) == 0 { + return nil + } + + for _, a := range aud { + for _, v := range val { + if a == v { + return nil + } + } + } + + return Err_2 + } +}