1 package jwt 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "math" 7 "strconv" 8 "time" 9 ) 10 11 // TimePrecision sets the precision of times and dates within this library. This 12 // has an influence on the precision of times when comparing expiry or other 13 // related time fields. Furthermore, it is also the precision of times when 14 // serializing. 15 // 16 // For backwards compatibility the default precision is set to seconds, so that 17 // no fractional timestamps are generated. 18 var TimePrecision = time.Second 19 20 // MarshalSingleStringAsArray modifies the behavior of the ClaimStrings type, 21 // especially 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 25 // behavior of the underlying []string. If it is set to false, it will serialize 26 // to a single string, if it contains one element. Otherwise, it will serialize 27 // 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 // 62 // 1. Take the normal unix timestamp to form the whole number part of the 63 // output, 64 // 2. Take the result of the Nanosecond function, which returns the offset 65 // within the second of the particular unix time instance, to form the 66 // decimal part of the output 67 // 3. Concatenate them to produce the final result 68 seconds := strconv.FormatInt(truncatedDate.Unix(), 10) 69 nanosecondsOffset := strconv.FormatFloat(float64(truncatedDate.Nanosecond())/float64(time.Second), 'f', prec, 64) 70 71 output := append([]byte(seconds), []byte(nanosecondsOffset)[1:]...) 72 73 return output, nil 74 } 75 76 // UnmarshalJSON is an implementation of the json.RawMessage interface and 77 // deserializes a [NumericDate] from a JSON representation, i.e. a 78 // [json.Number]. This number represents an UNIX epoch with either integer or 79 // non-integer seconds. 80 func (date *NumericDate) UnmarshalJSON(b []byte) (err error) { 81 var ( 82 number json.Number 83 f float64 84 ) 85 86 if err = json.Unmarshal(b, &number); err != nil { 87 return fmt.Errorf("could not parse NumericData: %w", err) 88 } 89 90 if f, err = number.Float64(); err != nil { 91 return fmt.Errorf("could not convert json number value to float: %w", err) 92 } 93 94 n := newNumericDateFromSeconds(f) 95 *date = *n 96 97 return nil 98 } 99 100 // ClaimStrings is basically just a slice of strings, but it can be either 101 // serialized from a string array or just a string. This type is necessary, 102 // since the "aud" claim can either be a single string or an array. 103 type ClaimStrings []string 104 105 func (s *ClaimStrings) UnmarshalJSON(data []byte) (err error) { 106 var value interface{} 107 108 if err = json.Unmarshal(data, &value); err != nil { 109 return err 110 } 111 112 var aud []string 113 114 switch v := value.(type) { 115 case string: 116 aud = append(aud, v) 117 case []string: 118 aud = ClaimStrings(v) 119 case []interface{}: 120 for _, vv := range v { 121 vs, ok := vv.(string) 122 if !ok { 123 return ErrInvalidType 124 } 125 aud = append(aud, vs) 126 } 127 case nil: 128 return nil 129 default: 130 return ErrInvalidType 131 } 132 133 *s = aud 134 135 return 136 } 137 138 func (s ClaimStrings) MarshalJSON() (b []byte, err error) { 139 // This handles a special case in the JWT RFC. If the string array, e.g. 140 // used by the "aud" field, only contains one element, it MAY be serialized 141 // as a single string. This may or may not be desired based on the ecosystem 142 // of other JWT library used, so we make it configurable by the variable 143 // MarshalSingleStringAsArray. 144 if len(s) == 1 && !MarshalSingleStringAsArray { 145 return json.Marshal(s[0]) 146 } 147 148 return json.Marshal([]string(s)) 149 } 150