You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

types.go 4.8KB

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