1package jwt 2 3import ( 4 "encoding/json" 5 "fmt" 6 "reflect" 7 "strconv" 8 "time" 9) 10 11// TimePrecision sets the precision of times and dates within this library. 12// This has an influence on the precision of times when comparing expiry or 13// other related time fields. Furthermore, it is also the precision of times 14// when serializing. 15// 16// For backwards compatibility the default precision is set to seconds, so that 17// no fractional timestamps are generated. 18var TimePrecision = time.Second 19 20// MarshalSingleStringAsArray modifies the behaviour of the ClaimStrings type, especially 21// its MarshalJSON function. 22// 23// If it is set to true (the default), it will always serialize the type as an 24// array of strings, even if it just contains one element, defaulting to the behaviour 25// of the underlying []string. If it is set to false, it will serialize to a single 26// string, if it contains one element. Otherwise, it will serialize to an array of strings. 27var MarshalSingleStringAsArray = true 28 29// NumericDate represents a JSON numeric date value, as referenced at 30// https://datatracker.ietf.org/doc/html/rfc7519#section-2. 31type NumericDate struct { 32 time.Time 33} 34 35// NewNumericDate constructs a new *NumericDate from a standard library time.Time struct. 36// It will truncate the timestamp according to the precision specified in TimePrecision. 37func NewNumericDate(t time.Time) *NumericDate { 38 return &NumericDate{t.Truncate(TimePrecision)} 39} 40 41// newNumericDateFromSeconds creates a new *NumericDate out of a float64 representing a 42// UNIX epoch with the float fraction representing non-integer seconds. 43func newNumericDateFromSeconds(f float64) *NumericDate { 44 return NewNumericDate(time.Unix(0, int64(f*float64(time.Second)))) 45} 46 47// MarshalJSON is an implementation of the json.RawMessage interface and serializes the UNIX epoch 48// represented in NumericDate to a byte array, using the precision specified in TimePrecision. 49func (date NumericDate) MarshalJSON() (b []byte, err error) { 50 f := float64(date.Truncate(TimePrecision).UnixNano()) / float64(time.Second) 51 52 return []byte(strconv.FormatFloat(f, 'f', -1, 64)), nil 53} 54 55// UnmarshalJSON is an implementation of the json.RawMessage interface and deserializses a 56// NumericDate from a JSON representation, i.e. a json.Number. This number represents an UNIX epoch 57// with either integer or non-integer seconds. 58func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { 59 var ( 60 number json.Number 61 f float64 62 ) 63 64 if err = json.Unmarshal(b, &number); err != nil { 65 return fmt.Errorf("could not parse NumericData: %w", err) 66 } 67 68 if f, err = number.Float64(); err != nil { 69 return fmt.Errorf("could not convert json number value to float: %w", err) 70 } 71 72 n := newNumericDateFromSeconds(f) 73 *date = *n 74 75 return nil 76} 77 78// ClaimStrings is basically just a slice of strings, but it can be either serialized from a string array or just a string. 79// This type is necessary, since the "aud" claim can either be a single string or an array. 80type ClaimStrings []string 81 82func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) { 83 var value interface{} 84 85 if err = json.Unmarshal(data, &value); err != nil { 86 return err 87 } 88 89 var aud []string 90 91 switch v := value.(type) { 92 case string: 93 aud = append(aud, v) 94 case []string: 95 aud = ClaimStrings(v) 96 case []interface{}: 97 for _, vv := range v { 98 vs, ok := vv.(string) 99 if !ok { 100 return &json.UnsupportedTypeError{Type: reflect.TypeOf(vv)} 101 } 102 aud = append(aud, vs) 103 } 104 case nil: 105 return nil 106 default: 107 return &json.UnsupportedTypeError{Type: reflect.TypeOf(v)} 108 } 109 110 *s = aud 111 112 return 113} 114 115func (s ClaimStrings) MarshalJSON() (b []byte, err error) { 116 // This handles a special case in the JWT RFC. If the string array, e.g. used by the "aud" field, 117 // only contains one element, it MAY be serialized as a single string. This may or may not be 118 // desired based on the ecosystem of other JWT library used, so we make it configurable by the 119 // variable MarshalSingleStringAsArray. 120 if len(s) == 1 && !MarshalSingleStringAsArray { 121 return json.Marshal(s[0]) 122 } 123 124 return json.Marshal([]string(s)) 125} 126