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