Skip to content

Commit

Permalink
update to v0.0.8
Browse files Browse the repository at this point in the history
fix #3

add a new Decode which just decodes the token in compact form without verification and validation
  • Loading branch information
kataras committed Dec 15, 2020
1 parent 933b4a7 commit 9f23c50
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 53 deletions.
30 changes: 29 additions & 1 deletion claims.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jwt

import (
"encoding/json"
"errors"
"time"
)
Expand Down Expand Up @@ -53,7 +54,33 @@ type Claims struct {
// claim is present, the party reading the data in this JWT must find itself in the aud claim or
// disregard the data contained in the JWT. As in the case of the iss and sub claims, this claim
// is application specific.
Audience []string `json:"aud,omitempty"`
Audience Audience `json:"aud,omitempty"`
}

// Audience represents the "aud" standard JWT claim.
// See the `Claims` structure for details.
type Audience []string

// UnmarshalJSON implements the json.Unmarshaler interface.
// The audience is expected to be single string an array of strings.
func (aud *Audience) UnmarshalJSON(data []byte) (err error) {
// Fixes #3.
if len(data) > 0 {
switch data[0] {
case '"': // it's a single string.
var audString string
err = json.Unmarshal(data, &audString)
if err == nil {
*aud = []string{audString}
}
case '[': // it's an array of strings.
var audStrings []string
err = json.Unmarshal(data, &audStrings)
*aud = audStrings
}
}

return
}

// Age returns the total age of the claims,
Expand Down Expand Up @@ -129,6 +156,7 @@ func (c Claims) ApplyClaims(dest *Claims) {

if v := c.Audience; len(v) > 0 {
dest.Audience = v
// dest.RawAudience, _ = json.Marshal(v) // lint: ignore
}
}

Expand Down
79 changes: 52 additions & 27 deletions token.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,30 +205,55 @@ func Base64Decode(src []byte) ([]byte, error) {
return buf[:n], err
}

/* Good idea but costs in performance, it's better
to load the original key before it's passed to the public token API.
So instead of the below, it's better to export some helper functions,
e.g. for loading key pairs from PEM files.
Now, the question is to have all those helpers in the main package?
Or:
1) create different subpackages (e.g. jwt/rsa, jwt/ecdsa, jwt/eddsa)
1.1) this introduce another question: maybe it's better to move the alg impl on their packages?
2) have them inside the main package, in the alg's source file (so they're easier to lookup),
and also have common prefix names so their API is easy visible to end-developers, e.g.
LoadPrivateKeyRSA/ECDSA/EdDSA(filename) - ParsePrivateKeyRSA/ECDSA/EdDSA(keyBytes) and
LoadPublicKeyRSA/ECDSA/EdDSA(filename) - ParsePublicKeyRSA/ECDSA/EdDSA(keyBytes) and
MustLoadRSA/ECDSA/EdDSA(privateFilename, publicFilename string) as a shortcut for the above.
^ The 2nd option was chosen.
func (key PrivateKey) parse(alg string) interface{} {
switch alg {
case HS256.Name(), HS384.Name(), HS512.Name(): // expect string or []byte
case RS256.Name(), RS384.Name(), RS512.Name(), PS256.Name(), PS384.Name(), PS512.Name():
case ES256.Name(), ES384.Name(), ES512.Name():
case EdDSA.Name():
default:
return key
}
}
*/
// Decode decodes the token of compact form WITHOUT verification and validation.
//
// This function is only useful to read a token's claims
// when the source is trusted and no algorithm verification or direct signature and
// content validation is required.
//
// Use `Verify/VerifyEncrypted` functions instead.
func Decode(token []byte) (*UnverifiedToken, error) {
parts := bytes.Split(token, sep)
if len(parts) != 3 {
return nil, ErrTokenForm
}

header := parts[0]
payload := parts[1]
signature := parts[2]

headerDecoded, err := Base64Decode(header)
if err != nil {
return nil, err
}

signatureDecoded, err := Base64Decode(signature)
if err != nil {
return nil, err
}

payload, err = Base64Decode(payload)
if err != nil {
return nil, err
}

tok := &UnverifiedToken{
Header: headerDecoded,
Payload: payload,
Signature: signatureDecoded,
}
return tok, nil
}

// UnverifiedToken contains the compact form token parts.
// Look its `Claims` method to decode to a custom structure.
type UnverifiedToken struct {
Header []byte
Payload []byte
Signature []byte
}

// Claims decodes the `Payload` field to the "dest".
func (t *UnverifiedToken) Claims(dest interface{}) error {
return Unmarshal(t.Payload, dest)
}
13 changes: 13 additions & 0 deletions token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,19 @@ func TestCompareHeader(t *testing.T) {
}
}

func TestDecodeWithoutVerify(t *testing.T) {
input := testToken
tok, err := Decode(input)
if err != nil {
t.Fatal(err)
}
expectedPayload := []byte(`{"username":"kataras"}`)

if !bytes.Equal(tok.Payload, expectedPayload) {
t.Fatalf("expected payload part to be:\n%q\\nnbut got:\n %q", expectedPayload, tok.Payload)
}
}

