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