Skip to content

Commit

Permalink
feat: JWT library with flexible signers
Browse files Browse the repository at this point in the history
closes hyperledger-archives#1264

Signed-off-by: Dmitriy Kinoshenko <dkinoshenko@gmail.com>
  • Loading branch information
kdimak committed Feb 14, 2020
1 parent b071c8d commit db246de
Show file tree
Hide file tree
Showing 7 changed files with 1,817 additions and 0 deletions.
123 changes: 123 additions & 0 deletions pkg/doc/jwt/claims.go
@@ -0,0 +1,123 @@
/*
*
* * Copyright SecureKey Technologies Inc. All Rights Reserved.
* *
* * SPDX-License-Identifier: Apache-2.0
*
*/

package jwt

import (
"encoding/json"
"errors"
"strconv"
"time"
)

// Claims defines JWT Claims Set (https://tools.ietf.org/html/rfc7519#section-4)
type Claims struct {

// Issuer defines iss claim.
Issuer string `json:"iss,omitempty"`

// Subject defines sub claim.
Subject string `json:"sub,omitempty"`

// Audience defines aud claim.
Audience Audience `json:"aud,omitempty"`

// Expiry defines exp claim.
Expiry *NumericDate `json:"exp,omitempty"`

// NotBefore defines nbf claim.
NotBefore *NumericDate `json:"nbf,omitempty"`

// IssuedAt defines iat claim.
IssuedAt *NumericDate `json:"iat,omitempty"`

// ID defines jti claim.
ID string `json:"jti,omitempty"`
}

// NumericDate is a JSON numeric value representing the number of seconds from
// 1970-01-01T00:00:00Z UTC until the specified UTC date/time, ignoring leap seconds.
type NumericDate int64

// NewNumericDate creates a new NumericDate from time.Time
func NewNumericDate(t time.Time) *NumericDate {
if t.IsZero() {
return nil
}

nd := NumericDate(t.Unix())

return &nd
}

// MarshalJSON converts NumericDate to JSON bytes.
func (n NumericDate) MarshalJSON() ([]byte, error) {
return []byte(strconv.FormatInt(int64(n), 10)), nil
}

// UnmarshalJSON defines custom unmarshalling of NumericDate from JSON bytes.
func (n *NumericDate) UnmarshalJSON(data []byte) error {
s := string(data)

f, err := strconv.ParseFloat(s, 64)
if err != nil {
return errors.New("not a number value")
}

*n = NumericDate(f)

return nil
}

// Time provides time.Time value of NumericDate.
func (n NumericDate) Time() time.Time {
return time.Unix(int64(n), 0)
}

// Audience identifies the recipients that the JWT is intended for.
type Audience []string

// MarshalJSON converts Audience to JSON bytes.
func (s Audience) MarshalJSON() ([]byte, error) {
if len(s) == 1 {
return json.Marshal(s[0])
}

return json.Marshal([]string(s))
}

// UnmarshalJSON defines custom unmarshalling of Audience from JSON bytes.
func (s *Audience) UnmarshalJSON(b []byte) error {
var i interface{}

if err := json.Unmarshal(b, &i); err != nil {
return err
}

switch v := i.(type) {
case string:
*s = []string{v}
case []interface{}:
a := make([]string, len(v))

for i, e := range v {
s, ok := e.(string)
if !ok {
return errors.New("expecting string Audience item")
}

a[i] = s
}

*s = a
default:
return errors.New("expecting string or []interface{} Audience")
}

return nil
}
88 changes: 88 additions & 0 deletions pkg/doc/jwt/claims_test.go
@@ -0,0 +1,88 @@
/*
* Copyright SecureKey Technologies Inc. All Rights Reserved.
*
* SPDX-License-Identifier: Apache-2.0
*/

package jwt

import (
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/require"
)

func TestNewNumericDate(t *testing.T) {
require.NotNil(t, NewNumericDate(time.Now()))
require.Nil(t, NewNumericDate(time.Time{}))
}

func TestNumericDate_MarshalJSON(t *testing.T) {
date := NewNumericDate(time.Now())
bytes, err := json.Marshal(date)
require.NoError(t, err)
require.NotEmpty(t, bytes)
}

func TestNumericDate_UnmarshalJSON(t *testing.T) {
date := NewNumericDate(time.Now())
bytes, err := json.Marshal(date)
require.NoError(t, err)
require.NotEmpty(t, bytes)

var parsedDate NumericDate
err = json.Unmarshal(bytes, &parsedDate)
require.NoError(t, err)

err = parsedDate.UnmarshalJSON([]byte("not a number"))
require.Error(t, err)
require.EqualError(t, err, "not a number value")
}

func TestNumericDate_Time(t *testing.T) {
now := time.Unix(0, 0)
date := NewNumericDate(now)
require.True(t, now.Equal(date.Time()))
}

func TestAudience_MarshalJSON(t *testing.T) {
single := Audience{"aud"}
bytes, err := json.Marshal(single)
require.NoError(t, err)
require.Equal(t, "\"aud\"", string(bytes))

many := Audience{"aud1", "aud2"}
bytes, err = json.Marshal(many)
require.NoError(t, err)
require.Equal(t, "[\"aud1\",\"aud2\"]", string(bytes))
}

func TestAudience_UnmarshalJSON(t *testing.T) {
var aud Audience

// single
err := json.Unmarshal([]byte("\"aud\""), &aud)
require.NoError(t, err)
require.Equal(t, Audience{"aud"}, aud)

// many
err = json.Unmarshal([]byte("[\"aud1\",\"aud2\"]"), &aud)
require.NoError(t, err)
require.Equal(t, Audience{"aud1", "aud2"}, aud)

// invalid aud in many
err = json.Unmarshal([]byte("[\"aud1\",7]"), &aud)
require.Error(t, err)
require.EqualError(t, err, "expecting string Audience item")

// invalid aud
err = json.Unmarshal([]byte("7"), &aud)
require.Error(t, err)
require.EqualError(t, err, "expecting string or []interface{} Audience")

// invalid JSON
err = aud.UnmarshalJSON([]byte("invalid JSON"))
require.Error(t, err)
}

0 comments on commit db246de

Please sign in to comment.