func BenchmarkEncodeToken(b *testing.B) {
var claims = map[string]interface{}{
"username": "kataras",
Expand Down
50 changes: 25 additions & 25 deletions verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,31 @@ func VerifyEncrypted(alg Alg, key PublicKey, decrypt InjectFunc, token []byte, v
return verifiedTok, nil
}

// VerifiedToken holds the information about a verified token.
// Look `Verify` for more.
type VerifiedToken struct {
Token []byte // The original token.
Header []byte // The header (decoded) part.
Payload []byte // The payload (decoded) part.
Signature []byte // The signature (decoded) part.
StandardClaims Claims // Any standard claims extracted from the payload.
}

// Claims decodes the token's payload to the "dest".
// If the application requires custom claims, this is the method to Go.
//
// It calls the `Unmarshal(t.Payload, dest)` package-level function .
// When called, it decodes the token's payload (aka claims)
// to the "dest" pointer of a struct or map value.
// Note that the `StandardClaims` field is always set,
// as it contains the standard JWT claims,
// and validated at the `Verify` function itself,
// therefore NO FURTHER STEP is required
// to validate the "exp", "iat" and "nbf" claims.
func (t *VerifiedToken) Claims(dest interface{}) error {
return Unmarshal(t.Payload, dest)
}

var errPayloadNotJSON = errors.New("payload is not a type of JSON") // malformed JSON or it's not a JSON at all.

// Plain can be provided as a Token Validator at `Verify` and `VerifyEncrypted` functions
Expand Down Expand Up @@ -125,28 +150,3 @@ type (
func (fn TokenValidatorFunc) ValidateToken(token []byte, standardClaims Claims, err error) error {
return fn(token, standardClaims, err)
}

// VerifiedToken holds the information about a verified token.
// Look `Verify` for more.
type VerifiedToken struct {
Token []byte // The original token.
Header []byte // The header (decoded) part.
Payload []byte // The payload (decoded) part.
Signature []byte // The signature (decoded) part.
StandardClaims Claims // Any standard claims extracted from the payload.
}

// Claims decodes the token's payload to the "dest".
// If the application requires custom claims, this is the method to Go.
//
// It calls the `Unmarshal(t.Payload, dest)` package-level function .
// When called, it decodes the token's payload (aka claims)
// to the "dest" pointer of a struct or map value.
// Note that the `StandardClaims` field is always set,
// as it contains the standard JWT claims,
// and validated at the `Verify` function itself,
// therefore NO FURTHER STEP is required
// to validate the "exp", "iat" and "nbf" claims.
func (t *VerifiedToken) Claims(dest interface{}) error {
return Unmarshal(t.Payload, dest)
}
73 changes: 73 additions & 0 deletions verify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package jwt
import (
"bytes"
"errors"
"reflect"
"testing"
)

Expand Down Expand Up @@ -52,3 +53,75 @@ func TestPlainTokenValidator(t *testing.T) {
t.Fatalf("expected raw payload to match: %q but got: %q", payload, verifiedToken.Payload)
}
}

func TestVerifyWithSingleAudienceString_CustomClaims(t *testing.T) {
type customClaims struct {
Key string `json:"key"`
Audience string `json:"aud"` // test custom struct with a single string as audience (see #3).
}

tok := customClaims{"test key", "api"}
token, err := Sign(testAlg, testSecret, tok)
if err != nil {
t.Fatal(err)
}

verifiedToken, err := Verify(testAlg, testSecret, token)
if err != nil {
t.Fatal(err)
}

var got customClaims
err = verifiedToken.Claims(&got)
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(got, tok) {
t.Fatalf("expected:\n%#+v\n\nbut got:\n%#+v", tok, got)
}
}

func TestVerifyWithSingleAudienceString_CustomClaimsAndStandard(t *testing.T) {
type customClaims struct {
Key string `json:"key"`
}

standardClaims := Claims{Audience: []string{"api"}}

custom := customClaims{"test key"}
token, err := Sign(testAlg, testSecret, custom, standardClaims)
if err != nil {
t.Fatal(err)
}

verifiedToken, err := Verify(testAlg, testSecret, token)
if err != nil {
t.Fatal(err)
}

var gotCustom customClaims
err = verifiedToken.Claims(&gotCustom)
if err != nil {
t.Fatal(err)
}

if !reflect.DeepEqual(gotCustom, custom) {
t.Fatalf("expected:\n%#+v\n\nbut got:\n%#+v", custom, gotCustom)
}

var gotStandard Claims
err = verifiedToken.Claims(&gotStandard)
if err != nil {
t.Fatal(err)
}

// here we validate the Audience.UnmarshalJSON
if !reflect.DeepEqual(gotStandard, standardClaims) {
t.Fatalf("expected:\n%#+v\n\nbut got:\n%#+v", standardClaims, gotStandard)
}

if !reflect.DeepEqual(verifiedToken.StandardClaims, standardClaims) {
t.Fatalf("expected:\n%#+v\n\nbut got:\n%#+v", standardClaims, gotStandard)
}
}

0 comments on commit 9f23c50

Please sign in to comment.