├── .github └── FUNDING.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── claims.go ├── claims_pub.go ├── claims_pub_test.go ├── claims_reg.go ├── claims_reg_test.go ├── claims_test.go ├── errors.go ├── example_test.go ├── go.mod ├── logo.png ├── misc.go ├── sjwt.go └── sjwt_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: brianvoe 4 | patreon: brianvoe 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - stable 5 | - master 6 | 7 | install: 8 | - go get -t -v ./... 9 | 10 | script: 11 | - go test -race -coverprofile=coverage.txt -covermode=atomic 12 | 13 | after_success: 14 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at brian@webiswhatido.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Make a pull request and submit it and ill take a look at it. Thanks! -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Brian Voelker 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![alt text](https://raw.githubusercontent.com/brianvoe/sjwt/master/logo.png) 2 | 3 | # sjwt [![Go Report Card](https://goreportcard.com/badge/github.com/brianvoe/sjwt)](https://goreportcard.com/report/github.com/brianvoe/sjwt) [![GoDoc](https://godoc.org/github.com/brianvoe/sjwt?status.svg)](https://godoc.org/github.com/brianvoe/sjwt) [![license](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/brianvoe/sjwt/master/LICENSE) 4 | 5 | Buy Me A Coffee 6 | 7 | Simple JSON Web Token - Uses HMAC SHA-256 8 | 9 | Minimalistic and efficient tool for handling JSON Web Tokens in Go applications. It offers a straightforward approach to integrating JWT for authentication and security, designed for ease of use. 10 | 11 | ## Features 12 | 13 | - **Easy JWT for Go**: Implement JWT in Go with minimal effort. 14 | - **Secure & Simple**: Reliable security features, easy to integrate. 15 | - **Open Source**: MIT licensed, open for community contributions. 16 | 17 | ## Install 18 | ```bash 19 | go get -u github.com/brianvoe/sjwt 20 | ``` 21 | 22 | ## Example 23 | ```go 24 | // Set Claims 25 | claims := sjwt.New() 26 | claims.Set("username", "billymister") 27 | claims.Set("account_id", 8675309) 28 | 29 | // Generate jwt 30 | secretKey := []byte("secret_key_here") 31 | jwt := claims.Generate(secretKey) 32 | ``` 33 | 34 | ## Example parse 35 | ```go 36 | // Parse jwt 37 | jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 38 | claims, _ := sjwt.Parse(jwt) 39 | 40 | // Get claims 41 | name, err := claims.GetStr("name") // John Doe 42 | ``` 43 | 44 | ## Example verify and validate 45 | ```go 46 | jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 47 | secretKey := []byte("secret_key_here") 48 | 49 | // Verify that the secret signature is valid 50 | hasVerified := sjwt.Verify(jwt, secretKey) 51 | 52 | // Parse jwt 53 | claims, _ := sjwt.Parse(jwt) 54 | 55 | // Validate will check(if set) Expiration At and Not Before At dates 56 | err := claims.Validate() 57 | ``` 58 | 59 | ## Example usage of registered claims 60 | ```go 61 | // Set Claims 62 | claims := sjwt.New() 63 | claims.SetTokenID() // UUID generated 64 | claims.SetSubject("Subject Title") // Subject of the token 65 | claims.SetIssuer("Google") // Issuer of the token 66 | claims.SetAudience([]string{"Google", "Facebook"}) // Audience the toke is for 67 | claims.SetIssuedAt(time.Now()) // IssuedAt in time, value is set in unix 68 | claims.SetNotBeforeAt(time.Now().Add(time.Hour * 1)) // Token valid in 1 hour 69 | claims.SetExpiresAt(time.Now().Add(time.Hour * 24)) // Token expires in 24 hours 70 | 71 | // Generate jwt 72 | secretKey := []byte("secret_key_here") 73 | jwt := claims.Generate(secretKey) 74 | ``` 75 | 76 | ## Example usage of struct to claims 77 | ```go 78 | type Info struct { 79 | Name string `json:"name"` 80 | } 81 | 82 | // Marshal your struct into claims 83 | info := Info{Name: "Billy Mister"} 84 | claims, _ := sjwt.ToClaims(info) 85 | 86 | // Generate jwt 87 | secretKey := []byte("secret_key_here") 88 | jwt := claims.Generate(secretKey) 89 | ``` 90 | 91 | ## Why? 92 | For all the times I have needed the use of a jwt, its always been a simple HMAC SHA-256 and thats normally the use of most jwt tokens. 93 | -------------------------------------------------------------------------------- /claims.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | // Claims is the main container for our body information 9 | type Claims map[string]interface{} 10 | 11 | // New will initiate a new claims 12 | func New() *Claims { 13 | return &Claims{} 14 | } 15 | 16 | // ToClaims takes in an interface and unmarshals it to claims 17 | func ToClaims(struc interface{}) (Claims, error) { 18 | strucBytes, _ := json.Marshal(struc) 19 | var claims Claims 20 | err := json.Unmarshal(strucBytes, &claims) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return claims, nil 26 | } 27 | 28 | // ToStruct takes your claims and sets value to struct 29 | func (c Claims) ToStruct(struc interface{}) error { 30 | claimsBytes, _ := json.Marshal(c) 31 | err := json.Unmarshal(claimsBytes, struc) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // Validate checks expiration and not before times 40 | func (c Claims) Validate() error { 41 | now := time.Now().Unix() 42 | 43 | // Check if not before at is set and if current time hasnt started yet 44 | if c.Has(NotBeforeAt) { 45 | nbf, _ := c.GetNotBeforeAt() 46 | if now < nbf { 47 | return ErrTokenNotYetValid 48 | } 49 | } 50 | 51 | // Check if expiration at is set and if current time is passed 52 | if c.Has(ExpiresAt) { 53 | exp, _ := c.GetExpiresAt() 54 | if now >= exp { 55 | return ErrTokenHasExpired 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | -------------------------------------------------------------------------------- /claims_pub.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // Set adds/sets a name/value to claims 9 | func (c Claims) Set(name string, value interface{}) { c[name] = value } 10 | 11 | // Del deletes a name/value from claims 12 | func (c Claims) Del(name string) { delete(c, name) } 13 | 14 | // Has will let you know whether or not a claim exists 15 | func (c Claims) Has(name string) bool { _, ok := c[name]; return ok } 16 | 17 | // Get gets claim value 18 | func (c Claims) Get(name string) (interface{}, error) { 19 | if !c.Has(name) { 20 | return nil, ErrNotFound 21 | } 22 | 23 | return c[name], nil 24 | } 25 | 26 | // GetBool will get the boolean value on the Claims 27 | func (c Claims) GetBool(name string) (bool, error) { 28 | if !c.Has(name) { 29 | return false, ErrNotFound 30 | } 31 | 32 | // Type check 33 | switch val := c[name].(type) { 34 | case string: 35 | v, _ := strconv.ParseBool(val) 36 | return v, nil 37 | case bool: 38 | return val, nil 39 | } 40 | 41 | return false, ErrClaimValueInvalid 42 | } 43 | 44 | // GetStr will get the string value on the Claims 45 | func (c Claims) GetStr(name string) (string, error) { 46 | if !c.Has(name) { 47 | return "", ErrNotFound 48 | } 49 | 50 | switch val := c[name].(type) { 51 | case float32: 52 | return strconv.FormatFloat(float64(val), 'f', -1, 32), nil 53 | case float64: 54 | return strconv.FormatFloat(val, 'f', -1, 64), nil 55 | } 56 | 57 | return fmt.Sprintf("%v", c[name]), nil 58 | } 59 | 60 | // GetInt will get the int value on the Claims 61 | func (c Claims) GetInt(name string) (int, error) { 62 | if !c.Has(name) { 63 | return 0, ErrNotFound 64 | } 65 | 66 | switch val := c[name].(type) { 67 | case string: 68 | v, err := strconv.ParseInt(val, 10, 64) 69 | if err != nil { 70 | return 0, ErrClaimValueInvalid 71 | } 72 | return int(v), nil 73 | case float32: 74 | return int(val), nil 75 | case float64: 76 | return int(val), nil 77 | case uint: 78 | return int(val), nil 79 | case uint8: 80 | return int(val), nil 81 | case uint16: 82 | return int(val), nil 83 | case uint32: 84 | return int(val), nil 85 | case uint64: 86 | return int(val), nil 87 | case int: 88 | return int(val), nil 89 | case int8: 90 | return int(val), nil 91 | case int16: 92 | return int(val), nil 93 | case int32: 94 | return int(val), nil 95 | case int64: 96 | return int(val), nil 97 | } 98 | 99 | return 0, ErrClaimValueInvalid 100 | } 101 | 102 | // GetFloat will get the float value on the Claims 103 | func (c Claims) GetFloat(name string) (float64, error) { 104 | if !c.Has(name) { 105 | return 0, ErrNotFound 106 | } 107 | 108 | switch val := c[name].(type) { 109 | case float32: 110 | return float64(val), nil 111 | case float64: 112 | return float64(val), nil 113 | case string: 114 | v, _ := strconv.ParseFloat(val, 64) 115 | return v, nil 116 | } 117 | 118 | return 0, ErrClaimValueInvalid 119 | } 120 | -------------------------------------------------------------------------------- /claims_pub_test.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import "testing" 4 | 5 | func TestClaims(t *testing.T) { 6 | claims := New() 7 | claims.Set("temp", "temp val") 8 | claims.Set("bool", true) 9 | claims.Set("stringbool", "true") 10 | claims.Set("string", "hello world") 11 | claims.Set("intstring", 8675309) 12 | claims.Set("float32string", float32(86753.09)) 13 | claims.Set("float64string", float64(86753.09)) 14 | claims.Set("int", 8675309) 15 | claims.Set("uintint", uint(8675309)) 16 | claims.Set("floatint", 86753.09) 17 | claims.Set("stringint", "8675309") 18 | claims.Set("float", 8675309.69) 19 | claims.Set("stringfloat", "8675309.69") 20 | 21 | // Check has function 22 | if !claims.Has("temp") { 23 | t.Error("temp doesnt exist when it should") 24 | } 25 | 26 | // Check normal get 27 | temp, _ := claims.Get("temp") 28 | if temp.(string) != "temp val" { 29 | t.Error("getting temp received incorrect value") 30 | } 31 | 32 | // Check deletion 33 | claims.Del("temp") 34 | if claims.Has("temp") { 35 | t.Error("temp exists when it should have been recently deleted") 36 | } 37 | 38 | // Boolean 39 | bool, _ := claims.GetBool("bool") 40 | if bool != true { 41 | t.Error("bool claim is incorrect, got: ", bool) 42 | } 43 | stringbool, _ := claims.GetBool("stringbool") 44 | if stringbool != true { 45 | t.Error("stringbool claim is incorrect, got: ", stringbool) 46 | } 47 | 48 | // String 49 | string, _ := claims.GetStr("string") 50 | if string != "hello world" { 51 | t.Error("string claim is incorrect, got: ", string) 52 | } 53 | intstring, _ := claims.GetStr("intstring") 54 | if intstring != "8675309" { 55 | t.Error("intstring claim is incorrect, got: ", intstring) 56 | } 57 | float32string, _ := claims.GetStr("float32string") 58 | if float32string != "86753.09" { 59 | t.Error("float32string claim is incorrect, got: ", float32string) 60 | } 61 | float64string, _ := claims.GetStr("float64string") 62 | if float64string != "86753.09" { 63 | t.Error("float64string claim is incorrect, got: ", float64string) 64 | } 65 | 66 | // Integer 67 | int, _ := claims.GetInt("int") 68 | if int != 8675309 { 69 | t.Error("int claim is incorrect, got: ", int) 70 | } 71 | uintint, _ := claims.GetInt("uintint") 72 | if uintint != 8675309 { 73 | t.Error("uintint claim is incorrect, got: ", uintint) 74 | } 75 | floatint, _ := claims.GetInt("floatint") 76 | if floatint != 86753 { 77 | t.Error("floatint claim is incorrect, got: ", floatint) 78 | } 79 | stringint, _ := claims.GetInt("stringint") 80 | if stringint != 8675309 { 81 | t.Error("stringint claim is incorrect, got: ", stringint) 82 | } 83 | 84 | // Float 85 | float, _ := claims.GetFloat("float") 86 | if float != 8675309.69 { 87 | t.Error("float claim is incorrect, got: ", float) 88 | } 89 | stringfloat, _ := claims.GetFloat("stringfloat") 90 | if stringfloat != 8675309.69 { 91 | t.Error("stringfloat claim is incorrect, got: ", stringfloat) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /claims_reg.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import "time" 4 | 5 | const ( 6 | // TokenID is a unique identifier for this token 7 | TokenID = "jti" 8 | 9 | // Issuer is the principal that issued the token 10 | Issuer = "iss" 11 | 12 | // Audience identifies the recipents the token is intended for 13 | Audience = "aud" 14 | 15 | // Subject is the subject of the token 16 | Subject = "sub" 17 | 18 | // IssuedAt is a timesatamp for when the token was issued 19 | IssuedAt = "iat" 20 | 21 | // ExpiresAt is a timestamp for when the token should expire 22 | ExpiresAt = "exp" 23 | 24 | // NotBeforeAt is a timestamp for which this token should not be excepted until 25 | NotBeforeAt = "nbf" 26 | ) 27 | 28 | // SetTokenID will set a random uuid v4 id 29 | func (c Claims) SetTokenID() { c[TokenID] = UUID() } 30 | 31 | // DeleteTokenID deletes token id 32 | func (c Claims) DeleteTokenID() { delete(c, TokenID) } 33 | 34 | // GetTokenID will get the id set on the Claims 35 | func (c Claims) GetTokenID() (string, error) { 36 | if !c.Has(TokenID) { 37 | return "", ErrNotFound 38 | } 39 | 40 | switch val := c[TokenID].(type) { 41 | case string: 42 | return val, nil 43 | } 44 | 45 | return "", ErrClaimValueInvalid 46 | } 47 | 48 | // SetIssuer will set a string value for the issuer 49 | func (c Claims) SetIssuer(issuer string) { c[Issuer] = issuer } 50 | 51 | // DeleteIssuer deletes issuer 52 | func (c Claims) DeleteIssuer() { delete(c, Issuer) } 53 | 54 | // GetIssuer will get the issuer set on the Claims 55 | func (c Claims) GetIssuer() (string, error) { 56 | if !c.Has(Issuer) { 57 | return "", ErrNotFound 58 | } 59 | 60 | switch val := c[Issuer].(type) { 61 | case string: 62 | return val, nil 63 | } 64 | 65 | return "", ErrClaimValueInvalid 66 | } 67 | 68 | // SetAudience will set a string value for the audience 69 | func (c Claims) SetAudience(audience []string) { c[Audience] = audience } 70 | 71 | // DeleteAudience deletes audience 72 | func (c Claims) DeleteAudience() { delete(c, Audience) } 73 | 74 | // GetAudience will get the audience set on the Claims 75 | func (c Claims) GetAudience() ([]string, error) { 76 | if !c.Has(Audience) { 77 | return []string{}, ErrNotFound 78 | } 79 | 80 | switch val := c[Audience].(type) { 81 | case []string: 82 | return val, nil 83 | } 84 | 85 | return []string{}, ErrClaimValueInvalid 86 | } 87 | 88 | // SetSubject will set a subject value 89 | func (c Claims) SetSubject(subject string) { c[Subject] = subject } 90 | 91 | // DeleteSubject deletes token id 92 | func (c Claims) DeleteSubject() { delete(c, Subject) } 93 | 94 | // GetSubject will get the subject set on the Claims 95 | func (c Claims) GetSubject() (string, error) { 96 | if !c.Has(Subject) { 97 | return "", ErrNotFound 98 | } 99 | 100 | switch val := c[Subject].(type) { 101 | case string: 102 | return val, nil 103 | } 104 | 105 | return "", ErrClaimValueInvalid 106 | } 107 | 108 | // SetIssuedAt will set an issued at timestamp in nanoseconds 109 | func (c Claims) SetIssuedAt(issuedAt time.Time) { c[IssuedAt] = issuedAt.Unix() } 110 | 111 | // DeleteIssuedAt deletes issued at 112 | func (c Claims) DeleteIssuedAt() { delete(c, IssuedAt) } 113 | 114 | // GetIssuedAt will get the issued at timestamp set on the Claims 115 | func (c Claims) GetIssuedAt() (int64, error) { 116 | if !c.Has(IssuedAt) { 117 | return 0, ErrNotFound 118 | } 119 | 120 | issuedAt, err := c.GetInt(IssuedAt) 121 | if err != nil { 122 | return 0, ErrClaimValueInvalid 123 | } 124 | 125 | return int64(issuedAt), nil 126 | } 127 | 128 | // SetExpiresAt will set an expires at timestamp in nanoseconds 129 | func (c Claims) SetExpiresAt(expiresAt time.Time) { c[ExpiresAt] = expiresAt.Unix() } 130 | 131 | // DeleteExpiresAt deletes expires at 132 | func (c Claims) DeleteExpiresAt() { delete(c, ExpiresAt) } 133 | 134 | // GetExpiresAt will get the expires at timestamp set on the Claims 135 | func (c Claims) GetExpiresAt() (int64, error) { 136 | if !c.Has(ExpiresAt) { 137 | return 0, ErrNotFound 138 | } 139 | 140 | expiresAt, err := c.GetInt(ExpiresAt) 141 | if err != nil { 142 | return 0, ErrClaimValueInvalid 143 | } 144 | 145 | return int64(expiresAt), nil 146 | } 147 | 148 | // SetNotBeforeAt will set an not before at timestamp in nanoseconds 149 | func (c Claims) SetNotBeforeAt(notbeforeAt time.Time) { c[NotBeforeAt] = notbeforeAt.Unix() } 150 | 151 | // DeleteNotBeforeAt deletes not before at 152 | func (c Claims) DeleteNotBeforeAt() { delete(c, NotBeforeAt) } 153 | 154 | // GetNotBeforeAt will get the not before at timestamp set on the Claims 155 | func (c Claims) GetNotBeforeAt() (int64, error) { 156 | if !c.Has(NotBeforeAt) { 157 | return 0, ErrNotFound 158 | } 159 | 160 | notBeforeAt, err := c.GetInt(NotBeforeAt) 161 | if err != nil { 162 | return 0, ErrClaimValueInvalid 163 | } 164 | 165 | return int64(notBeforeAt), nil 166 | } 167 | -------------------------------------------------------------------------------- /claims_reg_test.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestTokenId(t *testing.T) { 9 | claims := New() 10 | claims.SetTokenID() 11 | tokenID, _ := claims.GetTokenID() 12 | if tokenID == "" { 13 | t.Error("token id was not set") 14 | } 15 | claims.DeleteTokenID() 16 | if claims.Has(TokenID) { 17 | t.Error("token id should have been deleted") 18 | } 19 | tokenID, _ = claims.GetTokenID() 20 | if tokenID != "" { 21 | t.Error("should have gotten blank value") 22 | } 23 | } 24 | 25 | func TestIssuer(t *testing.T) { 26 | claims := New() 27 | claims.SetIssuer("Google") 28 | issuer, _ := claims.GetIssuer() 29 | if issuer != "Google" { 30 | t.Error("issuer was not set") 31 | } 32 | claims.DeleteIssuer() 33 | if claims.Has(Issuer) { 34 | t.Error("issuer should have been deleted") 35 | } 36 | issuer, _ = claims.GetIssuer() 37 | if issuer != "" { 38 | t.Error("should have gotten blank value") 39 | } 40 | } 41 | 42 | func TestAudience(t *testing.T) { 43 | claims := New() 44 | claims.SetAudience([]string{"Google", "Facebook"}) 45 | audience, _ := claims.GetAudience() 46 | if len(audience) != 2 || audience[0] != "Google" || audience[1] != "Facebook" { 47 | t.Error("audience was not set") 48 | } 49 | claims.DeleteAudience() 50 | if claims.Has(Audience) { 51 | t.Error("audience should have been deleted") 52 | } 53 | audience, _ = claims.GetAudience() 54 | if len(audience) != 0 { 55 | t.Error("should have gotten empty string array") 56 | } 57 | } 58 | 59 | func TestSubject(t *testing.T) { 60 | claims := New() 61 | claims.SetSubject("Google") 62 | subject, _ := claims.GetSubject() 63 | if subject != "Google" { 64 | t.Error("subject was not set") 65 | } 66 | claims.DeleteSubject() 67 | if claims.Has(Subject) { 68 | t.Error("subject should have been deleted") 69 | } 70 | subject, _ = claims.GetSubject() 71 | if subject != "" { 72 | t.Error("should have gotten blank value") 73 | } 74 | } 75 | 76 | func TestIssuedAt(t *testing.T) { 77 | now := time.Now() 78 | claims := New() 79 | claims.SetIssuedAt(now) 80 | issuedAt, _ := claims.GetIssuedAt() 81 | if issuedAt != now.Unix() { 82 | t.Error("issuedAt was not set") 83 | } 84 | claims.DeleteIssuedAt() 85 | if claims.Has(IssuedAt) { 86 | t.Error("issuedAt should have been deleted") 87 | } 88 | issuedAt, _ = claims.GetIssuedAt() 89 | if issuedAt != 0 { 90 | t.Error("should have gotten 0 value") 91 | } 92 | } 93 | func TestExpiresAt(t *testing.T) { 94 | now := time.Now() 95 | claims := New() 96 | claims.SetExpiresAt(now) 97 | expiresAt, _ := claims.GetExpiresAt() 98 | if expiresAt != now.Unix() { 99 | t.Error("expiresAt was not set") 100 | } 101 | claims.DeleteExpiresAt() 102 | if claims.Has(ExpiresAt) { 103 | t.Error("expiresAt should have been deleted") 104 | } 105 | expiresAt, _ = claims.GetExpiresAt() 106 | if expiresAt != 0 { 107 | t.Error("should have gotten 0 value") 108 | } 109 | } 110 | 111 | func TestNotBeforeAt(t *testing.T) { 112 | now := time.Now() 113 | claims := New() 114 | claims.SetNotBeforeAt(now) 115 | notBeforeAt, _ := claims.GetNotBeforeAt() 116 | if notBeforeAt != now.Unix() { 117 | t.Error("notBeforeAt was not set") 118 | } 119 | claims.DeleteNotBeforeAt() 120 | if claims.Has(NotBeforeAt) { 121 | t.Error("NotBeforeAt should have been deleted") 122 | } 123 | notBeforeAt, _ = claims.GetNotBeforeAt() 124 | if notBeforeAt != 0 { 125 | t.Error("should have gotten 0 value") 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /claims_test.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | type testStruc struct { 9 | FirstName string `json:"first_name"` 10 | LastName string `json:"last_name"` 11 | } 12 | 13 | func TestToClaims(t *testing.T) { 14 | test := testStruc{ 15 | FirstName: "Billy", 16 | LastName: "Mister", 17 | } 18 | 19 | claims, err := ToClaims(test) 20 | if err != nil { 21 | t.Error("Error ToClaims: ", err) 22 | } 23 | if !claims.Has("first_name") { 24 | t.Error("Tried to get claim from struct. Non found") 25 | } 26 | 27 | } 28 | 29 | func TestToStruct(t *testing.T) { 30 | claims := New() 31 | claims.Set("first_name", "Billy") 32 | claims.Set("last_name", "Mister") 33 | 34 | // Try to set claims into struct 35 | var test testStruc 36 | claims.ToStruct(&test) 37 | 38 | if test.FirstName != "Billy" { 39 | t.Error("Tried to get first name from test struct after running ToStruct and it failed") 40 | } 41 | } 42 | 43 | func TestValidate(t *testing.T) { 44 | // Validate just the claim 45 | claims := New() 46 | claims.SetIssuedAt(time.Now()) 47 | claims.SetNotBeforeAt(time.Now()) 48 | claims.SetExpiresAt(time.Now().Add(time.Hour)) 49 | err := claims.Validate() 50 | if err != nil { 51 | t.Error("Validate was not successful when it should be") 52 | } 53 | 54 | // Validate on parsed claims 55 | token := claims.Generate([]byte(secretKey)) 56 | parsedClaims, err := Parse(token) 57 | err = parsedClaims.Validate() 58 | if err != nil { 59 | t.Error("Validate was not successful on parsed claims when it should be") 60 | } 61 | } 62 | 63 | func TestValidateExp(t *testing.T) { 64 | // Succes 65 | claims := New() 66 | claims.SetExpiresAt(time.Now().Add(time.Hour)) 67 | err := claims.Validate() 68 | if err != nil { 69 | t.Error("Validate was not successful when it should be") 70 | } 71 | 72 | // Error 73 | claims.SetExpiresAt(time.Now().Add(time.Hour * -1)) 74 | err = claims.Validate() 75 | if err != ErrTokenHasExpired { 76 | t.Error("Token should have expired") 77 | } 78 | } 79 | 80 | func TestValidateNotBefore(t *testing.T) { 81 | // Succes 82 | claims := New() 83 | claims.SetNotBeforeAt(time.Now()) 84 | err := claims.Validate() 85 | if err != nil { 86 | t.Error("Validate was not successful when it should be") 87 | } 88 | 89 | // Error 90 | claims.SetNotBeforeAt(time.Now().Add(time.Hour)) 91 | err = claims.Validate() 92 | if err != ErrTokenNotYetValid { 93 | t.Error("Token should have failed due to token not being valid yet") 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrNotFound is an error string clarifying 7 | // that the attempted key does not exist in the claims 8 | ErrNotFound = errors.New("Claim key not found in claims") 9 | 10 | // ErrClaimValueInvalid is an error string clarifying 11 | // that the attempt to retrieve a value could not be properly converted 12 | ErrClaimValueInvalid = errors.New("Claim value invalid") 13 | 14 | // ErrTokenInvalid is an error string clarifying 15 | // the provided token is an invalid format 16 | ErrTokenInvalid = errors.New("Token is invalid") 17 | 18 | // ErrTokenHasExpired is an error string clarifying 19 | // the current unix timestamp has exceed the exp unix timestamp 20 | ErrTokenHasExpired = errors.New("Token has expired") 21 | 22 | // ErrTokenNotYetValid is an error string clarifying 23 | // the current unix timestamp has not exceeded the nbf unix timestamp 24 | ErrTokenNotYetValid = errors.New("Token is not yet valid") 25 | ) 26 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func Example() { 9 | // Add Claims 10 | claims := New() 11 | claims.Set("username", "billymister") 12 | claims.Set("account_id", 8675309) 13 | 14 | // Generate jwt 15 | secretKey := []byte("secret_key_here") 16 | jwt := claims.Generate(secretKey) 17 | fmt.Println(jwt) 18 | } 19 | 20 | func Example_parse() { 21 | // Parse jwt 22 | jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 23 | claims, _ := Parse(jwt) 24 | 25 | // Get claims 26 | name, _ := claims.GetStr("name") 27 | fmt.Println(name) 28 | // Output: John Doe 29 | } 30 | 31 | func Example_registeredClaims() { 32 | // Add Claims 33 | claims := New() 34 | claims.SetTokenID() // UUID generated 35 | claims.SetSubject("Subject Title") // Subject of the token 36 | claims.SetIssuer("Google") // Issuer of the token 37 | claims.SetAudience([]string{"Google", "Facebook"}) // Audience the toke is for 38 | claims.SetIssuedAt(time.Now()) // IssuedAt in time, value is set in unix 39 | claims.SetNotBeforeAt(time.Now().Add(time.Hour * 1)) // Token valid in 1 hour 40 | claims.SetExpiresAt(time.Now().Add(time.Hour * 24)) // Token expires in 24 hours 41 | 42 | // Generate jwt 43 | secretKey := []byte("secret_key_here") 44 | jwt := claims.Generate(secretKey) 45 | fmt.Println(jwt) 46 | } 47 | 48 | func Example_publicClaims() { 49 | // Add Claims 50 | claims := New() 51 | claims.Set("username", "billymister") 52 | claims.Set("account_id", 8675309) 53 | 54 | // Generate jwt 55 | secretKey := []byte("secret_key_here") 56 | jwt := claims.Generate(secretKey) 57 | fmt.Println(jwt) 58 | } 59 | 60 | func Example_structToClaims() { 61 | type Info struct { 62 | Name string `json:"name"` 63 | } 64 | 65 | // Marshal your struct into claims 66 | info := Info{Name: "Billy Mister"} 67 | claims, _ := ToClaims(info) 68 | 69 | // Generate jwt 70 | secretKey := []byte("secret_key_here") 71 | jwt := claims.Generate(secretKey) 72 | fmt.Println(jwt) 73 | // output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQmlsbHkgTWlzdGVyIn0.2FYrpCNy1tg_4UvimpSrgAy-nT9snh-l4w9VLz71b6Y 74 | } 75 | 76 | func Example_claimsToStruct() { 77 | type Info struct { 78 | Name string `json:"name"` 79 | } 80 | 81 | // Parse jwt 82 | jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQmlsbHkgTWlzdGVyIn0.2FYrpCNy1tg_4UvimpSrgAy-nT9snh-l4w9VLz71b6Y" 83 | claims, _ := Parse(jwt) 84 | 85 | // Marshal your struct into claims 86 | info := Info{} 87 | claims.ToStruct(&info) 88 | 89 | name, _ := claims.GetStr("name") 90 | fmt.Println(name) 91 | // output: Billy Mister 92 | } 93 | 94 | func Example_verifySignature() { 95 | secretKey := []byte("secret_key_here") 96 | jwt := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiQmlsbHkgTWlzdGVyIn0.2FYrpCNy1tg_4UvimpSrgAy-nT9snh-l4w9VLz71b6Y" 97 | 98 | // Pass jwt and secret key to verify 99 | verified := Verify(jwt, secretKey) 100 | fmt.Println(verified) 101 | // output: true 102 | } 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/brianvoe/sjwt 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianvoe/sjwt/341266c92f557d1f60ef31555f5b240d811ac03b/logo.png -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | ) 7 | 8 | // UUID (version 4) will generate a random unique identifier based upon random nunbers 9 | // Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 10 | func UUID() string { 11 | version := byte(4) 12 | uuid := make([]byte, 16) 13 | rand.Read(uuid) 14 | 15 | // Set version 16 | uuid[6] = (uuid[6] & 0x0f) | (version << 4) 17 | 18 | // Set variant 19 | uuid[8] = (uuid[8] & 0xbf) | 0x80 20 | 21 | buf := make([]byte, 36) 22 | var dash byte = '-' 23 | hex.Encode(buf[0:8], uuid[0:4]) 24 | buf[8] = dash 25 | hex.Encode(buf[9:13], uuid[4:6]) 26 | buf[13] = dash 27 | hex.Encode(buf[14:18], uuid[6:8]) 28 | buf[18] = dash 29 | hex.Encode(buf[19:23], uuid[8:10]) 30 | buf[23] = dash 31 | hex.Encode(buf[24:], uuid[10:]) 32 | 33 | return string(buf) 34 | } 35 | -------------------------------------------------------------------------------- /sjwt.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "strings" 10 | ) 11 | 12 | // Generate takes in claims and a secret and outputs jwt token 13 | func (c Claims) Generate(secret []byte) string { 14 | // Encode header and claims 15 | headerEnc, _ := json.Marshal(map[string]string{"typ": "JWT", "alg": "HS256"}) 16 | claimsEnc, _ := json.Marshal(c) 17 | jwtStr := fmt.Sprintf( 18 | "%s.%s", 19 | base64.RawURLEncoding.EncodeToString(headerEnc), 20 | base64.RawURLEncoding.EncodeToString(claimsEnc), 21 | ) 22 | 23 | // Sign with sha 256 24 | mac := hmac.New(sha256.New, secret) 25 | mac.Write([]byte(jwtStr)) 26 | 27 | return fmt.Sprintf("%s.%s", jwtStr, base64.RawURLEncoding.EncodeToString(mac.Sum(nil))) 28 | } 29 | 30 | // Parse will take in the token string grab the body and unmarshal into claims interface 31 | func Parse(tokenStr string) (Claims, error) { 32 | tokenArray := strings.Split(tokenStr, ".") 33 | 34 | // Make sure token array contains 3 parts 35 | if len(tokenArray) != 3 { 36 | return nil, ErrTokenInvalid 37 | } 38 | 39 | claimsByte, err := base64.RawURLEncoding.DecodeString(tokenArray[1]) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | var claims Claims 45 | err = json.Unmarshal(claimsByte, &claims) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return claims, nil 51 | } 52 | 53 | // Verify will take in the token string and secret and identify the signature matches 54 | func Verify(tokenStr string, secret []byte) bool { 55 | token := strings.Split(tokenStr, ".") 56 | if len(token) != 3 { 57 | return false 58 | } 59 | mac := hmac.New(sha256.New, secret) 60 | mac.Write([]byte(fmt.Sprintf("%s.%s", token[0], token[1]))) 61 | sig, _ := base64.RawURLEncoding.DecodeString(token[2]) 62 | return hmac.Equal(sig, mac.Sum(nil)) 63 | } 64 | -------------------------------------------------------------------------------- /sjwt_test.go: -------------------------------------------------------------------------------- 1 | package sjwt 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var secretKey = []byte("whats up yall") 8 | 9 | func TestGenerate(t *testing.T) { 10 | claims := New() 11 | claims.Set("hello", "world") 12 | jwt := claims.Generate(secretKey) 13 | if jwt == "" { 14 | t.Error("jwt is empty") 15 | } 16 | } 17 | 18 | func BenchmarkGenerate(b *testing.B) { 19 | for i := 0; i < b.N; i++ { 20 | claims := New() 21 | claims.Set("hello", "world") 22 | claims.Generate(secretKey) 23 | } 24 | } 25 | 26 | func TestParse(t *testing.T) { 27 | claims := New() 28 | claims.Set("hello", "world") 29 | jwt := claims.Generate(secretKey) 30 | 31 | newClaims, err := Parse(jwt) 32 | if err != nil { 33 | t.Error("error parsing claims") 34 | } 35 | if !newClaims.Has("hello") { 36 | t.Error("error getting claims hello from parsed claims") 37 | } 38 | 39 | hello, _ := newClaims.GetStr("hello") 40 | if hello != "world" { 41 | t.Error("error hello does not equal world") 42 | } 43 | } 44 | 45 | func TestParseEmpty(t *testing.T) { 46 | _, err := Parse("") 47 | if err != ErrTokenInvalid { 48 | t.Error("error should have failed to parse empty jwt") 49 | } 50 | } 51 | 52 | func TestParseDecodeError(t *testing.T) { 53 | _, err := Parse("..") 54 | if err == nil { 55 | t.Error("error should have failed to parse empty jwt") 56 | } 57 | } 58 | 59 | func TestVerify(t *testing.T) { 60 | claims := New() 61 | claims.Set("hello", "world") 62 | jwt := claims.Generate(secretKey) 63 | 64 | verified := Verify(jwt, secretKey) 65 | if !verified { 66 | t.Error("verification failed") 67 | } 68 | 69 | verified = Verify(jwt, []byte("Bad secret")) 70 | if verified { 71 | t.Error("verification should have failed") 72 | } 73 | } 74 | 75 | func TestVerifyError(t *testing.T) { 76 | jwt := "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9." + 77 | "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + 78 | "uk1qJnGuGHHGFw6fXpVILrdo52JqyD3EzvW3_DxhgZPAqU-OKzzPy7xdRNeQRba5CI6VGmlo6DBYqRCteiiOTw" 79 | 80 | verified := Verify(jwt, secretKey) 81 | if verified { 82 | t.Error("verification should have failed") 83 | } 84 | } 85 | 86 | func TestVerifyInvalidJWTError(t *testing.T) { 87 | jwt := "not_a_jwt" 88 | 89 | verified := Verify(jwt, secretKey) 90 | if verified { 91 | t.Error("verification should have failed") 92 | } 93 | } 94 | --------------------------------------------------------------------------------