commit 9f706be828b0103c27afdd60d7ac16aef9165709 Author: Darko Luketic Date: Sun Nov 14 15:05:55 2021 +0100 initial split from 04756eee65e20001b3a44af6249b60c52af446b7 diff --git a/README.md b/README.md new file mode 100644 index 0000000..2679131 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# oidc + +This package is split off from [go-oidc-middleware](https://github.com/XenitAB/go-oidc-middleware) +to provide a re-usable package. diff --git a/cty.go b/cty.go new file mode 100644 index 0000000..f4289eb --- /dev/null +++ b/cty.go @@ -0,0 +1,191 @@ +package oidc + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +func getCtyValueWithImpliedType(a interface{}) (cty.Value, error) { + if a == nil { + return cty.NilVal, fmt.Errorf("input is nil") + } + + valueType, err := gocty.ImpliedType(a) + if err != nil { + return cty.NilVal, fmt.Errorf("unable to get cty.Type: %w", err) + } + + return getCtyValueWithType(a, valueType) +} + +func getCtyValueWithType(a interface{}, vt cty.Type) (cty.Value, error) { + if a == nil { + return cty.NilVal, fmt.Errorf("input value is nil") + } + + if vt == cty.NilType { + return cty.NilVal, fmt.Errorf("input type is nil") + } + + value, err := gocty.ToCtyValue(a, vt) + if err != nil { + // we should never receive this error + return cty.NilVal, fmt.Errorf("unable to get cty.Value: %w", err) + } + + return value, nil +} + +func getCtyValues(a interface{}, b interface{}) (cty.Value, cty.Value, error) { + first, err := getCtyValueWithImpliedType(a) + if err != nil { + return cty.NilVal, cty.NilVal, err + } + + second, err := getCtyValueWithType(b, first.Type()) + if err != nil { + return cty.NilVal, cty.NilVal, err + } + + return first, second, nil +} + +func isCtyPrimitiveValueValid(a cty.Value, b cty.Value) bool { + if !isCtyTypeSame(a, b) { + return false + } + + if getCtyType(a) != primitiveCtyType { + return false + } + + return a.Equals(b) == cty.True +} + +func isCtyListValid(a cty.Value, b cty.Value) bool { + if !isCtyTypeSame(a, b) { + return false + } + + if getCtyType(a) != listCtyType { + return false + } + + listA := a.AsValueSlice() + listB := b.AsValueSlice() + + for i := range listA { + if !ctyListContains(listB, listA[i]) { + return false + } + } + + return true +} + +func isCtyMapValid(a cty.Value, b cty.Value) bool { + if !isCtyTypeSame(a, b) { + return false + } + + if getCtyType(a) != mapCtyType { + return false + } + + mapA := a.AsValueMap() + mapB := b.AsValueMap() + + for k := range mapA { + mapBValue, ok := mapB[k] + if !ok { + return false + } + + err := isCtyValueValid(mapA[k], mapBValue) + if err != nil { + return false + } + } + + return true +} + +func ctyListContains(a []cty.Value, b cty.Value) bool { + for i := range a { + err := isCtyValueValid(a[i], b) + if err == nil { + return true + } + } + + return false +} + +func isCtyTypeSame(a cty.Value, b cty.Value) bool { + return a.Type().Equals(b.Type()) +} + +func isCtyValueValid(a cty.Value, b cty.Value) error { + if !isCtyTypeSame(a, b) { + return fmt.Errorf("should be type %s, was type: %s", a.Type().GoString(), b.Type().GoString()) + } + + switch getCtyType(a) { + case primitiveCtyType: + valid := isCtyPrimitiveValueValid(a, b) + if !valid { + return fmt.Errorf("should be %s, was: %s", a.GoString(), b.GoString()) + } + case listCtyType: + valid := isCtyListValid(a, b) + if !valid { + return fmt.Errorf("should contain %s, received: %s", a.GoString(), b.GoString()) + } + case mapCtyType: + valid := isCtyMapValid(a, b) + if !valid { + return fmt.Errorf("should contain %s, received: %s", a.GoString(), b.GoString()) + } + default: + return fmt.Errorf("non-implemented type - should be %s, received: %s", a.GoString(), b.GoString()) + } + + return nil +} + +type ctyType int + +const ( + unknownCtyType = iota + primitiveCtyType + listCtyType + mapCtyType +) + +func getCtyType(a cty.Value) ctyType { + if a.Type().IsPrimitiveType() { + return primitiveCtyType + } + + switch { + case a.Type().IsListType(): + return listCtyType + + // Adding the other cases to make it easier in the + // future to build logic for more types. + case a.Type().IsMapType(): + return mapCtyType + case a.Type().IsSetType(): + return unknownCtyType + case a.Type().IsObjectType(): + return unknownCtyType + case a.Type().IsTupleType(): + return unknownCtyType + case a.Type().IsCapsuleType(): + return unknownCtyType + } + + return unknownCtyType +} diff --git a/cty_test.go b/cty_test.go new file mode 100644 index 0000000..5160067 --- /dev/null +++ b/cty_test.go @@ -0,0 +1,642 @@ +package oidc + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" +) + +func TestGetCtyValueWithImpliedType(t *testing.T) { + cases := []struct { + testDescription string + input interface{} + expectedCtyType cty.Type + expectedError bool + }{ + { + testDescription: "string as cty.String", + input: "foo", + expectedCtyType: cty.String, + expectedError: false, + }, + { + testDescription: "string number as cty.String", + input: "1234", + expectedCtyType: cty.String, + expectedError: false, + }, + { + testDescription: "int as cty.Number", + input: int(1234), + expectedCtyType: cty.Number, + expectedError: false, + }, + { + testDescription: "float64 as cty.Number", + input: float64(1234), + expectedCtyType: cty.Number, + expectedError: false, + }, + { + testDescription: "list of strings as cty.List(cty.String)", + input: []string{"foo"}, + expectedCtyType: cty.List(cty.String), + expectedError: false, + }, + { + testDescription: "string map as cty.Map(cty.String)", + input: map[string]string{"foo": "bar"}, + expectedCtyType: cty.Map(cty.String), + expectedError: false, + }, + { + testDescription: "empty array of interfaces as cty.NilType and error", + input: []interface{}{}, + expectedCtyType: cty.NilType, + expectedError: true, + }, + { + testDescription: "nil as cty.NilType and error", + input: nil, + expectedCtyType: cty.NilType, + expectedError: true, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + v, err := getCtyValueWithImpliedType(c.input) + require.Equal(t, c.expectedCtyType, v.Type()) + + if c.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } +} + +func TestGetCtyValueWithType(t *testing.T) { + cases := []struct { + testDescription string + input interface{} + inputType cty.Type + expectedCtyType cty.Type + expectedError bool + }{ + { + testDescription: "string as cty.String", + input: "foo", + inputType: cty.String, + expectedCtyType: cty.String, + expectedError: false, + }, + { + testDescription: "string number as cty.String", + input: "1234", + inputType: cty.String, + expectedCtyType: cty.String, + expectedError: false, + }, + { + testDescription: "int as cty.Number", + input: int(1234), + inputType: cty.Number, + expectedCtyType: cty.Number, + expectedError: false, + }, + { + testDescription: "float64 as cty.Number", + input: float64(1234), + inputType: cty.Number, + expectedCtyType: cty.Number, + expectedError: false, + }, + { + testDescription: "list of strings as cty.List(cty.String)", + input: []string{"foo"}, + inputType: cty.List(cty.String), + expectedCtyType: cty.List(cty.String), + expectedError: false, + }, + { + testDescription: "string map as cty.Map(cty.String)", + input: map[string]string{"foo": "bar"}, + inputType: cty.Map(cty.String), + expectedCtyType: cty.Map(cty.String), + expectedError: false, + }, + { + testDescription: "empty array of interfaces as cty.NilType and error", + input: []interface{}{}, + inputType: cty.NilType, + expectedCtyType: cty.NilType, + expectedError: true, + }, + { + testDescription: "nil as cty.NilType and error", + input: nil, + inputType: cty.NilType, + expectedCtyType: cty.NilType, + expectedError: true, + }, + { + testDescription: "interface list in an interface map", + input: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": []interface{}{ + "uno", + "dos", + "tres", + }, + }, + }, + inputType: cty.Map(cty.Map(cty.List(cty.String))), + expectedCtyType: cty.Map(cty.Map(cty.List(cty.String))), + expectedError: false, + }, + { + testDescription: "interface list in an interface map with wrong input type", + input: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": []interface{}{ + "uno", + "dos", + "tres", + }, + }, + }, + inputType: cty.String, + expectedCtyType: cty.NilType, + expectedError: true, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + v, err := getCtyValueWithType(c.input, c.inputType) + require.Equal(t, c.expectedCtyType, v.Type()) + + if c.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } +} + +func TestGetCtyValues(t *testing.T) { + var a, b interface{} + + a = "foo" + b = "bar" + + ctyA, ctyB, err := getCtyValues(a, b) + + require.NoError(t, err) + require.Equal(t, "cty.StringVal(\"foo\")", ctyA.GoString()) + require.Equal(t, "cty.StringVal(\"bar\")", ctyB.GoString()) +} + +func TestIsCtyPrimitiveValueValid(t *testing.T) { + cases := []struct { + testDescription string + firstValue cty.Value + secondValue cty.Value + expectedResult bool + }{ + { + testDescription: "same input strings", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("foo"), + expectedResult: true, + }, + { + testDescription: "same input numbers", + firstValue: cty.NumberIntVal(1337), + secondValue: cty.NumberIntVal(1337), + expectedResult: true, + }, + { + testDescription: "different input strings", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("bar"), + expectedResult: false, + }, + { + testDescription: "different input numbers", + firstValue: cty.NumberIntVal(1337), + secondValue: cty.NumberIntVal(7331), + expectedResult: false, + }, + { + testDescription: "different types", + firstValue: cty.StringVal("bar"), + secondValue: cty.NumberIntVal(7331), + expectedResult: false, + }, + { + testDescription: "input list", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + expectedResult: false, + }, + { + testDescription: "input map", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + result := isCtyPrimitiveValueValid(c.firstValue, c.secondValue) + require.Equal(t, c.expectedResult, result) + } +} + +func TestIsCtyListValid(t *testing.T) { + cases := []struct { + testDescription string + firstValue cty.Value + secondValue cty.Value + expectedResult bool + }{ + { + testDescription: "same input string", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + expectedResult: true, + }, + { + testDescription: "same input int", + firstValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + secondValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + expectedResult: true, + }, + { + testDescription: "different input string", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("bar")}), + expectedResult: false, + }, + { + testDescription: "different input int", + firstValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + secondValue: cty.ListVal([]cty.Value{cty.NumberIntVal(7331)}), + expectedResult: false, + }, + { + testDescription: "same input multiple second", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("bar")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar"), cty.StringVal("baz")}), + expectedResult: true, + }, + { + testDescription: "input string", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("foo"), + expectedResult: false, + }, + { + testDescription: "same input map", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + expectedResult: false, + }, + { + testDescription: "different types", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + result := isCtyListValid(c.firstValue, c.secondValue) + require.Equal(t, c.expectedResult, result) + } +} + +func TestIsCtyMapValid(t *testing.T) { + cases := []struct { + testDescription string + firstValue cty.Value + secondValue cty.Value + expectedResult bool + }{ + { + testDescription: "same input string", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + expectedResult: true, + }, + { + testDescription: "same input int", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + expectedResult: true, + }, + { + testDescription: "different input string", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("bar")}), + expectedResult: false, + }, + { + testDescription: "different input int", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(7331)}), + expectedResult: false, + }, + { + testDescription: "different types", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + expectedResult: false, + }, + { + testDescription: "input string", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("foo"), + expectedResult: false, + }, + { + testDescription: "input list", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + expectedResult: false, + }, + { + testDescription: "same input multiple second", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"a": cty.StringVal("b"), "foo": cty.StringVal("foo"), "c": cty.StringVal("d")}), + expectedResult: true, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + result := isCtyMapValid(c.firstValue, c.secondValue) + require.Equal(t, c.expectedResult, result) + } +} + +func TestCtyListContains(t *testing.T) { + cases := []struct { + testDescription string + slice []cty.Value + value cty.Value + expectedResult bool + }{ + { + testDescription: "same input string", + slice: []cty.Value{cty.StringVal("foo")}, + value: cty.StringVal("foo"), + expectedResult: true, + }, + { + testDescription: "same input int", + slice: []cty.Value{cty.NumberIntVal(1337)}, + value: cty.NumberIntVal(1337), + expectedResult: true, + }, + { + testDescription: "different input string", + slice: []cty.Value{cty.StringVal("foo")}, + value: cty.StringVal("bar"), + expectedResult: false, + }, + { + testDescription: "different input int", + slice: []cty.Value{cty.NumberIntVal(1337)}, + value: cty.NumberIntVal(7331), + expectedResult: false, + }, + { + testDescription: "same input string multiple", + slice: []cty.Value{cty.StringVal("foo"), cty.StringVal("bar"), cty.StringVal("baz")}, + value: cty.StringVal("bar"), + expectedResult: true, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + result := ctyListContains(c.slice, c.value) + require.Equal(t, c.expectedResult, result) + } +} + +func TestIsCtyTypeSame(t *testing.T) { + cases := []struct { + testDescription string + firstValue cty.Value + secondValue cty.Value + expectedResult bool + }{ + { + testDescription: "same input strings", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("foo"), + expectedResult: true, + }, + { + testDescription: "same input numbers", + firstValue: cty.NumberIntVal(1337), + secondValue: cty.NumberIntVal(1337), + expectedResult: true, + }, + { + testDescription: "different input strings", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("bar"), + expectedResult: true, + }, + { + testDescription: "different input numbers", + firstValue: cty.NumberIntVal(1337), + secondValue: cty.NumberIntVal(7331), + expectedResult: true, + }, + { + testDescription: "different types", + firstValue: cty.StringVal("foo"), + secondValue: cty.NumberIntVal(1337), + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + result := isCtyTypeSame(c.firstValue, c.secondValue) + require.Equal(t, c.expectedResult, result) + } +} + +func TestIsCtyValueValid(t *testing.T) { + cases := []struct { + testDescription string + firstValue cty.Value + secondValue cty.Value + expectedError bool + }{ + { + testDescription: "same input strings", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("foo"), + expectedError: false, + }, + { + testDescription: "same input numbers", + firstValue: cty.NumberIntVal(1337), + secondValue: cty.NumberIntVal(1337), + expectedError: false, + }, + { + testDescription: "different input strings", + firstValue: cty.StringVal("foo"), + secondValue: cty.StringVal("bar"), + expectedError: true, + }, + { + testDescription: "different input numbers", + firstValue: cty.NumberIntVal(1337), + secondValue: cty.NumberIntVal(7331), + expectedError: true, + }, + { + testDescription: "different types", + firstValue: cty.StringVal("foo"), + secondValue: cty.NumberIntVal(1337), + expectedError: true, + }, + { + testDescription: "same input list string", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + expectedError: false, + }, + { + testDescription: "same input list int", + firstValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + secondValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + expectedError: false, + }, + { + testDescription: "different input list string", + firstValue: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.ListVal([]cty.Value{cty.StringVal("bar")}), + expectedError: true, + }, + { + testDescription: "different input list int", + firstValue: cty.ListVal([]cty.Value{cty.NumberIntVal(1337)}), + secondValue: cty.ListVal([]cty.Value{cty.NumberIntVal(7331)}), + expectedError: true, + }, + { + testDescription: "same input map string", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + expectedError: false, + }, + { + testDescription: "same input map int", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + expectedError: false, + }, + { + testDescription: "different input map string", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("bar")}), + expectedError: true, + }, + { + testDescription: "different input map int", + firstValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1337)}), + secondValue: cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(7331)}), + expectedError: true, + }, + { + testDescription: "non-imlemented type", + firstValue: cty.SetVal([]cty.Value{cty.StringVal("foo")}), + secondValue: cty.SetVal([]cty.Value{cty.StringVal("foo")}), + expectedError: true, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + err := isCtyValueValid(c.firstValue, c.secondValue) + + if c.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + } +} + +func TestGetCtyType(t *testing.T) { + cases := []struct { + testDescription string + input cty.Value + expectedType ctyType + }{ + { + testDescription: "string is primitiveCtyType", + input: cty.StringVal("foo"), + expectedType: primitiveCtyType, + }, + { + testDescription: "int is primitiveCtyType", + input: cty.NumberIntVal(1337), + expectedType: primitiveCtyType, + }, + { + testDescription: "float is primitiveCtyType", + input: cty.NumberFloatVal(1337), + expectedType: primitiveCtyType, + }, + { + testDescription: "bool is primitiveCtyType", + input: cty.BoolVal(true), + expectedType: primitiveCtyType, + }, + { + testDescription: "slice is listCtyType", + input: cty.ListVal([]cty.Value{cty.StringVal("foo")}), + expectedType: listCtyType, + }, + { + testDescription: "map is mapCtyType", + input: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("foo")}), + expectedType: mapCtyType, + }, + { + testDescription: "set is unknownCtyType", + input: cty.SetVal([]cty.Value{cty.StringVal("foo")}), + expectedType: unknownCtyType, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + resultType := getCtyType(c.input) + require.Equal(t, c.expectedType, resultType) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..37554e9 --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +module git.icod.de/dalu/oidc + +go 1.17 + +require ( + github.com/lestrrat-go/jwx v1.2.11 + github.com/stretchr/testify v1.7.0 + github.com/xenitab/dispans v0.0.10 + github.com/zclconf/go-cty v1.10.0 + go.uber.org/ratelimit v0.2.0 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c +) + +require ( + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/go-oauth2/oauth2/v4 v4.4.2 // indirect + github.com/go-session/session v3.1.2+incompatible // indirect + github.com/goccy/go-json v0.7.10 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.0 // indirect + github.com/lestrrat-go/iter v1.0.1 // indirect + github.com/lestrrat-go/option v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/tidwall/btree v0.6.1 // indirect + github.com/tidwall/buntdb v1.2.7 // indirect + github.com/tidwall/gjson v1.11.0 // indirect + github.com/tidwall/grect v0.1.3 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/rtred v0.1.2 // indirect + github.com/tidwall/tinyqueue v0.1.1 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/net v0.0.0-20211104170005-ce137452f963 // indirect + golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a8d7f1a --- /dev/null +++ b/go.sum @@ -0,0 +1,548 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-oauth2/oauth2/v4 v4.4.2 h1:tWQlR5I4/qhWiyOME67BAFmo622yi+2mm7DMm8DpMdg= +github.com/go-oauth2/oauth2/v4 v4.4.2/go.mod h1:K4DemYzNwwYnIDOPdHtX/7SlO0AHdtlphsTgE7lA3PA= +github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= +github.com/goccy/go-json v0.7.10 h1:ulhbuNe1JqE68nMRXXTJRrUu0uhouf0VevLINxQq4Ec= +github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= +github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.11 h1:e9BS5NQ003hxXogNsgf5fEWf01ZJvj4Aj1qy7Dykqm8= +github.com/lestrrat-go/jwx v1.2.11/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= +github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/btree v0.6.1 h1:75VVgBeviiDO+3g4U+7+BaNBNhNINxB0ULPT3fs9pMY= +github.com/tidwall/btree v0.6.1/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= +github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= +github.com/tidwall/buntdb v1.2.7 h1:SIyObKAymzLyGhDeIhVk2Yc1/EwfCC75Uyu77CHlVoA= +github.com/tidwall/buntdb v1.2.7/go.mod h1:b6KvZM27x/8JLI5hgRhRu60pa3q0Tz9c50TyD46OHUM= +github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.11.0 h1:C16pk7tQNiH6VlCrtIXL1w8GaOsi1X3W8KDkE1BuYd4= +github.com/tidwall/gjson v1.11.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= +github.com/tidwall/grect v0.1.3 h1:z9YwQAMUxVSBde3b7Sl8Da37rffgNfZ6Fq6h9t6KdXE= +github.com/tidwall/grect v0.1.3/go.mod h1:8GMjwh3gPZVpLBI/jDz9uslCe0dpxRpWDdtN0lWAS/E= +github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= +github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= +github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= +github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= +github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.14.0 h1:67bfuW9azCMwW/Jlq/C+VeihNpAuJMWkYPBig1gdi3A= +github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xenitab/dispans v0.0.10 h1:S+gSUM14rDJWK7MYNrjb8JbjeQPip6mlNJyLX+g7Agc= +github.com/xenitab/dispans v0.0.10/go.mod h1:CqxCO5jE4j2+6BK7z3pofaVTenZfRMqWtUAR+ucbi58= +github.com/xenitab/pkg v0.0.3 h1:1hFU9GWxXgKhqvZFvoVznz5j63OlM5Fl3UacEhvPKdU= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= +github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20211104170005-ce137452f963 h1:8gJUadZl+kWvZBqG/LautX0X6qe5qTC2VI/3V3NBRAY= +golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/keyhandler.go b/keyhandler.go new file mode 100644 index 0000000..2cb7cef --- /dev/null +++ b/keyhandler.go @@ -0,0 +1,167 @@ +package oidc + +import ( + "context" + "fmt" + "net/http" + "sync" + "time" + + "github.com/lestrrat-go/jwx/jwk" + "go.uber.org/ratelimit" + "golang.org/x/sync/semaphore" +) + +type keyHandler struct { + sync.RWMutex + jwksURI string + disableKeyID bool + keySet jwk.Set + fetchTimeout time.Duration + keyUpdateSemaphore *semaphore.Weighted + keyUpdateChannel chan keyUpdate + keyUpdateCount int + keyUpdateLimiter ratelimit.Limiter + httpClient *http.Client +} + +type keyUpdate struct { + keySet jwk.Set + err error +} + +func newKeyHandler(httpClient *http.Client, jwksUri string, fetchTimeout time.Duration, keyUpdateRPS uint, disableKeyID bool) (*keyHandler, error) { + h := &keyHandler{ + jwksURI: jwksUri, + disableKeyID: disableKeyID, + fetchTimeout: fetchTimeout, + keyUpdateSemaphore: semaphore.NewWeighted(int64(1)), + keyUpdateChannel: make(chan keyUpdate), + keyUpdateLimiter: ratelimit.New(int(keyUpdateRPS)), + httpClient: httpClient, + } + + ctx := context.Background() + + _, err := h.updateKeySet(ctx) + if err != nil { + return nil, err + } + + return h, nil +} + +func (h *keyHandler) updateKeySet(ctx context.Context) (jwk.Set, error) { + ctx, cancel := context.WithTimeout(ctx, h.fetchTimeout) + defer cancel() + keySet, err := jwk.Fetch(ctx, h.jwksURI, jwk.WithHTTPClient(h.httpClient)) + if err != nil { + return nil, fmt.Errorf("unable to fetch keys from %q: %w", h.jwksURI, err) + } + + if h.disableKeyID && keySet.Len() != 1 { + return nil, fmt.Errorf("keyID is disabled, but received a keySet with more than one key: %d", keySet.Len()) + } + + h.Lock() + h.keySet = keySet + h.keyUpdateCount++ + h.Unlock() + + return keySet, nil +} + +// waitForUpdateKeySetSet handles concurrent requests to update the jwks as well as rate limiting. +func (h *keyHandler) waitForUpdateKeySetAndGetKeySet(ctx context.Context) (jwk.Set, error) { + // ok will be false if there's already an update in progress. + ok := h.keyUpdateSemaphore.TryAcquire(1) + if ok { + defer h.keyUpdateSemaphore.Release(1) + _ = h.keyUpdateLimiter.Take() + keySet, err := h.updateKeySet(ctx) + + result := keyUpdate{ + keySet, + err, + } + + // start go routine to handle all requests waiting for result. + go func(res keyUpdate) { + // for each request waiting for update, send result to them. + for { + select { + case h.keyUpdateChannel <- res: + default: + return + } + } + }(result) + + return keySet, err + } + + // wait for the request that is updating keys and return the result from it + result := <-h.keyUpdateChannel + return result.keySet, result.err +} + +func (h *keyHandler) waitForUpdateKeySetAndGetKey(ctx context.Context) (jwk.Key, error) { + keySet, err := h.waitForUpdateKeySetAndGetKeySet(ctx) + if err != nil { + return nil, err + } + + key, found := keySet.Get(0) + if !found { + return nil, fmt.Errorf("no key found") + } + + return key, nil +} + +func (h *keyHandler) getKey(ctx context.Context, keyID string) (jwk.Key, error) { + if h.disableKeyID { + return h.getKeyWithoutKeyID() + } + + return h.getKeyFromID(ctx, keyID) +} + +func (h *keyHandler) getKeySet() jwk.Set { + h.RLock() + defer h.RUnlock() + return h.keySet +} + +func (h *keyHandler) getKeyFromID(ctx context.Context, keyID string) (jwk.Key, error) { + keySet := h.getKeySet() + + key, found := keySet.LookupKeyID(keyID) + + if !found { + updatedKeySet, err := h.waitForUpdateKeySetAndGetKeySet(ctx) + if err != nil { + return nil, fmt.Errorf("unable to update key set for key %q: %w", keyID, err) + } + + updatedKey, found := updatedKeySet.LookupKeyID(keyID) + if !found { + return nil, fmt.Errorf("unable to find key %q", keyID) + } + + return updatedKey, nil + } + + return key, nil +} + +func (h *keyHandler) getKeyWithoutKeyID() (jwk.Key, error) { + keySet := h.getKeySet() + + key, found := keySet.Get(0) + if !found { + return nil, fmt.Errorf("no key found") + } + + return key, nil +} diff --git a/keyhandler_test.go b/keyhandler_test.go new file mode 100644 index 0000000..aab407b --- /dev/null +++ b/keyhandler_test.go @@ -0,0 +1,339 @@ +package oidc + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/lestrrat-go/jwx/jwk" + "github.com/stretchr/testify/require" + "github.com/xenitab/dispans/server" +) + +func TestNewKeyHandler(t *testing.T) { + ctx := context.Background() + + op := server.NewTesting(t) + issuer := op.GetURL(t) + discoveryUri := GetDiscoveryUriFromIssuer(issuer) + jwksUri, err := getJwksUriFromDiscoveryUri(http.DefaultClient, discoveryUri, 10*time.Millisecond) + require.NoError(t, err) + + keyHandler, err := newKeyHandler(http.DefaultClient, jwksUri, 10*time.Millisecond, 100, false) + require.NoError(t, err) + + keySet1 := keyHandler.getKeySet() + require.Equal(t, 1, keySet1.Len()) + + expectedKey1, ok := keySet1.Get(0) + require.True(t, ok) + + token1 := op.GetToken(t) + keyID1, err := getKeyIDFromTokenString(token1.AccessToken) + require.NoError(t, err) + + // Test valid key id + key1, err := keyHandler.getKeyFromID(ctx, keyID1) + require.NoError(t, err) + require.Equal(t, expectedKey1, key1) + + // Test invalid key id + _, err = keyHandler.getKeyFromID(ctx, "foo") + require.Error(t, err) + + // Test with rotated keys + op.RotateKeys(t) + + token2 := op.GetToken(t) + keyID2, err := getKeyIDFromTokenString(token2.AccessToken) + require.NoError(t, err) + + key2, err := keyHandler.getKeyFromID(ctx, keyID2) + require.NoError(t, err) + + keySet2 := keyHandler.getKeySet() + require.Equal(t, 1, keySet2.Len()) + + expectedKey2, ok := keySet2.Get(0) + require.True(t, ok) + + require.Equal(t, expectedKey2, key2) + + // Test that old key doesn't match new key + require.NotEqual(t, key1, key2) + + // Validate that error is returned when using fake jwks uri + _, err = newKeyHandler(http.DefaultClient, "http://foo.bar/baz", 10*time.Millisecond, 100, false) + require.Error(t, err) + + // Validate that error is returned when keys are rotated, + // new token with new key and jwks uri isn't accessible + op.RotateKeys(t) + token3 := op.GetToken(t) + keyID3, err := getKeyIDFromTokenString(token3.AccessToken) + require.NoError(t, err) + op.Close(t) + _, err = keyHandler.getKeyFromID(ctx, keyID3) + require.Error(t, err) +} + +func TestUpdate(t *testing.T) { + ctx := context.Background() + + op := server.NewTesting(t) + issuer := op.GetURL(t) + discoveryUri := GetDiscoveryUriFromIssuer(issuer) + jwksUri, err := getJwksUriFromDiscoveryUri(http.DefaultClient, discoveryUri, 10*time.Millisecond) + require.NoError(t, err) + + rateLimit := uint(10) + keyHandler, err := newKeyHandler(http.DefaultClient, jwksUri, 10*time.Millisecond, rateLimit, false) + require.NoError(t, err) + + require.Equal(t, 1, keyHandler.keyUpdateCount) + + _, err = keyHandler.waitForUpdateKeySetAndGetKeySet(ctx) + require.NoError(t, err) + + require.Equal(t, 2, keyHandler.keyUpdateCount) + + concurrentUpdate := func(workers int) { + wg1 := sync.WaitGroup{} + wg1.Add(1) + + wg2 := sync.WaitGroup{} + for i := 0; i < workers; i++ { + wg2.Add(1) + go func() { + wg1.Wait() + _, err := keyHandler.waitForUpdateKeySetAndGetKeySet(ctx) + require.NoError(t, err) + wg2.Done() + }() + } + wg1.Done() + wg2.Wait() + } + + concurrentUpdate(100) + require.Equal(t, 3, keyHandler.keyUpdateCount) + concurrentUpdate(100) + require.Equal(t, 4, keyHandler.keyUpdateCount) + concurrentUpdate(100) + require.Equal(t, 5, keyHandler.keyUpdateCount) + + multipleConcurrentUpdates := func() { + wg1 := sync.WaitGroup{} + wg1.Add(1) + + wg2 := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg2.Add(1) + go func() { + wg1.Wait() + concurrentUpdate(10) + wg2.Done() + }() + } + wg1.Done() + wg2.Wait() + } + + multipleConcurrentUpdates() + require.Equal(t, 6, keyHandler.keyUpdateCount) + + // test rate limit + time.Sleep(10 * time.Millisecond) + start := time.Now() + _, err = keyHandler.waitForUpdateKeySetAndGetKeySet(ctx) + require.NoError(t, err) + stop := time.Now() + expectedStop := start.Add(time.Second / time.Duration(rateLimit)) + + require.WithinDuration(t, expectedStop, stop, 20*time.Millisecond) + + require.Equal(t, 7, keyHandler.keyUpdateCount) +} + +func TestNewKeyHandlerWithKeyIDDisabled(t *testing.T) { + disableKeyID := true + keySets := testNewTestKeySet(t) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + _, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + _, err = newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.Error(t, err) +} + +func TestNewKeyHandlerWithKeyIDEnabled(t *testing.T) { + disableKeyID := false + keySets := testNewTestKeySet(t) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + _, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + _, err = newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) +} + +func TestUpdateKeySetWithKeyIDDisabled(t *testing.T) { + ctx := context.Background() + + disableKeyID := true + keySets := testNewTestKeySet(t) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keyHandler, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + _, err = keyHandler.updateKeySet(ctx) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + _, err = keyHandler.updateKeySet(ctx) + require.Error(t, err) +} + +func TestUpdateKeySetWithKeyIDEnabled(t *testing.T) { + ctx := context.Background() + + disableKeyID := false + keySets := testNewTestKeySet(t) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keyHandler, err := newKeyHandler(http.DefaultClient, testServer.URL, 100*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + _, err = keyHandler.updateKeySet(ctx) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + _, err = keyHandler.updateKeySet(ctx) + require.NoError(t, err) +} + +func TestWaitForUpdateKeySetWithKeyIDDisabled(t *testing.T) { + ctx := context.Background() + + disableKeyID := true + keySets := testNewTestKeySet(t) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keyHandler, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + _, err = keyHandler.waitForUpdateKeySetAndGetKey(ctx) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + _, err = keyHandler.waitForUpdateKeySetAndGetKey(ctx) + require.Error(t, err) +} + +func TestWaitForUpdateKeySetWithKeyIDEnabled(t *testing.T) { + ctx := context.Background() + + disableKeyID := false + keySets := testNewTestKeySet(t) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keyHandler, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + _, err = keyHandler.waitForUpdateKeySetAndGetKey(ctx) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + _, err = keyHandler.waitForUpdateKeySetAndGetKey(ctx) + require.NoError(t, err) +} + +func testNewJwksServer(t *testing.T, keySets *testKeySets) *httptest.Server { + t.Helper() + + testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(keySets.publicKeySet) + require.NoError(t, err) + })) + + return testServer +} + +type testKeySets struct { + privateKeySet jwk.Set + publicKeySet jwk.Set +} + +func testNewTestKeySet(t *testing.T) *testKeySets { + t.Helper() + + return &testKeySets{} +} + +func (k *testKeySets) setKeys(privKeySet jwk.Set, pubKeySet jwk.Set) { + k.privateKeySet = privKeySet + k.publicKeySet = pubKeySet +} + +func testNewKeySet(t *testing.T, numKeys int, disableKeyID bool) (jwk.Set, jwk.Set) { + t.Helper() + + privKeySet := jwk.NewSet() + pubKeySet := jwk.NewSet() + for i := 0; i < numKeys; i++ { + privKey, pubKey := testNewKey(t) + + if disableKeyID { + err := privKey.Remove(jwk.KeyIDKey) + require.NoError(t, err) + + err = pubKey.Remove(jwk.KeyIDKey) + require.NoError(t, err) + } + + privKeySet.Add(privKey) + pubKeySet.Add(pubKey) + } + + return privKeySet, pubKeySet +} diff --git a/oidc.go b/oidc.go new file mode 100644 index 0000000..a0d94ff --- /dev/null +++ b/oidc.go @@ -0,0 +1,391 @@ +package oidc + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "git.icod.de/dalu/oidc/options" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" + "github.com/lestrrat-go/jwx/jwt" +) + +var ( + errSignatureVerification = fmt.Errorf("failed to verify signature") +) + +type handler struct { + issuer string + discoveryUri string + jwksUri string + jwksFetchTimeout time.Duration + jwksRateLimit uint + fallbackSignatureAlgorithm jwa.SignatureAlgorithm + allowedTokenDrift time.Duration + requiredAudience string + requiredTokenType string + requiredClaims map[string]interface{} + disableKeyID bool + httpClient *http.Client + + keyHandler *keyHandler +} + +func NewHandler(setters ...options.Option) (*handler, error) { + opts := options.New(setters...) + + h := &handler{ + issuer: opts.Issuer, + discoveryUri: opts.DiscoveryUri, + jwksUri: opts.JwksUri, + jwksFetchTimeout: opts.JwksFetchTimeout, + jwksRateLimit: opts.JwksRateLimit, + allowedTokenDrift: opts.AllowedTokenDrift, + requiredTokenType: opts.RequiredTokenType, + requiredAudience: opts.RequiredAudience, + requiredClaims: opts.RequiredClaims, + disableKeyID: opts.DisableKeyID, + httpClient: opts.HttpClient, + } + + if h.issuer == "" { + return nil, fmt.Errorf("issuer is empty") + } + if h.discoveryUri == "" { + h.discoveryUri = GetDiscoveryUriFromIssuer(h.issuer) + } + if opts.FallbackSignatureAlgorithm != "" { + alg, err := getSignatureAlgorithmFromString(opts.FallbackSignatureAlgorithm) + if err != nil { + return nil, fmt.Errorf("FallbackSignatureAlgorithm not accepted: %w", err) + } + + h.fallbackSignatureAlgorithm = alg + } + if !opts.LazyLoadJwks { + err := h.loadJwks() + if err != nil { + return nil, fmt.Errorf("unable to load jwks: %w", err) + } + } + + return h, nil +} + +func (h *handler) loadJwks() error { + if h.jwksUri == "" { + jwksUri, err := getJwksUriFromDiscoveryUri(h.httpClient, h.discoveryUri, 5*time.Second) + if err != nil { + return fmt.Errorf("unable to fetch jwksUri from discoveryUri (%s): %w", h.discoveryUri, err) + } + h.jwksUri = jwksUri + } + + keyHandler, err := newKeyHandler(h.httpClient, h.jwksUri, h.jwksFetchTimeout, h.jwksRateLimit, h.disableKeyID) + if err != nil { + return fmt.Errorf("unable to initialize keyHandler: %w", err) + } + + h.keyHandler = keyHandler + + return nil +} + +func (h *handler) SetIssuer(issuer string) { + h.issuer = issuer +} + +func (h *handler) SetDiscoveryUri(discoveryUri string) { + h.discoveryUri = discoveryUri +} + +type ParseTokenFunc func(ctx context.Context, tokenString string) (jwt.Token, error) + +func (h *handler) ParseToken(ctx context.Context, tokenString string) (jwt.Token, error) { + if h.keyHandler == nil { + err := h.loadJwks() + if err != nil { + return nil, fmt.Errorf("unable to load jwks: %w", err) + } + } + + tokenTypeValid := isTokenTypeValid(h.requiredTokenType, tokenString) + if !tokenTypeValid { + return nil, fmt.Errorf("token type %q required", h.requiredTokenType) + } + + keyID := "" + if !h.disableKeyID { + var err error + keyID, err = getKeyIDFromTokenString(tokenString) + if err != nil { + return nil, err + } + } + + key, err := h.keyHandler.getKey(ctx, keyID) + if err != nil { + return nil, fmt.Errorf("unable to get public key: %w", err) + } + + alg, err := getSignatureAlgorithm(key.KeyType(), key.Algorithm(), h.fallbackSignatureAlgorithm) + if err != nil { + return nil, err + } + + token, err := getAndValidateTokenFromString(tokenString, key, alg) + if err != nil { + if h.disableKeyID && errors.Is(err, errSignatureVerification) { + updatedKey, err := h.keyHandler.waitForUpdateKeySetAndGetKey(ctx) + if err != nil { + return nil, err + } + + alg, err := getSignatureAlgorithm(key.KeyType(), key.Algorithm(), h.fallbackSignatureAlgorithm) + if err != nil { + return nil, err + } + + token, err = getAndValidateTokenFromString(tokenString, updatedKey, alg) + if err != nil { + return nil, err + } + } else { + return nil, err + } + } + + validExpiration := isTokenExpirationValid(token.Expiration(), h.allowedTokenDrift) + if !validExpiration { + return nil, fmt.Errorf("token has expired: %s", token.Expiration()) + } + + validIssuer := isTokenIssuerValid(h.issuer, token.Issuer()) + if !validIssuer { + return nil, fmt.Errorf("required issuer %q was not found, received: %s", h.issuer, token.Issuer()) + } + + validAudience := isTokenAudienceValid(h.requiredAudience, token.Audience()) + if !validAudience { + return nil, fmt.Errorf("required audience %q was not found, received: %v", h.requiredAudience, token.Audience()) + } + + if h.requiredClaims != nil { + tokenClaims, err := token.AsMap(ctx) + if err != nil { + return nil, fmt.Errorf("unable to get token claims: %w", err) + } + + err = isRequiredClaimsValid(h.requiredClaims, tokenClaims) + if err != nil { + return nil, fmt.Errorf("unable to validate required claims: %w", err) + } + } + + return token, nil +} + +func GetDiscoveryUriFromIssuer(issuer string) string { + return fmt.Sprintf("%s/.well-known/openid-configuration", strings.TrimSuffix(issuer, "/")) +} + +func getJwksUriFromDiscoveryUri(httpClient *http.Client, discoveryUri string, fetchTimeout time.Duration) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, discoveryUri, nil) + if err != nil { + return "", err + } + + req.Header.Set("Accept", "application/json") + + res, err := httpClient.Do(req) + if err != nil { + return "", err + } + + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + err = res.Body.Close() + if err != nil { + return "", err + } + + var discoveryData struct { + JwksUri string `json:"jwks_uri"` + } + + err = json.Unmarshal(bodyBytes, &discoveryData) + if err != nil { + return "", err + } + + if discoveryData.JwksUri == "" { + return "", fmt.Errorf("JwksUri is empty") + } + + return discoveryData.JwksUri, nil +} + +func getKeyIDFromTokenString(tokenString string) (string, error) { + headers, err := getHeadersFromTokenString(tokenString) + if err != nil { + return "", err + } + + keyID := headers.KeyID() + if keyID == "" { + return "", fmt.Errorf("token header does not contain key id (kid)") + } + + return keyID, nil +} + +func getTokenTypeFromTokenString(tokenString string) (string, error) { + headers, err := getHeadersFromTokenString(tokenString) + if err != nil { + return "", err + } + + tokenType := headers.Type() + if tokenType == "" { + return "", fmt.Errorf("token header does not contain type (typ)") + } + + return tokenType, nil +} + +func getHeadersFromTokenString(tokenString string) (jws.Headers, error) { + msg, err := jws.ParseString(tokenString) + if err != nil { + return nil, fmt.Errorf("unable to parse tokenString: %w", err) + } + + signatures := msg.Signatures() + if len(signatures) != 1 { + return nil, fmt.Errorf("more than one signature in token") + } + + headers := signatures[0].ProtectedHeaders() + + return headers, nil +} + +func isTokenAudienceValid(requiredAudience string, audiences []string) bool { + if requiredAudience == "" { + return true + } + + for _, audience := range audiences { + if audience == requiredAudience { + return true + } + } + + return false +} + +func isTokenExpirationValid(expiration time.Time, allowedDrift time.Duration) bool { + expirationWithAllowedDrift := expiration.Round(0).Add(allowedDrift) + + return expirationWithAllowedDrift.After(time.Now()) +} + +func isTokenIssuerValid(requiredIssuer string, tokenIssuer string) bool { + if requiredIssuer == "" { + return false + } + + return tokenIssuer == requiredIssuer +} + +func isTokenTypeValid(requiredTokenType string, tokenString string) bool { + if requiredTokenType == "" { + return true + } + + tokenType, err := getTokenTypeFromTokenString(tokenString) + if err != nil { + return false + } + + if tokenType != requiredTokenType { + return false + } + + return true +} + +func isRequiredClaimsValid(requiredClaims map[string]interface{}, tokenClaims map[string]interface{}) error { + for requiredKey, requiredValue := range requiredClaims { + tokenValue, ok := tokenClaims[requiredKey] + if !ok { + return fmt.Errorf("token does not have the claim: %s", requiredKey) + } + + required, received, err := getCtyValues(requiredValue, tokenValue) + if err != nil { + return err + } + + err = isCtyValueValid(required, received) + if err != nil { + return fmt.Errorf("claim %q not valid: %w", requiredKey, err) + } + } + + return nil +} + +func getAndValidateTokenFromString(tokenString string, key jwk.Key, alg jwa.SignatureAlgorithm) (jwt.Token, error) { + token, err := jwt.ParseString(tokenString, jwt.WithVerify(alg, key)) + if err != nil { + if strings.Contains(err.Error(), errSignatureVerification.Error()) { + return nil, errSignatureVerification + } + + return nil, err + } + + return token, nil +} + +func getSignatureAlgorithm(kty jwa.KeyType, keyAlg string, fallbackAlg jwa.SignatureAlgorithm) (jwa.SignatureAlgorithm, error) { + if keyAlg != "" { + return getSignatureAlgorithmFromString(keyAlg) + } + + if fallbackAlg != "" { + return fallbackAlg, nil + } + + switch kty { + case jwa.RSA: + return jwa.RS256, nil + case jwa.EC: + return jwa.ES256, nil + default: + return "", fmt.Errorf("unable to get signature algorithm with kty=%s, alg=%s, fallbackAlg=%s", kty, keyAlg, fallbackAlg) + } +} + +func getSignatureAlgorithmFromString(s string) (jwa.SignatureAlgorithm, error) { + var alg jwa.SignatureAlgorithm + err := alg.Accept(s) + if err != nil { + return "", err + } + + return alg, nil +} diff --git a/oidc_test.go b/oidc_test.go new file mode 100644 index 0000000..4326bdf --- /dev/null +++ b/oidc_test.go @@ -0,0 +1,1484 @@ +package oidc + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "fmt" + "net/http" + "testing" + "time" + + "git.icod.de/dalu/oidc/options" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwk" + "github.com/lestrrat-go/jwx/jws" + "github.com/lestrrat-go/jwx/jwt" + "github.com/stretchr/testify/require" + "github.com/xenitab/dispans/server" +) + +func TestGetHeadersFromTokenString(t *testing.T) { + key, _ := testNewKey(t) + + // Test with KeyID and Type + token1 := jwt.New() + err := token1.Set("foo", "bar") + require.NoError(t, err) + + headers1 := jws.NewHeaders() + err = headers1.Set(jws.TypeKey, "JWT") + require.NoError(t, err) + + signedTokenBytes1, err := jwt.Sign(token1, jwa.ES384, key, jwt.WithHeaders(headers1)) + require.NoError(t, err) + + signedToken1 := string(signedTokenBytes1) + parsedHeaders1, err := getHeadersFromTokenString(signedToken1) + require.NoError(t, err) + + require.Equal(t, key.KeyID(), parsedHeaders1.KeyID()) + require.Equal(t, headers1.Type(), parsedHeaders1.Type()) + + // Test with empty headers + payload1 := `{"foo":"bar"}` + + headers2 := jws.NewHeaders() + + signedTokenBytes2, err := jws.Sign([]byte(payload1), jwa.ES384, key, jws.WithHeaders(headers2)) + require.NoError(t, err) + + signedToken2 := string(signedTokenBytes2) + parsedHeaders2, err := getHeadersFromTokenString(signedToken2) + require.NoError(t, err) + + require.Empty(t, parsedHeaders2.Type()) + + // Test with multiple signatures + payload2 := `{"foo":"bar"}` + + signer1, err := jws.NewSigner(jwa.ES384) + require.NoError(t, err) + signer2, err := jws.NewSigner(jwa.ES384) + require.NoError(t, err) + + signedTokenBytes3, err := jws.SignMulti([]byte(payload2), jws.WithSigner(signer1, key, nil, nil), jws.WithSigner(signer2, key, nil, nil)) + require.NoError(t, err) + + signedToken3 := string(signedTokenBytes3) + + _, err = getHeadersFromTokenString(signedToken3) + require.Error(t, err) + require.Equal(t, "more than one signature in token", err.Error()) + + // Test with non-token string + _, err = getHeadersFromTokenString("foo") + require.Error(t, err) + require.Contains(t, err.Error(), "unable to parse tokenString") +} + +func TestGetKeyIDFromTokenString(t *testing.T) { + key, _ := testNewKey(t) + + // Test with KeyID + token1 := jwt.New() + err := token1.Set("foo", "bar") + require.NoError(t, err) + + headers1 := jws.NewHeaders() + + signedTokenBytes1, err := jwt.Sign(token1, jwa.ES384, key, jwt.WithHeaders(headers1)) + require.NoError(t, err) + + signedToken1 := string(signedTokenBytes1) + keyID, err := getKeyIDFromTokenString(signedToken1) + require.NoError(t, err) + + require.Equal(t, key.KeyID(), keyID) + + // Test without KeyID + keyWithoutKeyID := key + err = keyWithoutKeyID.Remove(jwk.KeyIDKey) + require.NoError(t, err) + + token2 := jwt.New() + err = token2.Set("foo", "bar") + require.NoError(t, err) + + headers2 := jws.NewHeaders() + + signedTokenBytes2, err := jwt.Sign(token2, jwa.ES384, keyWithoutKeyID, jwt.WithHeaders(headers2)) + require.NoError(t, err) + + signedToken2 := string(signedTokenBytes2) + _, err = getKeyIDFromTokenString(signedToken2) + require.Error(t, err) + require.Equal(t, "token header does not contain key id (kid)", err.Error()) + + // Test with non-token string + _, err = getKeyIDFromTokenString("foo") + require.Error(t, err) + require.Contains(t, err.Error(), "unable to parse tokenString") +} + +func TestGetTokenTypeFromTokenString(t *testing.T) { + key, _ := testNewKey(t) + + // Test with Type + token1 := jwt.New() + err := token1.Set("foo", "bar") + require.NoError(t, err) + + headers1 := jws.NewHeaders() + err = headers1.Set(jws.TypeKey, "foo") + require.NoError(t, err) + + signedTokenBytes1, err := jwt.Sign(token1, jwa.ES384, key, jwt.WithHeaders(headers1)) + require.NoError(t, err) + + signedToken1 := string(signedTokenBytes1) + tokenType, err := getTokenTypeFromTokenString(signedToken1) + require.NoError(t, err) + + require.Equal(t, headers1.Type(), tokenType) + + // Test without KeyID + payload1 := `{"foo":"bar"}` + + signer1, err := jws.NewSigner(jwa.ES384) + require.NoError(t, err) + + signedTokenBytes2, err := jws.SignMulti([]byte(payload1), jws.WithSigner(signer1, key, nil, nil)) + require.NoError(t, err) + + signedToken2 := string(signedTokenBytes2) + _, err = getTokenTypeFromTokenString(signedToken2) + require.Error(t, err) + require.Equal(t, "token header does not contain type (typ)", err.Error()) + + // Test with non-token string + _, err = getTokenTypeFromTokenString("foo") + require.Error(t, err) + require.Contains(t, err.Error(), "unable to parse tokenString") +} + +func TestIsTokenAudienceValid(t *testing.T) { + cases := []struct { + testDescription string + requiredAudience string + tokenAudiences []string + expectedResult bool + }{ + { + testDescription: "empty requiredAudience, empty tokenAudiences", + requiredAudience: "", + tokenAudiences: []string{}, + expectedResult: true, + }, + { + testDescription: "empty requiredAudience, one tokenAudiences", + requiredAudience: "", + tokenAudiences: []string{"foo"}, + expectedResult: true, + }, + { + testDescription: "empty requiredAudience, two tokenAudiences", + requiredAudience: "", + tokenAudiences: []string{"foo", "bar"}, + expectedResult: true, + }, + { + testDescription: "empty requiredAudience, three tokenAudiences", + requiredAudience: "", + tokenAudiences: []string{"foo", "bar", "baz"}, + expectedResult: true, + }, + { + testDescription: "one tokenAudiences, same as requiredAudience", + requiredAudience: "foo", + tokenAudiences: []string{"foo"}, + expectedResult: true, + }, + { + testDescription: "two tokenAudiences, first same as requiredAudience", + requiredAudience: "foo", + tokenAudiences: []string{"foo", "bar"}, + expectedResult: true, + }, + { + testDescription: "two tokenAudiences, second same as requiredAudience", + requiredAudience: "bar", + tokenAudiences: []string{"foo", "bar"}, + expectedResult: true, + }, + { + testDescription: "three tokenAudiences, third same as requiredAudience", + requiredAudience: "baz", + tokenAudiences: []string{"foo", "bar", "baz"}, + expectedResult: true, + }, + { + testDescription: "set requiredAudience, empty tokenAudiences", + requiredAudience: "foo", + tokenAudiences: []string{}, + expectedResult: false, + }, + { + testDescription: "one tokenAudience, not same as requiredAudience", + requiredAudience: "foo", + tokenAudiences: []string{"bar"}, + expectedResult: false, + }, + { + testDescription: "two tokenAudience, none same as requiredAudience", + requiredAudience: "foo", + tokenAudiences: []string{"bar", "baz"}, + expectedResult: false, + }, + { + testDescription: "three tokenAudience, none same as requiredAudience", + requiredAudience: "foo", + tokenAudiences: []string{"bar", "baz", "foobar"}, + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + result := isTokenAudienceValid(c.requiredAudience, c.tokenAudiences) + require.Equal(t, c.expectedResult, result) + } +} + +func TestTokenExpirationValid(t *testing.T) { + cases := []struct { + testDescription string + expiration time.Time + allowedDrift time.Duration + expectedResult bool + }{ + { + testDescription: "expires now, 50 millisecond drift allowed", + expiration: time.Now(), + allowedDrift: 50 * time.Millisecond, + expectedResult: true, + }, + { + testDescription: "expires now, 10 second drift allowed", + expiration: time.Now(), + allowedDrift: 10 * time.Second, + expectedResult: true, + }, + { + testDescription: "expires in one hour, 10 second drift allowed", + expiration: time.Now().Add(1 * time.Hour), + allowedDrift: 10 * time.Second, + expectedResult: true, + }, + { + testDescription: "expired 5 seconds ago, 10 second drift allowed", + expiration: time.Now().Add(-5 * time.Second), + allowedDrift: 10 * time.Second, + expectedResult: true, + }, + { + testDescription: "expired 11 seconds ago, 10 second drift allowed", + expiration: time.Now().Add(-11 * time.Second), + allowedDrift: 10 * time.Second, + expectedResult: false, + }, + { + testDescription: "expires now, no drift", + expiration: time.Now(), + allowedDrift: 0, + expectedResult: false, + }, + { + testDescription: "expired an hour ago, no drift", + expiration: time.Now().Add(-1 * time.Hour), + allowedDrift: 0, + expectedResult: false, + }, + { + testDescription: "expired an hour ago, 10 second drift", + expiration: time.Now().Add(-1 * time.Hour), + allowedDrift: 10 * time.Second, + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + result := isTokenExpirationValid(c.expiration, c.allowedDrift) + require.Equal(t, c.expectedResult, result) + } +} + +func TestIsTokenIssuerValid(t *testing.T) { + cases := []struct { + testDescription string + requiredIssuer string + tokenIssuer string + expectedResult bool + }{ + { + testDescription: "both requiredIssuer and tokenIssuer are the same", + requiredIssuer: "foo", + tokenIssuer: "foo", + expectedResult: true, + }, + { + testDescription: "requiredIssuer and tokenIssuer are not the same", + requiredIssuer: "foo", + tokenIssuer: "bar", + expectedResult: false, + }, + { + testDescription: "both requiredIssuer and tokenIssuer are empty", + requiredIssuer: "", + tokenIssuer: "", + expectedResult: false, + }, + { + testDescription: "requiredIssuer is empty and tokenIssuer is set", + requiredIssuer: "", + tokenIssuer: "foo", + expectedResult: false, + }, + { + testDescription: "requiredIssuer is set and tokenIssuer is empty", + requiredIssuer: "foo", + tokenIssuer: "", + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + result := isTokenIssuerValid(c.requiredIssuer, c.tokenIssuer) + require.Equal(t, c.expectedResult, result) + } +} + +func TestIsTokenTypeValid(t *testing.T) { + cases := []struct { + testDescription string + requiredTokenType string + tokenType string + expectedResult bool + }{ + { + testDescription: "both requiredTokenType and tokenType are empty", + requiredTokenType: "", + tokenType: "", + expectedResult: true, + }, + { + testDescription: "requiredTokenType is empty and tokenType is set", + requiredTokenType: "", + tokenType: "foo", + expectedResult: true, + }, + { + testDescription: "both requiredTokenType and tokenType are set to the same", + requiredTokenType: "foo", + tokenType: "foo", + expectedResult: true, + }, + { + testDescription: "requiredTokenType and tokenType are set to different", + requiredTokenType: "foo", + tokenType: "bar", + expectedResult: false, + }, + { + testDescription: "requiredTokenType and tokenType are set to different but tokenType contains requiredTokenType", + requiredTokenType: "foo", + tokenType: "foobar", + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + key, _ := testNewKey(t) + payload := `{"foo":"bar"}` + + signer, err := jws.NewSigner(jwa.ES384) + require.NoError(t, err) + + var signedTokenBytes []byte + if c.tokenType == "" { + signedTokenBytes, err = jws.SignMulti([]byte(payload), jws.WithSigner(signer, key, nil, nil)) + require.NoError(t, err) + } else { + headers := jws.NewHeaders() + err = headers.Set(jws.TypeKey, c.tokenType) + require.NoError(t, err) + + signedTokenBytes, err = jws.SignMulti([]byte(payload), jws.WithSigner(signer, key, nil, headers)) + require.NoError(t, err) + } + + token := string(signedTokenBytes) + + result := isTokenTypeValid(c.requiredTokenType, token) + require.Equal(t, c.expectedResult, result) + } +} + +func TestGetAndValidateTokenFromString(t *testing.T) { + op := server.NewTesting(t) + defer op.Close(t) + + issuer := op.GetURL(t) + discoveryUri := GetDiscoveryUriFromIssuer(issuer) + jwksUri, err := getJwksUriFromDiscoveryUri(http.DefaultClient, discoveryUri, 10*time.Millisecond) + require.NoError(t, err) + + keyHandler, err := newKeyHandler(http.DefaultClient, jwksUri, 50*time.Millisecond, 100, false) + require.NoError(t, err) + + validKey, ok := keyHandler.getKeySet().Get(0) + require.True(t, ok) + + validAccessToken := op.GetToken(t).AccessToken + require.NotEmpty(t, validAccessToken) + + validIDToken, ok := op.GetToken(t).Extra("id_token").(string) + require.True(t, ok) + require.NotEmpty(t, validIDToken) + + invalidKey, invalidPubKey := testNewKey(t) + + invalidToken := jwt.New() + err = invalidToken.Set("foo", "bar") + require.NoError(t, err) + + invalidHeaders := jws.NewHeaders() + err = invalidHeaders.Set(jws.TypeKey, "JWT") + require.NoError(t, err) + + invalidTokenBytes, err := jwt.Sign(invalidToken, jwa.ES384, invalidKey, jwt.WithHeaders(invalidHeaders)) + require.NoError(t, err) + + invalidSignedToken := string(invalidTokenBytes) + + cases := []struct { + testDescription string + tokenString string + key jwk.Key + expectedError bool + }{ + { + testDescription: "valid access token, valid key", + tokenString: validAccessToken, + key: validKey, + expectedError: false, + }, + { + testDescription: "valid id token, valid key", + tokenString: validIDToken, + key: validKey, + expectedError: false, + }, + { + testDescription: "empty string, valid key", + tokenString: "", + key: validKey, + expectedError: true, + }, + { + testDescription: "random string, valid key", + tokenString: "foobar", + key: validKey, + expectedError: true, + }, + { + testDescription: "invalid token, valid key", + tokenString: invalidSignedToken, + key: validKey, + expectedError: true, + }, + { + testDescription: "invalid token, invalid key", + tokenString: invalidSignedToken, + key: invalidPubKey, + expectedError: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + alg, err := getSignatureAlgorithm(c.key.KeyType(), c.key.Algorithm(), jwa.ES384) + require.NoError(t, err) + + token, err := getAndValidateTokenFromString(c.tokenString, c.key, alg) + if c.expectedError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.NotEmpty(t, token) + } + } +} + +func TestParseToken(t *testing.T) { + keySets := testNewTestKeySet(t) + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + cases := []struct { + testDescription string + options []options.Option + numKeys int + customIssuer string + customExpirationMinutes int + customClaims map[string]string + expectedErrorContains string + }{ + { + testDescription: "successful parse with keyID, one key", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(false), + options.WithJwksRateLimit(100), + }, + numKeys: 1, + expectedErrorContains: "", + }, + { + testDescription: "successful parse without keyID, one key", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(true), + options.WithJwksRateLimit(100), + }, + numKeys: 1, + expectedErrorContains: "", + }, + { + testDescription: "successful parse with keyID, two keys", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(false), + options.WithJwksRateLimit(100), + }, + numKeys: 2, + expectedErrorContains: "", + }, + { + // without lazyLoad, New() panics + testDescription: "unsuccessful parse without keyID, two keys with lazyLoad", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(true), + options.WithJwksRateLimit(100), + options.WithLazyLoadJwks(true), + }, + numKeys: 2, + expectedErrorContains: "keyID is disabled, but received a keySet with more than one key", + }, + { + testDescription: "wrong issuer, with keyID", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(false), + }, + numKeys: 1, + customIssuer: "http://wrong.issuer", + expectedErrorContains: "required issuer \"http://foo.bar\" was not found", + }, + { + testDescription: "wrong issuer, without keyID", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(true), + }, + numKeys: 1, + customIssuer: "http://wrong.issuer", + expectedErrorContains: "required issuer \"http://foo.bar\" was not found", + }, + { + testDescription: "expired token, with keyID", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(false), + }, + numKeys: 1, + customExpirationMinutes: -1, + expectedErrorContains: "token has expired", + }, + { + testDescription: "expired token, without keyID", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(true), + }, + numKeys: 1, + customExpirationMinutes: -1, + expectedErrorContains: "token has expired", + }, + { + testDescription: "correct requiredClaim", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithRequiredClaims(map[string]interface{}{ + "foo": "bar", + }), + options.WithDisableKeyID(false), + }, + numKeys: 1, + expectedErrorContains: "", + }, + { + testDescription: "correct requiredClaim", + options: []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithRequiredClaims(map[string]interface{}{ + "foo": "bar", + }), + options.WithDisableKeyID(false), + }, + numKeys: 1, + customClaims: map[string]string{ + "foo": "baz", + }, + expectedErrorContains: "unable to validate required claims", + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + opts := &options.Options{} + + for _, setter := range c.options { + setter(opts) + } + + keySets.setKeys(testNewKeySet(t, c.numKeys, opts.DisableKeyID)) + + h, err := NewHandler(c.options...) + require.NoError(t, err) + + parseTokenFunc := h.ParseToken + + issuer := opts.Issuer + if c.customIssuer != "" { + issuer = c.customIssuer + } + + expirationMinutes := 1 + if c.customExpirationMinutes != 0 { + expirationMinutes = c.customExpirationMinutes + } + + customClaims := make(map[string]string) + customClaims["foo"] = "bar" + if c.customClaims != nil { + customClaims = c.customClaims + } + + token := testNewCustomTokenString(t, keySets.privateKeySet, issuer, expirationMinutes, customClaims) + + ctx := context.Background() + + _, err = parseTokenFunc(ctx, token) + + if c.expectedErrorContains == "" { + require.NoError(t, err) + } else { + require.Contains(t, err.Error(), c.expectedErrorContains) + } + } +} + +func TestParseTokenWithKeyID(t *testing.T) { + disableKeyID := false + keySets := testNewTestKeySet(t) + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + opts := []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(disableKeyID), + options.WithJwksRateLimit(100), + } + + h, err := NewHandler(opts...) + require.NoError(t, err) + + parseTokenFunc := h.ParseToken + + // first token should succeed + token1 := testNewTokenString(t, keySets.privateKeySet) + + ctx := context.Background() + + _, err = parseTokenFunc(ctx, token1) + require.NoError(t, err) + + // second token should succeed, rotation successful + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + token2 := testNewTokenString(t, keySets.privateKeySet) + + _, err = parseTokenFunc(ctx, token2) + require.NoError(t, err) + + // after rotation, first token should fail + _, err = parseTokenFunc(ctx, token1) + require.Error(t, err) + + // third token should succeed with two keys + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + token3 := testNewTokenString(t, keySets.privateKeySet) + + _, err = parseTokenFunc(ctx, token3) + require.NoError(t, err) + + // fourth token should fail since they token doesn't contain keyID + keySets.setKeys(testNewKeySet(t, 1, true)) + + token4 := testNewTokenString(t, keySets.privateKeySet) + + _, err = parseTokenFunc(ctx, token4) + require.Error(t, err) + + // fifth token should fail since it's the wrong key but correct keyID + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + currentPrivateKey, found := keySets.privateKeySet.Get(0) + require.True(t, found) + + currentKeyID := currentPrivateKey.KeyID() + invalidPrivKey, _ := testNewKey(t) + + err = invalidPrivKey.Set(jwk.KeyIDKey, currentKeyID) + require.NoError(t, err) + + invalidKeySet := jwk.NewSet() + invalidKeySet.Add(invalidPrivKey) + + token5 := testNewTokenString(t, invalidKeySet) + + _, err = parseTokenFunc(ctx, token5) + require.ErrorIs(t, err, errSignatureVerification) + + // sixth token should fail since the jwks can't be refreshed + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + token6 := testNewTokenString(t, keySets.privateKeySet) + + testServer.Close() + + _, err = parseTokenFunc(ctx, token6) + require.Error(t, err) +} + +func TestParseTokenWithoutKeyID(t *testing.T) { + disableKeyID := true + keySets := testNewTestKeySet(t) + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + opts := []options.Option{ + options.WithIssuer("http://foo.bar"), + options.WithDiscoveryUri("http://foo.bar"), + options.WithJwksUri(testServer.URL), + options.WithDisableKeyID(disableKeyID), + options.WithJwksRateLimit(100), + } + + h, err := NewHandler(opts...) + require.NoError(t, err) + + parseTokenFunc := h.ParseToken + + // first token should succeed + token1 := testNewTokenString(t, keySets.privateKeySet) + + ctx := context.Background() + + _, err = parseTokenFunc(ctx, token1) + require.NoError(t, err) + + // second token should succeed, with key rotation + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + token2 := testNewTokenString(t, keySets.privateKeySet) + + _, err = parseTokenFunc(ctx, token2) + require.NoError(t, err) + + // after rotation, first token should fail + _, err = parseTokenFunc(ctx, token1) + require.Error(t, err) + + // third token should fail since there are two keys present + keySets.setKeys(testNewKeySet(t, 2, disableKeyID)) + + token3 := testNewTokenString(t, keySets.privateKeySet) + + _, err = parseTokenFunc(ctx, token3) + require.Error(t, err) + + // fourth token should fail since the jwks can't be refreshed + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + token4 := testNewTokenString(t, keySets.privateKeySet) + + testServer.Close() + + _, err = parseTokenFunc(ctx, token4) + require.Error(t, err) +} + +func TestGetAndValidateTokenFromStringWithKeyID(t *testing.T) { + disableKeyID := false + keySets := testNewTestKeySet(t) + testServer := testNewJwksServer(t, keySets) + defer testServer.Close() + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + keyHandler, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + token1 := testNewTokenString(t, keySets.privateKeySet) + + keyID, err := getKeyIDFromTokenString(token1) + require.NoError(t, err) + + pubKey, err := keyHandler.getKey(context.Background(), keyID) + require.NoError(t, err) + + alg, err := getSignatureAlgorithm(pubKey.KeyType(), pubKey.Algorithm(), jwa.ES384) + require.NoError(t, err) + + _, err = getAndValidateTokenFromString(token1, pubKey, alg) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + token2 := testNewTokenString(t, keySets.privateKeySet) + + _, err = getAndValidateTokenFromString(token2, pubKey, alg) + require.Error(t, err) +} + +func TestGetAndValidateTokenFromStringWithoutKeyID(t *testing.T) { + disableKeyID := true + keySets := testNewTestKeySet(t) + testServer := testNewJwksServer(t, keySets) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + keyHandler, err := newKeyHandler(http.DefaultClient, testServer.URL, 10*time.Millisecond, 100, disableKeyID) + require.NoError(t, err) + + token1 := testNewTokenString(t, keySets.privateKeySet) + + pubKey, err := keyHandler.getKey(context.Background(), "") + require.NoError(t, err) + + alg, err := getSignatureAlgorithm(pubKey.KeyType(), pubKey.Algorithm(), jwa.ES384) + require.NoError(t, err) + + _, err = getAndValidateTokenFromString(token1, pubKey, alg) + require.NoError(t, err) + + keySets.setKeys(testNewKeySet(t, 1, disableKeyID)) + + token2 := testNewTokenString(t, keySets.privateKeySet) + + _, err = getAndValidateTokenFromString(token2, pubKey, alg) + require.ErrorIs(t, err, errSignatureVerification) +} + +func TestIsRequiredClaimsValid(t *testing.T) { + cases := []struct { + testDescription string + requiredClaims map[string]interface{} + tokenClaims map[string]interface{} + expectedResult bool + }{ + { + testDescription: "both are nil", + requiredClaims: nil, + tokenClaims: nil, + expectedResult: true, + }, + { + testDescription: "both are empty", + requiredClaims: map[string]interface{}{}, + tokenClaims: map[string]interface{}{}, + expectedResult: true, + }, + { + testDescription: "required claims are nil", + requiredClaims: nil, + tokenClaims: map[string]interface{}{ + "foo": "bar", + }, + expectedResult: true, + }, + { + testDescription: "required claims are empty", + requiredClaims: map[string]interface{}{}, + tokenClaims: map[string]interface{}{ + "foo": "bar", + }, + expectedResult: true, + }, + { + testDescription: "token claims are nil", + requiredClaims: map[string]interface{}{ + "foo": "bar", + }, + tokenClaims: nil, + expectedResult: false, + }, + { + testDescription: "token claims are empty", + requiredClaims: map[string]interface{}{ + "foo": "bar", + }, + tokenClaims: map[string]interface{}{}, + expectedResult: false, + }, + { + testDescription: "required is string, token is int", + requiredClaims: map[string]interface{}{ + "foo": "bar", + }, + tokenClaims: map[string]interface{}{ + "foo": 1337, + }, + expectedResult: false, + }, + { + testDescription: "matching with string", + requiredClaims: map[string]interface{}{ + "foo": "bar", + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + }, + expectedResult: true, + }, + { + testDescription: "matching with string and int", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + }, + expectedResult: true, + }, + { + testDescription: "matching with string and int in different orders", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + }, + tokenClaims: map[string]interface{}{ + "bar": 1337, + "foo": "bar", + }, + expectedResult: true, + }, + { + testDescription: "matching with string, int and float", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": 13.37, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": 13.37, + }, + expectedResult: true, + }, + { + testDescription: "not matching with string, int and float", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": 13.37, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": 12.27, + }, + expectedResult: false, + }, + { + testDescription: "matching slice", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo"}, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo"}, + }, + expectedResult: true, + }, + { + testDescription: "matching slice with multiple values", + requiredClaims: map[string]interface{}{ + "oof": []string{"foo", "bar"}, + }, + tokenClaims: map[string]interface{}{ + "oof": []string{"foo", "bar", "baz"}, + }, + expectedResult: true, + }, + { + testDescription: "required slice contains in token slice", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo"}, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo", "bar", "baz"}, + }, + expectedResult: true, + }, + { + testDescription: "not matching slice", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo"}, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"bar"}, + }, + expectedResult: false, + }, + { + testDescription: "matching map", + requiredClaims: map[string]interface{}{ + "foo": map[string]string{ + "foo": "bar", + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string]string{ + "foo": "bar", + }, + }, + expectedResult: true, + }, + { + testDescription: "matching map with multiple values", + requiredClaims: map[string]interface{}{ + "foo": map[string]string{ + "foo": "bar", + "bar": "foo", + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string]string{ + "a": "b", + "foo": "bar", + "bar": "foo", + "c": "d", + }, + }, + expectedResult: true, + }, + { + testDescription: "matching map with multiple keys in token claims", + requiredClaims: map[string]interface{}{ + "foo": map[string]string{ + "foo": "bar", + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string]string{ + "a": "b", + "foo": "bar", + "c": "d", + }, + }, + expectedResult: true, + }, + { + testDescription: "not matching map", + requiredClaims: map[string]interface{}{ + "foo": map[string]string{ + "foo": "bar", + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string]int{ + "foo": 1337, + }, + }, + expectedResult: false, + }, + { + testDescription: "matching map with string slice", + requiredClaims: map[string]interface{}{ + "foo": map[string][]string{ + "foo": {"bar"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string][]string{ + "foo": {"foo", "bar", "baz"}, + }, + }, + expectedResult: true, + }, + { + testDescription: "not matching map with string slice", + requiredClaims: map[string]interface{}{ + "foo": map[string][]string{ + "foo": {"foobar"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string][]string{ + "foo": {"foo", "bar", "baz"}, + }, + }, + expectedResult: false, + }, + { + testDescription: "matching slice with map", + requiredClaims: map[string]interface{}{ + "foo": []map[string]string{ + {"bar": "baz"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": []map[string]string{ + {"bar": "baz"}, + }, + }, + expectedResult: true, + }, + { + testDescription: "not matching slice with map", + requiredClaims: map[string]interface{}{ + "foo": []map[string]string{ + {"bar": "foobar"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": []map[string]string{ + {"bar": "baz"}, + }, + }, + expectedResult: false, + }, + { + testDescription: "matching primitive types, slice and map", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo"}, + "oof": []map[string]string{ + {"bar": "baz"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo"}, + "oof": []map[string]string{ + {"bar": "baz"}, + }, + }, + expectedResult: true, + }, + { + testDescription: "matching primitive types, slice and map where token contains multiple values", + requiredClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"bar"}, + "oof": []map[string]string{ + {"bar": "baz"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": "bar", + "bar": 1337, + "baz": []string{"foo", "bar", "baz"}, + "oof": []map[string]string{ + {"a": "b"}, + {"bar": "baz"}, + {"c": "d"}, + }, + }, + expectedResult: true, + }, + { + testDescription: "valid interface list in an interface map", + requiredClaims: map[string]interface{}{ + "foo": map[string][]string{ + "bar": {"baz"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": []interface{}{ + "uno", + "dos", + "baz", + "tres", + }, + }, + }, + expectedResult: true, + }, + { + testDescription: "invalid interface list in an interface map", + requiredClaims: map[string]interface{}{ + "foo": map[string][]string{ + "bar": {"baz"}, + }, + }, + tokenClaims: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": []interface{}{ + "uno", + "dos", + "tres", + }, + }, + }, + expectedResult: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + err := isRequiredClaimsValid(c.requiredClaims, c.tokenClaims) + + if c.expectedResult { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } +} + +func TestGetSignatureAlgorithm(t *testing.T) { + cases := []struct { + inputKty jwa.KeyType + inputAlg string + inputFallbackAlg jwa.SignatureAlgorithm + expectedResult jwa.SignatureAlgorithm + expectedError bool + }{ + { + inputKty: jwa.RSA, + inputAlg: "RS256", + inputFallbackAlg: "", + expectedResult: jwa.RS256, + expectedError: false, + }, + { + inputKty: jwa.EC, + inputAlg: "ES256", + inputFallbackAlg: "", + expectedResult: jwa.ES256, + expectedError: false, + }, + { + inputKty: jwa.RSA, + inputAlg: "", + inputFallbackAlg: "", + expectedResult: jwa.RS256, + expectedError: false, + }, + { + inputKty: jwa.EC, + inputAlg: "", + inputFallbackAlg: "", + expectedResult: jwa.ES256, + expectedError: false, + }, + { + inputKty: "", + inputAlg: "", + inputFallbackAlg: "", + expectedResult: "", + expectedError: true, + }, + { + inputKty: "", + inputAlg: "foobar", + inputFallbackAlg: "", + expectedResult: "", + expectedError: true, + }, + { + inputKty: "", + inputAlg: "", + inputFallbackAlg: jwa.ES384, + expectedResult: jwa.ES384, + expectedError: false, + }, + { + inputKty: jwa.RSA, + inputAlg: "", + inputFallbackAlg: jwa.ES384, + expectedResult: jwa.ES384, + expectedError: false, + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: inputKty=%s, inputAlg=%s, inputFallbackAlg=%s", i, c.inputKty, c.inputAlg, c.inputFallbackAlg) + + result, err := getSignatureAlgorithm(c.inputKty, c.inputAlg, c.inputFallbackAlg) + require.Equal(t, c.expectedResult, result) + + if !c.expectedError { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } +} + +func testNewKey(tb testing.TB) (jwk.Key, jwk.Key) { + tb.Helper() + + ecdsaKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(tb, err) + + key, err := jwk.New(ecdsaKey) + require.NoError(tb, err) + + _, ok := key.(jwk.ECDSAPrivateKey) + require.True(tb, ok) + + thumbprint, err := key.Thumbprint(crypto.SHA256) + require.NoError(tb, err) + + keyID := fmt.Sprintf("%x", thumbprint) + err = key.Set(jwk.KeyIDKey, keyID) + require.NoError(tb, err) + + pubKey, err := jwk.New(ecdsaKey.PublicKey) + require.NoError(tb, err) + + _, ok = pubKey.(jwk.ECDSAPublicKey) + require.True(tb, ok) + + err = pubKey.Set(jwk.KeyIDKey, keyID) + require.NoError(tb, err) + + err = pubKey.Set(jwk.AlgorithmKey, jwa.ES384) + require.NoError(tb, err) + + return key, pubKey +} + +func testNewTokenString(t *testing.T, privKeySet jwk.Set) string { + t.Helper() + + jwtToken := jwt.New() + err := jwtToken.Set(jwt.IssuerKey, "http://foo.bar") + require.NoError(t, err) + + err = jwtToken.Set(jwt.ExpirationKey, time.Now().Add(1*time.Minute).Unix()) + require.NoError(t, err) + + err = jwtToken.Set("foo", "bar") + require.NoError(t, err) + + headers := jws.NewHeaders() + err = headers.Set(jws.TypeKey, "JWT") + require.NoError(t, err) + + privKey, found := privKeySet.Get(0) + require.True(t, found) + + tokenBytes, err := jwt.Sign(jwtToken, jwa.ES384, privKey, jwt.WithHeaders(headers)) + require.NoError(t, err) + + return string(tokenBytes) +} + +func testNewCustomTokenString(t *testing.T, privKeySet jwk.Set, issuer string, expirationMinutes int, customClaims map[string]string) string { + t.Helper() + + jwtToken := jwt.New() + err := jwtToken.Set(jwt.IssuerKey, issuer) + require.NoError(t, err) + + err = jwtToken.Set(jwt.ExpirationKey, time.Now().Add(time.Duration(expirationMinutes)*time.Minute).Unix()) + require.NoError(t, err) + + for k, v := range customClaims { + err := jwtToken.Set(k, v) + require.NoError(t, err) + } + + headers := jws.NewHeaders() + + err = headers.Set(jws.TypeKey, "JWT") + require.NoError(t, err) + + privKey, found := privKeySet.Get(0) + require.True(t, found) + + tokenBytes, err := jwt.Sign(jwtToken, jwa.ES384, privKey, jwt.WithHeaders(headers)) + require.NoError(t, err) + + return string(tokenBytes) +} diff --git a/options/options.go b/options/options.go new file mode 100644 index 0000000..6a8add1 --- /dev/null +++ b/options/options.go @@ -0,0 +1,264 @@ +package options + +import ( + "net/http" + "time" +) + +// ClaimsContextKeyName is the type for they key value used to pass claims using request context. +// Using separate type because of the following: https://staticcheck.io/docs/checks#SA1029 +type ClaimsContextKeyName string + +// DefaultClaimsContextKeyName is of type ClaimsContextKeyName and defaults to "claims" +const DefaultClaimsContextKeyName ClaimsContextKeyName = "claims" + +// ErrorHandler is called by the middleware if not nil +type ErrorHandler func(description ErrorDescription, err error) + +// ErrorDescription is used to pass the description of the error to ErrorHandler +type ErrorDescription string + +const ( + // GetTokenErrorDescription is returned to ErrorHandler if the middleware is unable to get a token from the request + GetTokenErrorDescription ErrorDescription = "unable to get token string" + // ParseTokenErrorDescription is returned to ErrorHandler if the middleware is unable to parse the token extracted from the request + ParseTokenErrorDescription ErrorDescription = "unable to parse token string" + // ConvertTokenErrorDescription is returned to ErrorHandler if the middleware is unable to convert the token to a map + ConvertTokenErrorDescription ErrorDescription = "unable to convert token to map" +) + +// Options defines the options for OIDC Middleware. +type Options struct { + Issuer string + DiscoveryUri string + JwksUri string + JwksFetchTimeout time.Duration + JwksRateLimit uint + FallbackSignatureAlgorithm string + AllowedTokenDrift time.Duration + LazyLoadJwks bool + RequiredTokenType string + RequiredAudience string + RequiredClaims map[string]interface{} + DisableKeyID bool + HttpClient *http.Client + TokenString [][]TokenStringOption + ClaimsContextKeyName ClaimsContextKeyName + ErrorHandler ErrorHandler +} + +// New takes Option setters and returns an Options pointer. +// Mainly used by the internal functions and most likely not +// needed by any external application using this library. +func New(setters ...Option) *Options { + opts := &Options{ + JwksFetchTimeout: 5 * time.Second, + JwksRateLimit: 1, + AllowedTokenDrift: 10 * time.Second, + HttpClient: http.DefaultClient, + ClaimsContextKeyName: DefaultClaimsContextKeyName, + } + + for _, setter := range setters { + setter(opts) + } + + return opts +} + +// Option returns a function that modifies an Options pointer. +type Option func(*Options) + +// WithIssuer sets the Issuer parameter for Options. +// Issuer is the authority that issues the tokens +func WithIssuer(opt string) Option { + return func(opts *Options) { + opts.Issuer = opt + } +} + +// WithDiscoveryUri sets the Issuer parameter for an Options pointer. +// DiscoveryUri is where the `jwks_uri` will be grabbed +// Defaults to `fmt.Sprintf("%s/.well-known/openid-configuration", strings.TrimSuffix(issuer, "/"))` +func WithDiscoveryUri(opt string) Option { + return func(opts *Options) { + opts.DiscoveryUri = opt + } +} + +// WithJwksUri sets the JwksUri parameter for an Options pointer. +// JwksUri is used to download the public key(s) +// Defaults to the `jwks_uri` from the response of DiscoveryUri +func WithJwksUri(opt string) Option { + return func(opts *Options) { + opts.JwksUri = opt + } +} + +// WithJwksFetchTimeout sets the JwksFetchTimeout parameter for an Options pointer. +// JwksFetchTimeout sets the context timeout when downloading the jwks +// Defaults to 5 seconds +func WithJwksFetchTimeout(opt time.Duration) Option { + return func(opts *Options) { + opts.JwksFetchTimeout = opt + } +} + +// WithJwksRateLimit sets the JwksFetchTimeout parameter for an Options pointer. +// JwksRateLimit takes an uint and makes sure that the jwks will at a maximum +// be requested these many times per second. +// Defaults to 1 (Request Per Second) +// Please observe: Requests that force update of jwks (like wrong keyID) will be rate limited +func WithJwksRateLimit(opt uint) Option { + return func(opts *Options) { + opts.JwksRateLimit = opt + } +} + +// WithFallbackSignatureAlgorithm sets the FallbackSignatureAlgorithm parameter for an Options pointer. +// FallbackSignatureAlgorithm needs to be used when the jwks doesn't contain the alg key. +// If not specified and jwks doesn't contain alg key, will default to: +// - RS256 for key type (kty) RSA +// - ES256 for key type (kty) EC +// +// When specified and jwks contains alg key, alg key from jwks will be used. +// +// Example values (one of them): RS256 RS384 RS512 ES256 ES384 ES512 +func WithFallbackSignatureAlgorithm(opt string) Option { + return func(opts *Options) { + opts.FallbackSignatureAlgorithm = opt + } +} + +// WithAllowedTokenDrift sets the AllowedTokenDrift parameter for an Options pointer. +// AllowedTokenDrift adds the duration to the token expiration to allow +// for time drift between parties. +// Defaults to 10 seconds +func WithAllowedTokenDrift(opt time.Duration) Option { + return func(opts *Options) { + opts.AllowedTokenDrift = opt + } +} + +// WithLazyLoadJwks sets the LazyLoadJwks parameter for an Options pointer. +// LazyLoadJwks makes it possible to use OIDC Discovery without being +// able to load the keys at startup. +// Default setting is disabled. +// Please observe: If enabled, it will always load even though settings +// may be wrong / not working. +func WithLazyLoadJwks(opt bool) Option { + return func(opts *Options) { + opts.LazyLoadJwks = opt + } +} + +// WithRequiredTokenType sets the RequiredTokenType parameter for an Options pointer. +// RequiredTokenType is used if only specific tokens should be allowed. +// Default is empty string `""` and means all token types are allowed. +// Use case could be to configure this if the TokenType (set in the header of the JWT) +// should be `JWT` or maybe even `JWT+AT` to differentiate between access tokens and +// id tokens. Not all providers support or use this. +func WithRequiredTokenType(opt string) Option { + return func(opts *Options) { + opts.RequiredTokenType = opt + } +} + +// WithRequiredAudience sets the RequiredAudience parameter for an Options pointer. +// RequiredAudience is used to require a specific Audience `aud` in the claims. +// Defaults to empty string `""` and means all audiences are allowed. +func WithRequiredAudience(opt string) Option { + return func(opts *Options) { + opts.RequiredAudience = opt + } +} + +// WithRequiredClaims sets the RequiredClaims parameter for an Options pointer. +// RequiredClaims is used to require specific claims in the token +// Defaults to empty map (nil) and won't check for anything else +// Works with primitive types, slices and maps. +// Please observe: slices and strings checks that the token contains it, but more is allowed. +// Required claim []string{"bar"} matches token []string{"foo", "bar", "baz"} +// Required claim map[string]string{{"foo": "bar"}} matches token map[string]string{{"a": "b"},{"foo": "bar"},{"c": "d"}} +// +// Example: +// +// ```go +// map[string]interface{}{ +// "foo": "bar", +// "bar": 1337, +// "baz": []string{"bar"}, +// "oof": []map[string]string{ +// {"bar": "baz"}, +// }, +// }, +// ``` +func WithRequiredClaims(opt map[string]interface{}) Option { + return func(opts *Options) { + opts.RequiredClaims = opt + } +} + +// WithDisableKeyID sets the DisableKeyID parameter for an Options pointer. +// DisableKeyID adjusts if a KeyID needs to be extracted from the token or not +// Defaults to false and means KeyID is required to be present in both the jwks and token +// The OIDC specification doesn't require KeyID if there's only one key in the jwks: +// https://openid.net/specs/openid-connect-core-1_0.html#Signing +// +// This also means that if enabled, refresh of the jwks will be done if the token can't be +// validated due to invalid key. The JWKS fetch will fail if there's more than one key present. +func WithDisableKeyID(opt bool) Option { + return func(opts *Options) { + opts.DisableKeyID = opt + } +} + +// WithHttpClient sets the HttpClient parameter for an Options pointer. +// HttpClient takes a *http.Client for external calls +// Defaults to http.DefaultClient +func WithHttpClient(opt *http.Client) Option { + return func(opts *Options) { + opts.HttpClient = opt + } +} + +// WithTokenString sets the TokenString parameter for an Options pointer. +// TokenString makes it possible to configure how the JWT token should be extracted from +// an http header. Not supported by Echo JWT and will be ignored if used by it. +// Defaults to: 'Authorization: Bearer JWT' +func WithTokenString(setters ...TokenStringOption) Option { + var tokenString []TokenStringOption + tokenString = append(tokenString, setters...) + + return func(opts *Options) { + opts.TokenString = append(opts.TokenString, tokenString) + } +} + +// WithClaimsContextKeyName sets the ClaimsContextKeyName parameter for an Options pointer. +// ClaimsContextKeyName is the name of key that will be used to pass claims using request context. +// Not supported by Echo JWT and will be ignored if used by it. +// +// Important note: If you change this using `options.WithClaimsContextKeyName("foo")`, then +// you also need to use it like this: +// `claims, ok := r.Context().Value(options.ClaimsContextKeyName("foo")).(map[string]interface{})` +// +// Default: `options.DefaultClaimsContextKeyName` +// Used like this: ``claims, ok := r.Context().Value(options.DefaultClaimsContextKeyName).(map[string]interface{})`` +// +// When used with gin, it is converted to normal string - by default: +// `claimsValue, found := c.Get("claims")` +func WithClaimsContextKeyName(opt string) Option { + return func(opts *Options) { + opts.ClaimsContextKeyName = ClaimsContextKeyName(opt) + } +} + +// WithErrorHandler sets the ErrorHandler parameter for an Options pointer. +// You can pass a function to run custom logic on errors, logging as an example. +// Defaults to nil +func WithErrorHandler(opt ErrorHandler) Option { + return func(opts *Options) { + opts.ErrorHandler = opt + } +} diff --git a/options/options_test.go b/options/options_test.go new file mode 100644 index 0000000..3b89da1 --- /dev/null +++ b/options/options_test.go @@ -0,0 +1,101 @@ +package options + +import ( + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestOptions(t *testing.T) { + expectedResult := &Options{ + Issuer: "foo", + DiscoveryUri: "foo", + JwksUri: "foo", + JwksFetchTimeout: 1234 * time.Second, + JwksRateLimit: 1234, + FallbackSignatureAlgorithm: "foo", + AllowedTokenDrift: 1234 * time.Second, + LazyLoadJwks: true, + RequiredTokenType: "foo", + RequiredAudience: "foo", + RequiredClaims: map[string]interface{}{ + "foo": "bar", + }, + DisableKeyID: true, + HttpClient: &http.Client{ + Timeout: 1234 * time.Second, + }, + TokenString: nil, + ClaimsContextKeyName: ClaimsContextKeyName("foo"), + ErrorHandler: nil, + } + + expectedFirstTokenString := &TokenStringOptions{ + HeaderName: "foo", + TokenPrefix: "bar_", + ListSeparator: ",", + } + + expectedSecondTokenString := &TokenStringOptions{ + HeaderName: "too", + TokenPrefix: "lar_", + ListSeparator: "", + } + + setters := []Option{ + WithIssuer("foo"), + WithDiscoveryUri("foo"), + WithJwksUri("foo"), + WithJwksFetchTimeout(1234 * time.Second), + WithJwksRateLimit(1234), + WithFallbackSignatureAlgorithm("foo"), + WithAllowedTokenDrift(1234 * time.Second), + WithLazyLoadJwks(true), + WithRequiredTokenType("foo"), + WithRequiredAudience("foo"), + WithRequiredClaims(map[string]interface{}{ + "foo": "bar", + }), + WithDisableKeyID(true), + WithHttpClient(&http.Client{ + Timeout: 1234 * time.Second, + }), + WithTokenString( + WithTokenStringHeaderName("foo"), + WithTokenStringTokenPrefix("bar_"), + WithTokenStringListSeparator(","), + ), + WithTokenString( + WithTokenStringHeaderName("too"), + WithTokenStringTokenPrefix("lar_"), + ), + WithClaimsContextKeyName("foo"), + WithErrorHandler(nil), + } + + result := &Options{} + + for _, setter := range setters { + setter(result) + } + + resultFirstTokenString := &TokenStringOptions{} + resultSecondTokenString := &TokenStringOptions{} + + for _, setter := range result.TokenString[0] { + setter(resultFirstTokenString) + } + + for _, setter := range result.TokenString[1] { + setter(resultSecondTokenString) + } + + // Needed or else expectedResult can't be compared to result + result.TokenString = nil + + require.Equal(t, expectedResult, result) + require.Equal(t, expectedFirstTokenString, resultFirstTokenString) + require.Equal(t, expectedSecondTokenString, resultSecondTokenString) +} diff --git a/options/tokenstring.go b/options/tokenstring.go new file mode 100644 index 0000000..f43543b --- /dev/null +++ b/options/tokenstring.go @@ -0,0 +1,69 @@ +package options + +// TokenStringOptions handles the settings for how to extract the token from a request. +type TokenStringOptions struct { + HeaderName string + TokenPrefix string + ListSeparator string + PostExtractionFn func(string) (string, error) +} + +// NewTokenString takes TokenStringOption setters and returns +// a TokenStringOptions pointer. +// Mainly used by the internal functions and most likely not +// needed by any external application using this library. +func NewTokenString(setters ...TokenStringOption) *TokenStringOptions { + opts := &TokenStringOptions{ + HeaderName: "Authorization", + TokenPrefix: "Bearer ", + ListSeparator: "", + PostExtractionFn: nil, + } + + for _, setter := range setters { + setter(opts) + } + + return opts +} + +// TokenStringOption returns a function that modifies a TokenStringOptions pointer. +type TokenStringOption func(*TokenStringOptions) + +// WithTokenStringHeaderName sets the HeaderName parameter for a TokenStringOptions pointer. +// HeaderName is the name of the header. +// Default: "Authorization" +func WithTokenStringHeaderName(opt string) TokenStringOption { + return func(opts *TokenStringOptions) { + opts.HeaderName = opt + } +} + +// WithTokenStringTokenPrefix sets the TokenPrefix parameter for a TokenStringOptions pointer. +// TokenPrefix defines the prefix that should be trimmed from the header value +// to extract the token. +// Default: "Bearer " +func WithTokenStringTokenPrefix(opt string) TokenStringOption { + return func(opts *TokenStringOptions) { + opts.TokenPrefix = opt + } +} + +// WithTokenStringListSeparator sets the ListSeparator parameter for a TokenStringOptions pointer. +// ListSeparator defines if the value of the header is a list or not. +// The value will be split (up to 20 slices) by the ListSeparator. +// Default disabled: "" +func WithTokenStringListSeparator(opt string) TokenStringOption { + return func(opts *TokenStringOptions) { + opts.ListSeparator = opt + } +} + +// WithTokenStringPostExtractionFn sets the PostExtractionFn parameter for a TokenStringOptions pointer. +// PostExtractionFn will be run if not nil after a token has been successfully extracted. +// Default: nil +func WithTokenStringPostExtractionFn(opt func(string) (string, error)) TokenStringOption { + return func(opts *TokenStringOptions) { + opts.PostExtractionFn = opt + } +} diff --git a/tokenstring.go b/tokenstring.go new file mode 100644 index 0000000..aeb71a1 --- /dev/null +++ b/tokenstring.go @@ -0,0 +1,93 @@ +package oidc + +import ( + "fmt" + "strings" + + "git.icod.de/dalu/oidc/options" +) + +type GetHeaderFn func(key string) string + +const maxListSeparatorSlices = 20 + +// GetTokenString extracts a token string. +func GetTokenString(getHeaderFn GetHeaderFn, tokenStringOpts [][]options.TokenStringOption) (string, error) { + optsList := tokenStringOpts + if len(optsList) == 0 { + optsList = append(optsList, []options.TokenStringOption{}) + } + + var err error + for _, setters := range optsList { + opts := options.NewTokenString(setters...) + + var tokenString string + tokenString, err = getTokenString(getHeaderFn, opts) + if err == nil && tokenString != "" { + // if a PostExtractionFn is defined, pass the token to it + if opts.PostExtractionFn != nil { + tokenString, err = opts.PostExtractionFn(tokenString) + if err != nil { + // if the PostExtractionFn returns an error, continue the loop + continue + } + + if tokenString == "" { + // if the PostExtractionFn returns an empty string, continue the loop + err = fmt.Errorf("post extraction function returned an empty token string") + continue + } + + return tokenString, nil + } + + return tokenString, nil + } + } + + return "", fmt.Errorf("unable to extract token: %w", err) +} + +func getTokenString(getHeaderFn GetHeaderFn, opts *options.TokenStringOptions) (string, error) { + headerValue := getHeaderFn(opts.HeaderName) + if headerValue == "" { + return "", fmt.Errorf("%s header empty", opts.HeaderName) + } + + if opts.ListSeparator != "" && strings.Contains(headerValue, opts.ListSeparator) { + headerValueList := strings.SplitN(headerValue, opts.ListSeparator, maxListSeparatorSlices) + return getTokenFromList(headerValueList, opts) + } + + return getTokenFromString(headerValue, opts) +} + +func getTokenFromList(headerValueList []string, opts *options.TokenStringOptions) (string, error) { + for _, headerValue := range headerValueList { + tokenString, err := getTokenFromString(headerValue, opts) + if err == nil && tokenString != "" { + return tokenString, nil + } + } + + return "", fmt.Errorf("no token found in list") +} + +func getTokenFromString(headerValue string, opts *options.TokenStringOptions) (string, error) { + if headerValue == "" { + return "", fmt.Errorf("%s header empty", opts.HeaderName) + } + + if !strings.HasPrefix(headerValue, opts.TokenPrefix) { + return "", fmt.Errorf("%s header does not begin with: %s", opts.HeaderName, opts.TokenPrefix) + } + + token := strings.TrimPrefix(headerValue, opts.TokenPrefix) + + if token == "" { + return "", fmt.Errorf("%s header empty after prefix is trimmed", opts.HeaderName) + } + + return token, nil +} diff --git a/tokenstring_test.go b/tokenstring_test.go new file mode 100644 index 0000000..8bc21c6 --- /dev/null +++ b/tokenstring_test.go @@ -0,0 +1,405 @@ +package oidc + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "git.icod.de/dalu/oidc/options" + "github.com/stretchr/testify/require" +) + +func TestGetTokenString(t *testing.T) { + cases := []struct { + testDescription string + headers map[string][]string + options [][]options.TokenStringOption + expectedToken string + expectedErrorContains string + }{ + { + testDescription: "empty headers", + headers: make(map[string][]string), + expectedToken: "", + expectedErrorContains: "Authorization header empty", + }, + { + testDescription: "Authorization header empty", + headers: map[string][]string{ + "Authorization": {}, + }, + expectedToken: "", + expectedErrorContains: "Authorization header empty", + }, + { + testDescription: "Authorization header empty string", + headers: map[string][]string{ + "Authorization": {""}, + }, + expectedToken: "", + expectedErrorContains: "Authorization header empty", + }, + { + testDescription: "Authorization header first empty string", + headers: map[string][]string{ + "Authorization": {"", "Bearer foobar"}, + }, + expectedToken: "", + expectedErrorContains: "Authorization header empty", + }, + { + testDescription: "Authorization header single component", + headers: map[string][]string{ + "Authorization": {"foo"}, + }, + expectedToken: "", + expectedErrorContains: "Authorization header does not begin with: Bearer ", + }, + { + testDescription: "Authorization header three component", + headers: map[string][]string{ + "Authorization": {"foo bar baz"}, + }, + expectedToken: "", + expectedErrorContains: "Authorization header does not begin with: Bearer ", + }, + { + testDescription: "Authorization header two components", + headers: map[string][]string{ + "Authorization": {"foo bar"}, + }, + expectedToken: "", + expectedErrorContains: "Authorization header does not begin with: Bearer ", + }, + { + testDescription: "Authorization header two components", + headers: map[string][]string{ + "Authorization": {"Bearer foobar"}, + }, + expectedToken: "foobar", + expectedErrorContains: "", + }, + { + testDescription: "test options", + headers: map[string][]string{ + "Foo": {"Bar_baz"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Foo"), + options.WithTokenStringTokenPrefix("Bar_"), + }, + }, + expectedToken: "baz", + expectedErrorContains: "", + }, + { + testDescription: "test multiple options second header", + headers: map[string][]string{ + "Too": {"Lar_kaz"}, + "Foo": {"Bar_baz"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Foo"), + options.WithTokenStringTokenPrefix("Bar_"), + }, + { + options.WithTokenStringHeaderName("Too"), + options.WithTokenStringTokenPrefix("Lar_"), + }, + }, + expectedToken: "baz", + expectedErrorContains: "", + }, + { + testDescription: "test multiple options first header", + headers: map[string][]string{ + "Too": {"Lar_kaz"}, + "Foo": {"Bar_baz"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Too"), + options.WithTokenStringTokenPrefix("Lar_"), + }, + { + options.WithTokenStringHeaderName("Foo"), + options.WithTokenStringTokenPrefix("Bar_"), + }, + }, + expectedToken: "kaz", + expectedErrorContains: "", + }, + { + testDescription: "websockets", + headers: map[string][]string{ + "Sec-WebSocket-Protocol": {"Foo.bar,Too.lar,Koo.nar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Sec-WebSocket-Protocol"), + options.WithTokenStringTokenPrefix("Too."), + options.WithTokenStringListSeparator(","), + }, + }, + expectedToken: "lar", + expectedErrorContains: "", + }, + { + testDescription: "websockets", + headers: map[string][]string{ + "Sec-WebSocket-Protocol": {"Foo.bar,Too.lar,Koo.nar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Sec-WebSocket-Protocol"), + options.WithTokenStringTokenPrefix("Baz."), + options.WithTokenStringListSeparator(","), + }, + }, + expectedToken: "", + expectedErrorContains: "no token found in list", + }, + { + testDescription: "authorization first and and then websockets", + headers: map[string][]string{ + "Authorization": {"Bearer foobar"}, + "Sec-WebSocket-Protocol": {"Foo.bar,Too.lar,Koo.nar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + }, + { + options.WithTokenStringHeaderName("Sec-WebSocket-Protocol"), + options.WithTokenStringTokenPrefix("Too."), + options.WithTokenStringListSeparator(","), + }, + }, + expectedToken: "foobar", + expectedErrorContains: "", + }, + { + testDescription: "websockets first and then authorization", + headers: map[string][]string{ + "Authorization": {"Bearer foobar"}, + "Sec-WebSocket-Protocol": {"Foo.bar,Too.lar,Koo.nar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Sec-WebSocket-Protocol"), + options.WithTokenStringTokenPrefix("Too."), + options.WithTokenStringListSeparator(","), + }, + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + }, + }, + expectedToken: "lar", + expectedErrorContains: "", + }, + { + testDescription: "websockets first and then authorization, but without a token in websockets", + headers: map[string][]string{ + "Authorization": {"Bearer foobar"}, + "Sec-WebSocket-Protocol": {"Foo.bar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Sec-WebSocket-Protocol"), + options.WithTokenStringTokenPrefix("Too."), + options.WithTokenStringListSeparator(","), + }, + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + }, + }, + expectedToken: "foobar", + expectedErrorContains: "", + }, + { + testDescription: "one header with PostExtractionFn", + headers: map[string][]string{ + "Authorization": {"Bearer Zm9vYmFy"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + options.WithTokenStringPostExtractionFn(func(s string) (string, error) { + bytes, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + + return string(bytes), nil + }), + }, + }, + expectedToken: "foobar", + expectedErrorContains: "", + }, + { + testDescription: "two headers with PostExtractionFn error", + headers: map[string][]string{ + "Foo": {"Bar_baz"}, + "Authorization": {"Bearer foobar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + options.WithTokenStringPostExtractionFn(func(s string) (string, error) { + return "", fmt.Errorf("fake error") + }), + }, + { + options.WithTokenStringHeaderName("Foo"), + options.WithTokenStringTokenPrefix("Bar_"), + }, + }, + expectedToken: "baz", + expectedErrorContains: "", + }, + { + testDescription: "one header with PostExtractionFn error", + headers: map[string][]string{ + "Authorization": {"Bearer foobar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + options.WithTokenStringPostExtractionFn(func(s string) (string, error) { + return "", fmt.Errorf("fake error") + }), + }, + }, + expectedToken: "", + expectedErrorContains: "fake error", + }, + { + testDescription: "one header with PostExtractionFn returns empty string", + headers: map[string][]string{ + "Authorization": {"Bearer foobar"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + options.WithTokenStringPostExtractionFn(func(s string) (string, error) { + return "", nil + }), + }, + }, + expectedToken: "", + expectedErrorContains: "post extraction function returned an empty token string", + }, + { + testDescription: "kubernetes websocket test", + headers: map[string][]string{ + "Sec-WebSocket-Protocol": {"foo,bar,base64url.bearer.authorization.k8s.io.Rm9vQmFyQmF6,baz,test"}, + }, + options: [][]options.TokenStringOption{ + { + options.WithTokenStringHeaderName("Authorization"), + options.WithTokenStringTokenPrefix("Bearer "), + }, + { + options.WithTokenStringHeaderName("Sec-WebSocket-Protocol"), + options.WithTokenStringTokenPrefix("base64url.bearer.authorization.k8s.io."), + options.WithTokenStringListSeparator(","), + options.WithTokenStringPostExtractionFn(func(s string) (string, error) { + bytes, err := base64.RawStdEncoding.DecodeString(s) + if err != nil { + return "", err + } + + return string(bytes), nil + }), + }, + }, + expectedToken: "FooBarBaz", + expectedErrorContains: "", + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + + for k, values := range c.headers { + for _, v := range values { + req.Header.Add(k, v) + } + } + + token, err := GetTokenString(req.Header.Get, c.options) + require.Equal(t, c.expectedToken, token) + + if c.expectedErrorContains == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.expectedErrorContains) + } + } +} + +func TestGetTokenFromString(t *testing.T) { + cases := []struct { + testDescription string + options []options.TokenStringOption + headerValue string + expectedToken string + expectedErrorContains string + }{ + { + testDescription: "default working", + headerValue: "Bearer foobar", + expectedToken: "foobar", + expectedErrorContains: "", + }, + { + testDescription: "empty header", + headerValue: "", + expectedToken: "", + expectedErrorContains: "header empty", + }, + { + testDescription: "header doesn't begin with 'Bearer '", + headerValue: "Foo_bar", + expectedToken: "", + expectedErrorContains: "header does not begin with", + }, + { + testDescription: "header contains 'Bearer ' but nothing else", + headerValue: "Bearer ", + expectedToken: "", + expectedErrorContains: "header empty after prefix is trimmed", + }, + } + + for i, c := range cases { + t.Logf("Test iteration %d: %s", i, c.testDescription) + + opts := options.NewTokenString(c.options...) + + token, err := getTokenFromString(c.headerValue, opts) + require.Equal(t, c.expectedToken, token) + + if c.expectedErrorContains == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.expectedErrorContains) + } + } +}