1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
|
package types
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/lestrrat-go/jwx/v2/internal/json"
)
const (
DefaultPrecision uint32 = 0 // second level
MaxPrecision uint32 = 9 // nanosecond level
)
var Pedantic uint32
var ParsePrecision = DefaultPrecision
var FormatPrecision = DefaultPrecision
// NumericDate represents the date format used in the 'nbf' claim
type NumericDate struct {
time.Time
}
func (n *NumericDate) Get() time.Time {
if n == nil {
return (time.Time{}).UTC()
}
return n.Time
}
func intToTime(v interface{}, t *time.Time) bool {
var n int64
switch x := v.(type) {
case int64:
n = x
case int32:
n = int64(x)
case int16:
n = int64(x)
case int8:
n = int64(x)
case int:
n = int64(x)
default:
return false
}
*t = time.Unix(n, 0)
return true
}
func parseNumericString(x string) (time.Time, error) {
var t time.Time // empty time for empty return value
// Only check for the escape hatch if it's the pedantic
// flag is off
if Pedantic != 1 {
// This is an escape hatch for non-conformant providers
// that gives us RFC3339 instead of epoch time
for _, r := range x {
// 0x30 = '0', 0x39 = '9', 0x2E = '.'
if (r >= 0x30 && r <= 0x39) || r == 0x2E {
continue
}
// if it got here, then it probably isn't epoch time
tv, err := time.Parse(time.RFC3339, x)
if err != nil {
return t, fmt.Errorf(`value is not number of seconds since the epoch, and attempt to parse it as RFC3339 timestamp failed: %w`, err)
}
return tv, nil
}
}
var fractional string
whole := x
if i := strings.IndexRune(x, '.'); i > 0 {
if ParsePrecision > 0 && len(x) > i+1 {
fractional = x[i+1:] // everything after the '.'
if int(ParsePrecision) < len(fractional) {
// Remove insignificant digits
fractional = fractional[:int(ParsePrecision)]
}
// Replace missing fractional diits with zeros
for len(fractional) < int(MaxPrecision) {
fractional = fractional + "0"
}
}
whole = x[:i]
}
n, err := strconv.ParseInt(whole, 10, 64)
if err != nil {
return t, fmt.Errorf(`failed to parse whole value %q: %w`, whole, err)
}
var nsecs int64
if fractional != "" {
v, err := strconv.ParseInt(fractional, 10, 64)
if err != nil {
return t, fmt.Errorf(`failed to parse fractional value %q: %w`, fractional, err)
}
nsecs = v
}
return time.Unix(n, nsecs).UTC(), nil
}
func (n *NumericDate) Accept(v interface{}) error {
var t time.Time
switch x := v.(type) {
case float32:
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x))
if err != nil {
return fmt.Errorf(`failed to accept float32 %.9f: %w`, x, err)
}
t = tv
case float64:
tv, err := parseNumericString(fmt.Sprintf(`%.9f`, x))
if err != nil {
return fmt.Errorf(`failed to accept float32 %.9f: %w`, x, err)
}
t = tv
case json.Number:
tv, err := parseNumericString(x.String())
if err != nil {
return fmt.Errorf(`failed to accept json.Number %q: %w`, x.String(), err)
}
t = tv
case string:
tv, err := parseNumericString(x)
if err != nil {
return fmt.Errorf(`failed to accept string %q: %w`, x, err)
}
t = tv
case time.Time:
t = x
default:
if !intToTime(v, &t) {
return fmt.Errorf(`invalid type %T`, v)
}
}
n.Time = t.UTC()
return nil
}
func (n NumericDate) String() string {
if FormatPrecision == 0 {
return strconv.FormatInt(n.Unix(), 10)
}
// This is cheating, but it's better (easier) than doing floating point math
// We basically munge with strings after formatting an integer value
// for nanoseconds since epoch
s := strconv.FormatInt(n.UnixNano(), 10)
for len(s) < int(MaxPrecision) {
s = "0" + s
}
slwhole := len(s) - int(MaxPrecision)
s = s[:slwhole] + "." + s[slwhole:slwhole+int(FormatPrecision)]
if s[0] == '.' {
s = "0" + s
}
return s
}
// MarshalJSON translates from internal representation to JSON NumericDate
// See https://tools.ietf.org/html/rfc7519#page-6
func (n *NumericDate) MarshalJSON() ([]byte, error) {
if n.IsZero() {
return json.Marshal(nil)
}
return json.Marshal(n.String())
}
func (n *NumericDate) UnmarshalJSON(data []byte) error {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return fmt.Errorf(`failed to unmarshal date: %w`, err)
}
var n2 NumericDate
if err := n2.Accept(v); err != nil {
return fmt.Errorf(`invalid value for NumericDate: %w`, err)
}
*n = n2
return nil
}
|