mirror of https://github.com/k3s-io/k3s
Godeps: add Godpes for go-oidc.
parent
0318cb145c
commit
ad3f6e8e65
|
@ -105,6 +105,26 @@
|
|||
"Comment": "v2.0.0-13-g4cceaf7",
|
||||
"Rev": "4cceaf7283b76f27c4a732b20730dcdb61053bf5"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-oidc/http",
|
||||
"Rev": "ee7cb1fb480df22f7d8c4c90199e438e454ca3b6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-oidc/jose",
|
||||
"Rev": "ee7cb1fb480df22f7d8c4c90199e438e454ca3b6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-oidc/key",
|
||||
"Rev": "ee7cb1fb480df22f7d8c4c90199e438e454ca3b6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-oidc/oauth2",
|
||||
"Rev": "ee7cb1fb480df22f7d8c4c90199e438e454ca3b6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-oidc/oidc",
|
||||
"Rev": "ee7cb1fb480df22f7d8c4c90199e438e454ca3b6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-semver/semver",
|
||||
"Rev": "6fe83ccda8fb9b7549c9ab4ba47f47858bc950aa"
|
||||
|
@ -124,6 +144,22 @@
|
|||
"Comment": "v2-27-g97e243d",
|
||||
"Rev": "97e243d21a8e232e9d8af38ba2366dfcfceebeba"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/capnslog",
|
||||
"Rev": "fa94270d4bac0d8ae5dc6b71894e251aada93f74"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/health",
|
||||
"Rev": "fa94270d4bac0d8ae5dc6b71894e251aada93f74"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/httputil",
|
||||
"Rev": "fa94270d4bac0d8ae5dc6b71894e251aada93f74"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/timeutil",
|
||||
"Rev": "fa94270d4bac0d8ae5dc6b71894e251aada93f74"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/cpuguy83/go-md2man/md2man",
|
||||
"Comment": "v1.0.3-2-g71acacd",
|
||||
|
@ -347,6 +383,10 @@
|
|||
"Comment": "v0.8.8",
|
||||
"Rev": "afde71eb1740fd763ab9450e1f700ba0e53c36d0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jonboulle/clockwork",
|
||||
"Rev": "3f831b65b61282ba6bece21b91beea2edc4c887a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
"Rev": "772f5c38e468398c4511514f4f6aa9a4185bc0a0"
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type HandlerClient struct {
|
||||
Handler http.Handler
|
||||
}
|
||||
|
||||
func (hc *HandlerClient) Do(r *http.Request) (*http.Response, error) {
|
||||
w := httptest.NewRecorder()
|
||||
hc.Handler.ServeHTTP(w, r)
|
||||
|
||||
resp := http.Response{
|
||||
StatusCode: w.Code,
|
||||
Header: w.Header(),
|
||||
Body: ioutil.NopCloser(w.Body),
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
type RequestRecorder struct {
|
||||
Response *http.Response
|
||||
Error error
|
||||
|
||||
Request *http.Request
|
||||
}
|
||||
|
||||
func (rr *RequestRecorder) Do(req *http.Request) (*http.Response, error) {
|
||||
rr.Request = req
|
||||
|
||||
if rr.Response == nil && rr.Error == nil {
|
||||
panic("RequestRecorder Response and Error cannot both be nil")
|
||||
} else if rr.Response != nil && rr.Error != nil {
|
||||
panic("RequestRecorder Response and Error cannot both be non-nil")
|
||||
}
|
||||
|
||||
return rr.Response, rr.Error
|
||||
}
|
||||
|
||||
func (rr *RequestRecorder) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return rr.Do(req)
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
var (
|
||||
log = capnslog.NewPackageLogger("github.com/coreos/go-oidc", "http")
|
||||
)
|
||||
|
||||
func WriteError(w http.ResponseWriter, code int, msg string) {
|
||||
e := struct {
|
||||
Error string `json:"error"`
|
||||
}{
|
||||
Error: msg,
|
||||
}
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Errorf("Failed marshaling %#v to JSON: %v", e, err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// BasicAuth parses a username and password from the request's
|
||||
// Authorization header. This was pulled from golang master:
|
||||
// https://codereview.appspot.com/76540043
|
||||
func BasicAuth(r *http.Request) (username, password string, ok bool) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Basic ") {
|
||||
return
|
||||
}
|
||||
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic "))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cs := string(c)
|
||||
s := strings.IndexByte(cs, ':')
|
||||
if s < 0 {
|
||||
return
|
||||
}
|
||||
return cs[:s], cs[s+1:], true
|
||||
}
|
||||
|
||||
func cacheControlMaxAge(hdr string) (time.Duration, bool, error) {
|
||||
for _, field := range strings.Split(hdr, ",") {
|
||||
parts := strings.SplitN(strings.TrimSpace(field), "=", 2)
|
||||
k := strings.ToLower(strings.TrimSpace(parts[0]))
|
||||
if k != "max-age" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(parts) == 1 {
|
||||
return 0, false, errors.New("max-age has no value")
|
||||
}
|
||||
|
||||
v := strings.TrimSpace(parts[1])
|
||||
if v == "" {
|
||||
return 0, false, errors.New("max-age has empty value")
|
||||
}
|
||||
|
||||
age, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
if age <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
return time.Duration(age) * time.Second, true, nil
|
||||
}
|
||||
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
func expires(date, expires string) (time.Duration, bool, error) {
|
||||
if date == "" || expires == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
te, err := time.Parse(time.RFC1123, expires)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
td, err := time.Parse(time.RFC1123, date)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
ttl := te.Sub(td)
|
||||
|
||||
// headers indicate data already expired, caller should not
|
||||
// have to care about this case
|
||||
if ttl <= 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
return ttl, true, nil
|
||||
}
|
||||
|
||||
func Cacheable(hdr http.Header) (time.Duration, bool, error) {
|
||||
ttl, ok, err := cacheControlMaxAge(hdr.Get("Cache-Control"))
|
||||
if err != nil || ok {
|
||||
return ttl, ok, err
|
||||
}
|
||||
|
||||
return expires(hdr.Get("Date"), hdr.Get("Expires"))
|
||||
}
|
||||
|
||||
// MergeQuery appends additional query values to an existing URL.
|
||||
func MergeQuery(u url.URL, q url.Values) url.URL {
|
||||
uv := u.Query()
|
||||
for k, vs := range q {
|
||||
for _, v := range vs {
|
||||
uv.Add(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = uv.Encode()
|
||||
return u
|
||||
}
|
||||
|
||||
// NewResourceLocation appends a resource id to the end of the requested URL path.
|
||||
func NewResourceLocation(reqURL *url.URL, id string) string {
|
||||
var u url.URL
|
||||
u = *reqURL
|
||||
u.Path = path.Join(u.Path, id)
|
||||
u.RawQuery = ""
|
||||
u.Fragment = ""
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// CopyRequest returns a clone of the provided *http.Request.
|
||||
// The returned object is a shallow copy of the struct and a
|
||||
// deep copy of its Header field.
|
||||
func CopyRequest(r *http.Request) *http.Request {
|
||||
r2 := *r
|
||||
r2.Header = make(http.Header)
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = s
|
||||
}
|
||||
return &r2
|
||||
}
|
380
Godeps/_workspace/src/github.com/coreos/go-oidc/http/http_test.go
generated
vendored
Normal file
380
Godeps/_workspace/src/github.com/coreos/go-oidc/http/http_test.go
generated
vendored
Normal file
|
@ -0,0 +1,380 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCacheControlMaxAgeSuccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
hdr string
|
||||
wantAge time.Duration
|
||||
wantOK bool
|
||||
}{
|
||||
{"max-age=12", 12 * time.Second, true},
|
||||
{"max-age=-12", 0, false},
|
||||
{"max-age=0", 0, false},
|
||||
{"public, max-age=12", 12 * time.Second, true},
|
||||
{"public, max-age=40192, must-revalidate", 40192 * time.Second, true},
|
||||
{"public, not-max-age=12, must-revalidate", time.Duration(0), false},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
maxAge, ok, err := cacheControlMaxAge(tt.hdr)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: err=%v", i, err)
|
||||
}
|
||||
if tt.wantAge != maxAge {
|
||||
t.Errorf("case %d: want=%d got=%d", i, tt.wantAge, maxAge)
|
||||
}
|
||||
if tt.wantOK != ok {
|
||||
t.Errorf("case %d: incorrect ok value: want=%t got=%t", i, tt.wantOK, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheControlMaxAgeFail(t *testing.T) {
|
||||
tests := []string{
|
||||
"max-age=aasdf",
|
||||
"max-age=",
|
||||
"max-age",
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, ok, err := cacheControlMaxAge(tt)
|
||||
if ok {
|
||||
t.Errorf("case %d: want ok=false, got true", i)
|
||||
}
|
||||
if err == nil {
|
||||
t.Errorf("case %d: want non-nil err", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
u string
|
||||
q url.Values
|
||||
w string
|
||||
}{
|
||||
// No values
|
||||
{
|
||||
u: "http://example.com",
|
||||
q: nil,
|
||||
w: "http://example.com",
|
||||
},
|
||||
// No additional values
|
||||
{
|
||||
u: "http://example.com?foo=bar",
|
||||
q: nil,
|
||||
w: "http://example.com?foo=bar",
|
||||
},
|
||||
// Simple addition
|
||||
{
|
||||
u: "http://example.com",
|
||||
q: url.Values{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
w: "http://example.com?foo=bar",
|
||||
},
|
||||
// Addition with existing values
|
||||
{
|
||||
u: "http://example.com?dog=boo",
|
||||
q: url.Values{
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
w: "http://example.com?dog=boo&foo=bar",
|
||||
},
|
||||
// Merge
|
||||
{
|
||||
u: "http://example.com?dog=boo",
|
||||
q: url.Values{
|
||||
"dog": []string{"elroy"},
|
||||
},
|
||||
w: "http://example.com?dog=boo&dog=elroy",
|
||||
},
|
||||
// Add and merge
|
||||
{
|
||||
u: "http://example.com?dog=boo",
|
||||
q: url.Values{
|
||||
"dog": []string{"elroy"},
|
||||
"foo": []string{"bar"},
|
||||
},
|
||||
w: "http://example.com?dog=boo&dog=elroy&foo=bar",
|
||||
},
|
||||
// Multivalue merge
|
||||
{
|
||||
u: "http://example.com?dog=boo",
|
||||
q: url.Values{
|
||||
"dog": []string{"elroy", "penny"},
|
||||
},
|
||||
w: "http://example.com?dog=boo&dog=elroy&dog=penny",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
ur, err := url.Parse(tt.u)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: failed parsing test url: %v, error: %v", i, tt.u, err)
|
||||
}
|
||||
|
||||
got := MergeQuery(*ur, tt.q)
|
||||
want, err := url.Parse(tt.w)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: failed parsing want url: %v, error: %v", i, tt.w, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*want, got) {
|
||||
t.Errorf("case %d: want: %v, got: %v", i, *want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiresPass(t *testing.T) {
|
||||
tests := []struct {
|
||||
date string
|
||||
exp string
|
||||
wantTTL time.Duration
|
||||
wantOK bool
|
||||
}{
|
||||
// Expires and Date properly set
|
||||
{
|
||||
date: "Thu, 01 Dec 1983 22:00:00 GMT",
|
||||
exp: "Fri, 02 Dec 1983 01:00:00 GMT",
|
||||
wantTTL: 10800 * time.Second,
|
||||
wantOK: true,
|
||||
},
|
||||
// empty headers
|
||||
{
|
||||
date: "",
|
||||
exp: "",
|
||||
wantOK: false,
|
||||
},
|
||||
// lack of Expirs short-ciruits Date parsing
|
||||
{
|
||||
date: "foo",
|
||||
exp: "",
|
||||
wantOK: false,
|
||||
},
|
||||
// lack of Date short-ciruits Expires parsing
|
||||
{
|
||||
date: "",
|
||||
exp: "foo",
|
||||
wantOK: false,
|
||||
},
|
||||
// no Date
|
||||
{
|
||||
exp: "Thu, 01 Dec 1983 22:00:00 GMT",
|
||||
wantTTL: 0,
|
||||
wantOK: false,
|
||||
},
|
||||
// no Expires
|
||||
{
|
||||
date: "Thu, 01 Dec 1983 22:00:00 GMT",
|
||||
wantTTL: 0,
|
||||
wantOK: false,
|
||||
},
|
||||
// Expires < Date
|
||||
{
|
||||
date: "Fri, 02 Dec 1983 01:00:00 GMT",
|
||||
exp: "Thu, 01 Dec 1983 22:00:00 GMT",
|
||||
wantTTL: 0,
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
ttl, ok, err := expires(tt.date, tt.exp)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: err=%v", i, err)
|
||||
}
|
||||
if tt.wantTTL != ttl {
|
||||
t.Errorf("case %d: want=%d got=%d", i, tt.wantTTL, ttl)
|
||||
}
|
||||
if tt.wantOK != ok {
|
||||
t.Errorf("case %d: incorrect ok value: want=%t got=%t", i, tt.wantOK, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiresFail(t *testing.T) {
|
||||
tests := []struct {
|
||||
date string
|
||||
exp string
|
||||
}{
|
||||
// malformed Date header
|
||||
{
|
||||
date: "foo",
|
||||
exp: "Fri, 02 Dec 1983 01:00:00 GMT",
|
||||
},
|
||||
// malformed exp header
|
||||
{
|
||||
date: "Fri, 02 Dec 1983 01:00:00 GMT",
|
||||
exp: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, _, err := expires(tt.date, tt.exp)
|
||||
if err == nil {
|
||||
t.Errorf("case %d: expected non-nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheablePass(t *testing.T) {
|
||||
tests := []struct {
|
||||
headers http.Header
|
||||
wantTTL time.Duration
|
||||
wantOK bool
|
||||
}{
|
||||
// valid Cache-Control
|
||||
{
|
||||
headers: http.Header{
|
||||
"Cache-Control": []string{"max-age=100"},
|
||||
},
|
||||
wantTTL: 100 * time.Second,
|
||||
wantOK: true,
|
||||
},
|
||||
// valid Date/Expires
|
||||
{
|
||||
headers: http.Header{
|
||||
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
|
||||
"Expires": []string{"Fri, 02 Dec 1983 01:00:00 GMT"},
|
||||
},
|
||||
wantTTL: 10800 * time.Second,
|
||||
wantOK: true,
|
||||
},
|
||||
// Cache-Control supersedes Date/Expires
|
||||
{
|
||||
headers: http.Header{
|
||||
"Cache-Control": []string{"max-age=100"},
|
||||
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
|
||||
"Expires": []string{"Fri, 02 Dec 1983 01:00:00 GMT"},
|
||||
},
|
||||
wantTTL: 100 * time.Second,
|
||||
wantOK: true,
|
||||
},
|
||||
// no caching headers
|
||||
{
|
||||
headers: http.Header{},
|
||||
wantOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
ttl, ok, err := Cacheable(tt.headers)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: err=%v", i, err)
|
||||
continue
|
||||
}
|
||||
if tt.wantTTL != ttl {
|
||||
t.Errorf("case %d: want=%d got=%d", i, tt.wantTTL, ttl)
|
||||
}
|
||||
if tt.wantOK != ok {
|
||||
t.Errorf("case %d: incorrect ok value: want=%t got=%t", i, tt.wantOK, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheableFail(t *testing.T) {
|
||||
tests := []http.Header{
|
||||
// invalid Cache-Control short-circuits
|
||||
http.Header{
|
||||
"Cache-Control": []string{"max-age"},
|
||||
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
|
||||
"Expires": []string{"Fri, 02 Dec 1983 01:00:00 GMT"},
|
||||
},
|
||||
// no Cache-Control, invalid Expires
|
||||
http.Header{
|
||||
"Date": []string{"Thu, 01 Dec 1983 22:00:00 GMT"},
|
||||
"Expires": []string{"boo"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, _, err := Cacheable(tt)
|
||||
if err == nil {
|
||||
t.Errorf("case %d: want non-nil err", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewResourceLocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
ru *url.URL
|
||||
id string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
ru: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
},
|
||||
id: "foo",
|
||||
want: "http://example.com/foo",
|
||||
},
|
||||
// https
|
||||
{
|
||||
ru: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
},
|
||||
id: "foo",
|
||||
want: "https://example.com/foo",
|
||||
},
|
||||
// with path
|
||||
{
|
||||
ru: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
Path: "one/two/three",
|
||||
},
|
||||
id: "foo",
|
||||
want: "http://example.com/one/two/three/foo",
|
||||
},
|
||||
// with fragment
|
||||
{
|
||||
ru: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
Fragment: "frag",
|
||||
},
|
||||
id: "foo",
|
||||
want: "http://example.com/foo",
|
||||
},
|
||||
// with query
|
||||
{
|
||||
ru: &url.URL{
|
||||
Scheme: "http",
|
||||
Host: "example.com",
|
||||
RawQuery: "dog=elroy",
|
||||
},
|
||||
id: "foo",
|
||||
want: "http://example.com/foo",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := NewResourceLocation(tt.ru, tt.id)
|
||||
if tt.want != got {
|
||||
t.Errorf("case %d: want=%s, got=%s", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyRequest(t *testing.T) {
|
||||
r1, err := http.NewRequest("GET", "http://example.com", strings.NewReader("foo"))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
r2 := CopyRequest(r1)
|
||||
if !reflect.DeepEqual(r1, r2) {
|
||||
t.Fatalf("Result of CopyRequest incorrect: %#v != %#v", r1, r2)
|
||||
}
|
||||
}
|
14
Godeps/_workspace/src/github.com/coreos/go-oidc/http/middleware.go
generated
vendored
Normal file
14
Godeps/_workspace/src/github.com/coreos/go-oidc/http/middleware.go
generated
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type LoggingMiddleware struct {
|
||||
Next http.Handler
|
||||
}
|
||||
|
||||
func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Infof("HTTP %s %v", r.Method, r.URL)
|
||||
l.Next.ServeHTTP(w, r)
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ParseNonEmptyURL checks that a string is a parsable URL which is also not empty
|
||||
// since `url.Parse("")` does not return an error. Must contian a scheme and a host.
|
||||
func ParseNonEmptyURL(u string) (*url.URL, error) {
|
||||
if u == "" {
|
||||
return nil, errors.New("url is empty")
|
||||
}
|
||||
|
||||
ur, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ur.Scheme == "" {
|
||||
return nil, errors.New("url scheme is empty")
|
||||
}
|
||||
|
||||
if ur.Host == "" {
|
||||
return nil, errors.New("url host is empty")
|
||||
}
|
||||
|
||||
return ur, nil
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseNonEmptyURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
u string
|
||||
ok bool
|
||||
}{
|
||||
{"", false},
|
||||
{"http://", false},
|
||||
{"example.com", false},
|
||||
{"example", false},
|
||||
{"http://example", true},
|
||||
{"http://example:1234", true},
|
||||
{"http://example.com", true},
|
||||
{"http://example.com:1234", true},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
u, err := ParseNonEmptyURL(tt.u)
|
||||
if err != nil {
|
||||
t.Logf("err: %v", err)
|
||||
if tt.ok {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if !tt.ok {
|
||||
t.Errorf("case %d: expected error but got none", i)
|
||||
continue
|
||||
}
|
||||
|
||||
uu, err := url.Parse(tt.u)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if uu.String() != u.String() {
|
||||
t.Errorf("case %d: incorrect url value, want: %q, got: %q", i, uu.String(), u.String())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Claims map[string]interface{}
|
||||
|
||||
func (c Claims) Add(name string, value interface{}) {
|
||||
c[name] = value
|
||||
}
|
||||
|
||||
func (c Claims) StringClaim(name string) (string, bool, error) {
|
||||
cl, ok := c[name]
|
||||
if !ok {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
v, ok := cl.(string)
|
||||
if !ok {
|
||||
return "", false, fmt.Errorf("unable to parse claim as string: %v", name)
|
||||
}
|
||||
|
||||
return v, true, nil
|
||||
}
|
||||
|
||||
func (c Claims) Int64Claim(name string) (int64, bool, error) {
|
||||
cl, ok := c[name]
|
||||
if !ok {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
v, ok := cl.(int64)
|
||||
if !ok {
|
||||
vf, ok := cl.(float64)
|
||||
if !ok {
|
||||
return 0, false, fmt.Errorf("unable to parse claim as int64: %v", name)
|
||||
}
|
||||
v = int64(vf)
|
||||
}
|
||||
|
||||
return v, true, nil
|
||||
}
|
||||
|
||||
func (c Claims) TimeClaim(name string) (time.Time, bool, error) {
|
||||
v, ok, err := c.Int64Claim(name)
|
||||
if !ok || err != nil {
|
||||
return time.Time{}, ok, err
|
||||
}
|
||||
|
||||
return time.Unix(v, 0).UTC(), true, nil
|
||||
}
|
||||
|
||||
func decodeClaims(payload []byte) (Claims, error) {
|
||||
var c Claims
|
||||
if err := json.Unmarshal(payload, &c); err != nil {
|
||||
return nil, fmt.Errorf("malformed JWT claims, unable to decode: %v", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func marshalClaims(c Claims) ([]byte, error) {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func encodeClaims(c Claims) (string, error) {
|
||||
b, err := marshalClaims(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return encodeSegment(b), nil
|
||||
}
|
240
Godeps/_workspace/src/github.com/coreos/go-oidc/jose/claims_test.go
generated
vendored
Normal file
240
Godeps/_workspace/src/github.com/coreos/go-oidc/jose/claims_test.go
generated
vendored
Normal file
|
@ -0,0 +1,240 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
tests := []struct {
|
||||
cl Claims
|
||||
key string
|
||||
ok bool
|
||||
err bool
|
||||
val string
|
||||
}{
|
||||
// ok, no err, claim exists
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": "bar",
|
||||
},
|
||||
key: "foo",
|
||||
val: "bar",
|
||||
ok: true,
|
||||
err: false,
|
||||
},
|
||||
// no claims
|
||||
{
|
||||
cl: Claims{},
|
||||
key: "foo",
|
||||
val: "",
|
||||
ok: false,
|
||||
err: false,
|
||||
},
|
||||
// missing claim
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": "bar",
|
||||
},
|
||||
key: "xxx",
|
||||
val: "",
|
||||
ok: false,
|
||||
err: false,
|
||||
},
|
||||
// unparsable: type
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": struct{}{},
|
||||
},
|
||||
key: "foo",
|
||||
val: "",
|
||||
ok: false,
|
||||
err: true,
|
||||
},
|
||||
// unparsable: nil value
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": nil,
|
||||
},
|
||||
key: "foo",
|
||||
val: "",
|
||||
ok: false,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
val, ok, err := tt.cl.StringClaim(tt.key)
|
||||
|
||||
if tt.err && err == nil {
|
||||
t.Errorf("case %d: want err=non-nil, got err=nil", i)
|
||||
} else if !tt.err && err != nil {
|
||||
t.Errorf("case %d: want err=nil, got err=%v", i, err)
|
||||
}
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
|
||||
}
|
||||
|
||||
if tt.val != val {
|
||||
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
cl Claims
|
||||
key string
|
||||
ok bool
|
||||
err bool
|
||||
val int64
|
||||
}{
|
||||
// ok, no err, claim exists
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": int64(100),
|
||||
},
|
||||
key: "foo",
|
||||
val: int64(100),
|
||||
ok: true,
|
||||
err: false,
|
||||
},
|
||||
// no claims
|
||||
{
|
||||
cl: Claims{},
|
||||
key: "foo",
|
||||
val: 0,
|
||||
ok: false,
|
||||
err: false,
|
||||
},
|
||||
// missing claim
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": "bar",
|
||||
},
|
||||
key: "xxx",
|
||||
val: 0,
|
||||
ok: false,
|
||||
err: false,
|
||||
},
|
||||
// unparsable: type
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": struct{}{},
|
||||
},
|
||||
key: "foo",
|
||||
val: 0,
|
||||
ok: false,
|
||||
err: true,
|
||||
},
|
||||
// unparsable: nil value
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": nil,
|
||||
},
|
||||
key: "foo",
|
||||
val: 0,
|
||||
ok: false,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
val, ok, err := tt.cl.Int64Claim(tt.key)
|
||||
|
||||
if tt.err && err == nil {
|
||||
t.Errorf("case %d: want err=non-nil, got err=nil", i)
|
||||
} else if !tt.err && err != nil {
|
||||
t.Errorf("case %d: want err=nil, got err=%v", i, err)
|
||||
}
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
|
||||
}
|
||||
|
||||
if tt.val != val {
|
||||
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTime(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
unixNow := now.Unix()
|
||||
|
||||
tests := []struct {
|
||||
cl Claims
|
||||
key string
|
||||
ok bool
|
||||
err bool
|
||||
val time.Time
|
||||
}{
|
||||
// ok, no err, claim exists
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": unixNow,
|
||||
},
|
||||
key: "foo",
|
||||
val: time.Unix(now.Unix(), 0).UTC(),
|
||||
ok: true,
|
||||
err: false,
|
||||
},
|
||||
// no claims
|
||||
{
|
||||
cl: Claims{},
|
||||
key: "foo",
|
||||
val: time.Time{},
|
||||
ok: false,
|
||||
err: false,
|
||||
},
|
||||
// missing claim
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": "bar",
|
||||
},
|
||||
key: "xxx",
|
||||
val: time.Time{},
|
||||
ok: false,
|
||||
err: false,
|
||||
},
|
||||
// unparsable: type
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": struct{}{},
|
||||
},
|
||||
key: "foo",
|
||||
val: time.Time{},
|
||||
ok: false,
|
||||
err: true,
|
||||
},
|
||||
// unparsable: nil value
|
||||
{
|
||||
cl: Claims{
|
||||
"foo": nil,
|
||||
},
|
||||
key: "foo",
|
||||
val: time.Time{},
|
||||
ok: false,
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
val, ok, err := tt.cl.TimeClaim(tt.key)
|
||||
|
||||
if tt.err && err == nil {
|
||||
t.Errorf("case %d: want err=non-nil, got err=nil", i)
|
||||
} else if !tt.err && err != nil {
|
||||
t.Errorf("case %d: want err=nil, got err=%v", i, err)
|
||||
}
|
||||
|
||||
if tt.ok != ok {
|
||||
t.Errorf("case %d: want ok=%v, got ok=%v", i, tt.ok, ok)
|
||||
}
|
||||
|
||||
if tt.val != val {
|
||||
t.Errorf("case %d: want val=%v, got val=%v", i, tt.val, val)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderMediaType = "typ"
|
||||
HeaderKeyAlgorithm = "alg"
|
||||
HeaderKeyID = "kid"
|
||||
)
|
||||
|
||||
type JOSEHeader map[string]string
|
||||
|
||||
func (j JOSEHeader) Validate() error {
|
||||
if _, exists := j[HeaderKeyAlgorithm]; !exists {
|
||||
return fmt.Errorf("header missing %q parameter", HeaderKeyAlgorithm)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeHeader(seg string) (JOSEHeader, error) {
|
||||
b, err := decodeSegment(seg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var h JOSEHeader
|
||||
err = json.Unmarshal(b, &h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func encodeHeader(h JOSEHeader) (string, error) {
|
||||
b, err := json.Marshal(h)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return encodeSegment(b), nil
|
||||
}
|
||||
|
||||
// Decode JWT specific base64url encoding with padding stripped
|
||||
func decodeSegment(seg string) ([]byte, error) {
|
||||
if l := len(seg) % 4; l != 0 {
|
||||
seg += strings.Repeat("=", 4-l)
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(seg)
|
||||
}
|
||||
|
||||
// Encode JWT specific base64url encoding with padding stripped
|
||||
func encodeSegment(seg []byte) string {
|
||||
return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JSON Web Key
|
||||
// https://tools.ietf.org/html/draft-ietf-jose-json-web-key-36#page-5
|
||||
type JWK struct {
|
||||
ID string
|
||||
Type string
|
||||
Alg string
|
||||
Use string
|
||||
Exponent int
|
||||
Modulus *big.Int
|
||||
Secret []byte
|
||||
}
|
||||
|
||||
type jwkJSON struct {
|
||||
ID string `json:"kid"`
|
||||
Type string `json:"kty"`
|
||||
Alg string `json:"alg"`
|
||||
Use string `json:"use"`
|
||||
Exponent string `json:"e"`
|
||||
Modulus string `json:"n"`
|
||||
}
|
||||
|
||||
func (j *JWK) MarshalJSON() ([]byte, error) {
|
||||
t := jwkJSON{
|
||||
ID: j.ID,
|
||||
Type: j.Type,
|
||||
Alg: j.Alg,
|
||||
Use: j.Use,
|
||||
Exponent: encodeExponent(j.Exponent),
|
||||
Modulus: encodeModulus(j.Modulus),
|
||||
}
|
||||
|
||||
return json.Marshal(&t)
|
||||
}
|
||||
|
||||
func (j *JWK) UnmarshalJSON(data []byte) error {
|
||||
var t jwkJSON
|
||||
err := json.Unmarshal(data, &t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e, err := decodeExponent(t.Exponent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n, err := decodeModulus(t.Modulus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
j.ID = t.ID
|
||||
j.Type = t.Type
|
||||
j.Alg = t.Alg
|
||||
j.Use = t.Use
|
||||
j.Exponent = e
|
||||
j.Modulus = n
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeExponent(e string) (int, error) {
|
||||
decE, err := decodeBase64URLPaddingOptional(e)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var eBytes []byte
|
||||
if len(decE) < 8 {
|
||||
eBytes = make([]byte, 8-len(decE), 8)
|
||||
eBytes = append(eBytes, decE...)
|
||||
} else {
|
||||
eBytes = decE
|
||||
}
|
||||
eReader := bytes.NewReader(eBytes)
|
||||
var E uint64
|
||||
err = binary.Read(eReader, binary.BigEndian, &E)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(E), nil
|
||||
}
|
||||
|
||||
func encodeExponent(e int) string {
|
||||
b := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(b, uint64(e))
|
||||
var idx int
|
||||
for ; idx < 8; idx++ {
|
||||
if b[idx] != 0x0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b[idx:])
|
||||
}
|
||||
|
||||
// Turns a URL encoded modulus of a key into a big int.
|
||||
func decodeModulus(n string) (*big.Int, error) {
|
||||
decN, err := decodeBase64URLPaddingOptional(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
N := big.NewInt(0)
|
||||
N.SetBytes(decN)
|
||||
return N, nil
|
||||
}
|
||||
|
||||
func encodeModulus(n *big.Int) string {
|
||||
return base64.URLEncoding.EncodeToString(n.Bytes())
|
||||
}
|
||||
|
||||
// decodeBase64URLPaddingOptional decodes Base64 whether there is padding or not.
|
||||
// The stdlib version currently doesn't handle this.
|
||||
// We can get rid of this is if this bug:
|
||||
// https://github.com/golang/go/issues/4237
|
||||
// ever closes.
|
||||
func decodeBase64URLPaddingOptional(e string) ([]byte, error) {
|
||||
if m := len(e) % 4; m != 0 {
|
||||
e += strings.Repeat("=", 4-m)
|
||||
}
|
||||
return base64.URLEncoding.DecodeString(e)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeBase64URLPaddingOptional(t *testing.T) {
|
||||
tests := []struct {
|
||||
encoded string
|
||||
decoded string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
// With padding
|
||||
encoded: "VGVjdG9uaWM=",
|
||||
decoded: "Tectonic",
|
||||
},
|
||||
{
|
||||
// Without padding
|
||||
encoded: "VGVjdG9uaWM",
|
||||
decoded: "Tectonic",
|
||||
},
|
||||
{
|
||||
// Even More padding
|
||||
encoded: "VGVjdG9uaQ==",
|
||||
decoded: "Tectoni",
|
||||
},
|
||||
{
|
||||
// And take it away!
|
||||
encoded: "VGVjdG9uaQ",
|
||||
decoded: "Tectoni",
|
||||
},
|
||||
{
|
||||
// Too much padding.
|
||||
encoded: "VGVjdG9uaWNh=",
|
||||
decoded: "",
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
// Too much padding.
|
||||
encoded: "VGVjdG9uaWNh=",
|
||||
decoded: "",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got, err := decodeBase64URLPaddingOptional(tt.encoded)
|
||||
if tt.err {
|
||||
if err == nil {
|
||||
t.Errorf("case %d: expected non-nil err", i)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("case %d: want nil err, got: %v", i, err)
|
||||
}
|
||||
|
||||
if string(got) != tt.decoded {
|
||||
t.Errorf("case %d: want=%q, got=%q", i, tt.decoded, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type JWS struct {
|
||||
RawHeader string
|
||||
Header JOSEHeader
|
||||
RawPayload string
|
||||
Payload []byte
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
// Given a raw encoded JWS token parses it and verifies the structure.
|
||||
func ParseJWS(raw string) (JWS, error) {
|
||||
parts := strings.Split(raw, ".")
|
||||
if len(parts) != 3 {
|
||||
return JWS{}, fmt.Errorf("malformed JWS, only %d segments", len(parts))
|
||||
}
|
||||
|
||||
rawSig := parts[2]
|
||||
jws := JWS{
|
||||
RawHeader: parts[0],
|
||||
RawPayload: parts[1],
|
||||
}
|
||||
|
||||
header, err := decodeHeader(jws.RawHeader)
|
||||
if err != nil {
|
||||
return JWS{}, fmt.Errorf("malformed JWS, unable to decode header, %s", err)
|
||||
}
|
||||
if err = header.Validate(); err != nil {
|
||||
return JWS{}, fmt.Errorf("malformed JWS, %s", err)
|
||||
}
|
||||
jws.Header = header
|
||||
|
||||
payload, err := decodeSegment(jws.RawPayload)
|
||||
if err != nil {
|
||||
return JWS{}, fmt.Errorf("malformed JWS, unable to decode payload: %s", err)
|
||||
}
|
||||
jws.Payload = payload
|
||||
|
||||
sig, err := decodeSegment(rawSig)
|
||||
if err != nil {
|
||||
return JWS{}, fmt.Errorf("malformed JWS, unable to decode signature: %s", err)
|
||||
}
|
||||
jws.Signature = sig
|
||||
|
||||
return jws, nil
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testCase struct{ t string }
|
||||
|
||||
var validInput []testCase
|
||||
|
||||
var invalidInput []testCase
|
||||
|
||||
func init() {
|
||||
validInput = []testCase{
|
||||
{
|
||||
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
||||
},
|
||||
}
|
||||
|
||||
invalidInput = []testCase{
|
||||
// empty
|
||||
{
|
||||
"",
|
||||
},
|
||||
// undecodeable
|
||||
{
|
||||
"aaa.bbb.ccc",
|
||||
},
|
||||
// missing parts
|
||||
{
|
||||
"aaa",
|
||||
},
|
||||
// missing parts
|
||||
{
|
||||
"aaa.bbb",
|
||||
},
|
||||
// too many parts
|
||||
{
|
||||
"aaa.bbb.ccc.ddd",
|
||||
},
|
||||
// invalid header
|
||||
// EncodeHeader(map[string]string{"foo": "bar"})
|
||||
{
|
||||
"eyJmb28iOiJiYXIifQ.bbb.ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJWS(t *testing.T) {
|
||||
for i, tt := range validInput {
|
||||
jws, err := ParseJWS(tt.t)
|
||||
if err != nil {
|
||||
t.Errorf("test: %d. expected: valid, actual: invalid", i)
|
||||
}
|
||||
|
||||
expectedHeader := strings.Split(tt.t, ".")[0]
|
||||
if jws.RawHeader != expectedHeader {
|
||||
t.Errorf("test: %d. expected: %s, actual: %s", i, expectedHeader, jws.RawHeader)
|
||||
}
|
||||
|
||||
expectedPayload := strings.Split(tt.t, ".")[1]
|
||||
if jws.RawPayload != expectedPayload {
|
||||
t.Errorf("test: %d. expected: %s, actual: %s", i, expectedPayload, jws.RawPayload)
|
||||
}
|
||||
}
|
||||
|
||||
for i, tt := range invalidInput {
|
||||
_, err := ParseJWS(tt.t)
|
||||
if err == nil {
|
||||
t.Errorf("test: %d. expected: invalid, actual: valid", i)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type JWT JWS
|
||||
|
||||
func ParseJWT(token string) (jwt JWT, err error) {
|
||||
jws, err := ParseJWS(token)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return JWT(jws), nil
|
||||
}
|
||||
|
||||
func NewJWT(header JOSEHeader, claims Claims) (jwt JWT, err error) {
|
||||
jwt = JWT{}
|
||||
|
||||
jwt.Header = header
|
||||
jwt.Header[HeaderMediaType] = "JWT"
|
||||
|
||||
claimBytes, err := marshalClaims(claims)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
jwt.Payload = claimBytes
|
||||
|
||||
eh, err := encodeHeader(header)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
jwt.RawHeader = eh
|
||||
|
||||
ec, err := encodeClaims(claims)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
jwt.RawPayload = ec
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (j *JWT) KeyID() (string, bool) {
|
||||
kID, ok := j.Header[HeaderKeyID]
|
||||
return kID, ok
|
||||
}
|
||||
|
||||
func (j *JWT) Claims() (Claims, error) {
|
||||
return decodeClaims(j.Payload)
|
||||
}
|
||||
|
||||
// Encoded data part of the token which may be signed.
|
||||
func (j *JWT) Data() string {
|
||||
return strings.Join([]string{j.RawHeader, j.RawPayload}, ".")
|
||||
}
|
||||
|
||||
// Full encoded JWT token string in format: header.claims.signature
|
||||
func (j *JWT) Encode() string {
|
||||
d := j.Data()
|
||||
s := encodeSegment(j.Signature)
|
||||
return strings.Join([]string{d, s}, ".")
|
||||
}
|
||||
|
||||
func NewSignedJWT(claims map[string]interface{}, s Signer) (*JWT, error) {
|
||||
header := JOSEHeader{
|
||||
HeaderKeyAlgorithm: s.Alg(),
|
||||
HeaderKeyID: s.ID(),
|
||||
}
|
||||
|
||||
jwt, err := NewJWT(header, Claims(claims))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sig, err := s.Sign([]byte(jwt.Data()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jwt.Signature = sig
|
||||
|
||||
return &jwt, nil
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJWT(t *testing.T) {
|
||||
tests := []struct {
|
||||
r string
|
||||
h JOSEHeader
|
||||
c Claims
|
||||
}{
|
||||
{
|
||||
// Example from JWT spec:
|
||||
// http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#ExampleJWT
|
||||
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
|
||||
JOSEHeader{
|
||||
HeaderMediaType: "JWT",
|
||||
HeaderKeyAlgorithm: "HS256",
|
||||
},
|
||||
Claims{
|
||||
"iss": "joe",
|
||||
// NOTE: test numbers must be floats for equality checks to work since values are converted form interface{} to float64 by default.
|
||||
"exp": 1300819380.0,
|
||||
"http://example.com/is_root": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
jwt, err := ParseJWT(tt.r)
|
||||
if err != nil {
|
||||
t.Errorf("raw token should parse. test: %d. expected: valid, actual: invalid. err=%v", i, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tt.h, jwt.Header) {
|
||||
t.Errorf("JOSE headers should match. test: %d. expected: %v, actual: %v", i, tt.h, jwt.Header)
|
||||
}
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
t.Errorf("test: %d. expected: valid claim parsing. err=%v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.c, claims) {
|
||||
t.Errorf("claims should match. test: %d. expected: %v, actual: %v", i, tt.c, claims)
|
||||
}
|
||||
|
||||
enc := jwt.Encode()
|
||||
if enc != tt.r {
|
||||
t.Errorf("encoded jwt should match raw jwt. test: %d. expected: %v, actual: %v", i, tt.r, enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJWTHeaderTyp(t *testing.T) {
|
||||
jwt, err := NewJWT(JOSEHeader{}, Claims{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want := "JWT"
|
||||
got := jwt.Header[HeaderMediaType]
|
||||
if want != got {
|
||||
t.Fatalf("Header %q incorrect: want=%s got=%s", HeaderMediaType, want, got)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNewJWTHeaderKeyID(t *testing.T) {
|
||||
jwt, err := NewJWT(JOSEHeader{HeaderKeyID: "foo"}, Claims{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want := "foo"
|
||||
got, ok := jwt.KeyID()
|
||||
if !ok {
|
||||
t.Fatalf("KeyID not set")
|
||||
} else if want != got {
|
||||
t.Fatalf("KeyID incorrect: want=%s got=%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewJWTHeaderKeyIDNotSet(t *testing.T) {
|
||||
jwt, err := NewJWT(JOSEHeader{}, Claims{})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := jwt.KeyID(); ok {
|
||||
t.Fatalf("KeyID set, but should not be")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Verifier interface {
|
||||
ID() string
|
||||
Alg() string
|
||||
Verify(sig []byte, data []byte) error
|
||||
}
|
||||
|
||||
type Signer interface {
|
||||
Verifier
|
||||
Sign(data []byte) (sig []byte, err error)
|
||||
}
|
||||
|
||||
func NewVerifier(jwk JWK) (Verifier, error) {
|
||||
if strings.ToUpper(jwk.Type) != "RSA" {
|
||||
return nil, fmt.Errorf("unsupported key type %q", jwk.Type)
|
||||
}
|
||||
|
||||
return NewVerifierRSA(jwk)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
_ "crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type VerifierHMAC struct {
|
||||
KeyID string
|
||||
Hash crypto.Hash
|
||||
Secret []byte
|
||||
}
|
||||
|
||||
type SignerHMAC struct {
|
||||
VerifierHMAC
|
||||
}
|
||||
|
||||
func NewVerifierHMAC(jwk JWK) (*VerifierHMAC, error) {
|
||||
if strings.ToUpper(jwk.Alg) != "HS256" {
|
||||
return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg)
|
||||
}
|
||||
|
||||
v := VerifierHMAC{
|
||||
KeyID: jwk.ID,
|
||||
Secret: jwk.Secret,
|
||||
Hash: crypto.SHA256,
|
||||
}
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func (v *VerifierHMAC) ID() string {
|
||||
return v.KeyID
|
||||
}
|
||||
|
||||
func (v *VerifierHMAC) Alg() string {
|
||||
return "HS256"
|
||||
}
|
||||
|
||||
func (v *VerifierHMAC) Verify(sig []byte, data []byte) error {
|
||||
h := hmac.New(v.Hash.New, v.Secret)
|
||||
h.Write(data)
|
||||
if !bytes.Equal(sig, h.Sum(nil)) {
|
||||
return errors.New("invalid hmac signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSignerHMAC(kid string, secret []byte) *SignerHMAC {
|
||||
return &SignerHMAC{
|
||||
VerifierHMAC: VerifierHMAC{
|
||||
KeyID: kid,
|
||||
Secret: secret,
|
||||
Hash: crypto.SHA256,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SignerHMAC) Sign(data []byte) ([]byte, error) {
|
||||
h := hmac.New(s.Hash.New, s.Secret)
|
||||
h.Write(data)
|
||||
return h.Sum(nil), nil
|
||||
}
|
85
Godeps/_workspace/src/github.com/coreos/go-oidc/jose/sig_hmac_test.go
generated
vendored
Normal file
85
Godeps/_workspace/src/github.com/coreos/go-oidc/jose/sig_hmac_test.go
generated
vendored
Normal file
|
@ -0,0 +1,85 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var hmacTestCases = []struct {
|
||||
data string
|
||||
sig string
|
||||
jwk JWK
|
||||
valid bool
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
"test",
|
||||
"Aymga2LNFrM-tnkr6MYLFY2Jou46h2_Omogeu0iMCRQ=",
|
||||
JWK{
|
||||
ID: "fake-key",
|
||||
Alg: "HS256",
|
||||
Secret: []byte("secret"),
|
||||
},
|
||||
true,
|
||||
"valid case",
|
||||
},
|
||||
{
|
||||
"test",
|
||||
"Aymga2LNFrM-tnkr6MYLFY2Jou46h2_Omogeu0iMCRQ=",
|
||||
JWK{
|
||||
ID: "different-key",
|
||||
Alg: "HS256",
|
||||
Secret: []byte("secret"),
|
||||
},
|
||||
true,
|
||||
"invalid: different key, should not match",
|
||||
},
|
||||
{
|
||||
"test sig and non-matching data",
|
||||
"Aymga2LNFrM-tnkr6MYLFY2Jou46h2_Omogeu0iMCRQ=",
|
||||
JWK{
|
||||
ID: "fake-key",
|
||||
Alg: "HS256",
|
||||
Secret: []byte("secret"),
|
||||
},
|
||||
false,
|
||||
"invalid: sig and data should not match",
|
||||
},
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
for _, tt := range hmacTestCases {
|
||||
v, err := NewVerifierHMAC(tt.jwk)
|
||||
if err != nil {
|
||||
t.Errorf("should construct hmac verifier. test: %s. err=%v", tt.desc, err)
|
||||
}
|
||||
|
||||
decSig, _ := base64.URLEncoding.DecodeString(tt.sig)
|
||||
err = v.Verify(decSig, []byte(tt.data))
|
||||
if err == nil && !tt.valid {
|
||||
t.Errorf("verify failure. test: %s. expected: invalid, actual: valid.", tt.desc)
|
||||
}
|
||||
if err != nil && tt.valid {
|
||||
t.Errorf("verify failure. test: %s. expected: valid, actual: invalid. err=%v", tt.desc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
for _, tt := range hmacTestCases {
|
||||
s := NewSignerHMAC("test", tt.jwk.Secret)
|
||||
sig, err := s.Sign([]byte(tt.data))
|
||||
if err != nil {
|
||||
t.Errorf("sign failure. test: %s. err=%v", tt.desc, err)
|
||||
}
|
||||
|
||||
expSig, _ := base64.URLEncoding.DecodeString(tt.sig)
|
||||
if tt.valid && !bytes.Equal(sig, expSig) {
|
||||
t.Errorf("sign failure. test: %s. expected: %s, actual: %s.", tt.desc, tt.sig, base64.URLEncoding.EncodeToString(sig))
|
||||
}
|
||||
if !tt.valid && bytes.Equal(sig, expSig) {
|
||||
t.Errorf("sign failure. test: %s. expected: invalid signature.", tt.desc)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package jose
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type VerifierRSA struct {
|
||||
KeyID string
|
||||
Hash crypto.Hash
|
||||
PublicKey rsa.PublicKey
|
||||
}
|
||||
|
||||
type SignerRSA struct {
|
||||
PrivateKey rsa.PrivateKey
|
||||
VerifierRSA
|
||||
}
|
||||
|
||||
func NewVerifierRSA(jwk JWK) (*VerifierRSA, error) {
|
||||
if strings.ToUpper(jwk.Alg) != "RS256" {
|
||||
return nil, fmt.Errorf("unsupported key algorithm %q", jwk.Alg)
|
||||
}
|
||||
|
||||
v := VerifierRSA{
|
||||
KeyID: jwk.ID,
|
||||
PublicKey: rsa.PublicKey{
|
||||
N: jwk.Modulus,
|
||||
E: jwk.Exponent,
|
||||
},
|
||||
Hash: crypto.SHA256,
|
||||
}
|
||||
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func NewSignerRSA(kid string, key rsa.PrivateKey) *SignerRSA {
|
||||
return &SignerRSA{
|
||||
PrivateKey: key,
|
||||
VerifierRSA: VerifierRSA{
|
||||
KeyID: kid,
|
||||
PublicKey: key.PublicKey,
|
||||
Hash: crypto.SHA256,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (v *VerifierRSA) ID() string {
|
||||
return v.KeyID
|
||||
}
|
||||
|
||||
func (v *VerifierRSA) Alg() string {
|
||||
return "RS256"
|
||||
}
|
||||
|
||||
func (v *VerifierRSA) Verify(sig []byte, data []byte) error {
|
||||
h := v.Hash.New()
|
||||
h.Write(data)
|
||||
return rsa.VerifyPKCS1v15(&v.PublicKey, v.Hash, h.Sum(nil), sig)
|
||||
}
|
||||
|
||||
func (s *SignerRSA) Sign(data []byte) ([]byte, error) {
|
||||
h := s.Hash.New()
|
||||
h.Write(data)
|
||||
return rsa.SignPKCS1v15(rand.Reader, &s.PrivateKey, s.Hash, h.Sum(nil))
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
func NewPublicKey(jwk jose.JWK) *PublicKey {
|
||||
return &PublicKey{jwk: jwk}
|
||||
}
|
||||
|
||||
type PublicKey struct {
|
||||
jwk jose.JWK
|
||||
}
|
||||
|
||||
func (k *PublicKey) ID() string {
|
||||
return k.jwk.ID
|
||||
}
|
||||
|
||||
func (k *PublicKey) Verifier() (jose.Verifier, error) {
|
||||
return jose.NewVerifierRSA(k.jwk)
|
||||
}
|
||||
|
||||
type PrivateKey struct {
|
||||
KeyID string
|
||||
PrivateKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func (k *PrivateKey) ID() string {
|
||||
return k.KeyID
|
||||
}
|
||||
|
||||
func (k *PrivateKey) Signer() jose.Signer {
|
||||
return jose.NewSignerRSA(k.ID(), *k.PrivateKey)
|
||||
}
|
||||
|
||||
func (k *PrivateKey) JWK() jose.JWK {
|
||||
return jose.JWK{
|
||||
ID: k.KeyID,
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Exponent: k.PrivateKey.PublicKey.E,
|
||||
Modulus: k.PrivateKey.PublicKey.N,
|
||||
}
|
||||
}
|
||||
|
||||
type KeySet interface {
|
||||
ExpiresAt() time.Time
|
||||
}
|
||||
|
||||
type PublicKeySet struct {
|
||||
keys []PublicKey
|
||||
index map[string]*PublicKey
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func NewPublicKeySet(jwks []jose.JWK, exp time.Time) *PublicKeySet {
|
||||
keys := make([]PublicKey, len(jwks))
|
||||
index := make(map[string]*PublicKey)
|
||||
for i, jwk := range jwks {
|
||||
keys[i] = *NewPublicKey(jwk)
|
||||
index[keys[i].ID()] = &keys[i]
|
||||
}
|
||||
return &PublicKeySet{
|
||||
keys: keys,
|
||||
index: index,
|
||||
expiresAt: exp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PublicKeySet) ExpiresAt() time.Time {
|
||||
return s.expiresAt
|
||||
}
|
||||
|
||||
func (s *PublicKeySet) Keys() []PublicKey {
|
||||
return s.keys
|
||||
}
|
||||
|
||||
func (s *PublicKeySet) Key(id string) *PublicKey {
|
||||
return s.index[id]
|
||||
}
|
||||
|
||||
type PrivateKeySet struct {
|
||||
keys []*PrivateKey
|
||||
ActiveKeyID string
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
func NewPrivateKeySet(keys []*PrivateKey, exp time.Time) *PrivateKeySet {
|
||||
return &PrivateKeySet{
|
||||
keys: keys,
|
||||
ActiveKeyID: keys[0].ID(),
|
||||
expiresAt: exp.UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrivateKeySet) Keys() []*PrivateKey {
|
||||
return s.keys
|
||||
}
|
||||
|
||||
func (s *PrivateKeySet) ExpiresAt() time.Time {
|
||||
return s.expiresAt
|
||||
}
|
||||
|
||||
func (s *PrivateKeySet) Active() *PrivateKey {
|
||||
for i, k := range s.keys {
|
||||
if k.ID() == s.ActiveKeyID {
|
||||
return s.keys[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type GeneratePrivateKeyFunc func() (*PrivateKey, error)
|
||||
|
||||
func GeneratePrivateKey() (*PrivateKey, error) {
|
||||
pk, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
k := PrivateKey{
|
||||
KeyID: base64BigInt(pk.PublicKey.N),
|
||||
PrivateKey: pk,
|
||||
}
|
||||
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
func base64BigInt(b *big.Int) string {
|
||||
return base64.URLEncoding.EncodeToString(b.Bytes())
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
func TestPrivateRSAKeyJWK(t *testing.T) {
|
||||
n := big.NewInt(int64(17))
|
||||
if n == nil {
|
||||
panic("NewInt returned nil")
|
||||
}
|
||||
|
||||
k := &PrivateKey{
|
||||
KeyID: "foo",
|
||||
PrivateKey: &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{N: n, E: 65537},
|
||||
},
|
||||
}
|
||||
|
||||
want := jose.JWK{
|
||||
ID: "foo",
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Modulus: n,
|
||||
Exponent: 65537,
|
||||
}
|
||||
|
||||
got := k.JWK()
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("JWK mismatch: want=%#v got=%#v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeySetKey(t *testing.T) {
|
||||
n := big.NewInt(int64(17))
|
||||
if n == nil {
|
||||
panic("NewInt returned nil")
|
||||
}
|
||||
|
||||
k := jose.JWK{
|
||||
ID: "foo",
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Modulus: n,
|
||||
Exponent: 65537,
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
ks := NewPublicKeySet([]jose.JWK{k}, now)
|
||||
|
||||
want := &PublicKey{jwk: k}
|
||||
got := ks.Key("foo")
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("Unexpected response from PublicKeySet.Key: want=%#v got=%#v", want, got)
|
||||
}
|
||||
|
||||
got = ks.Key("bar")
|
||||
if got != nil {
|
||||
t.Errorf("Expected nil response from PublicKeySet.Key, got %#v", got)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/pkg/health"
|
||||
)
|
||||
|
||||
type PrivateKeyManager interface {
|
||||
ExpiresAt() time.Time
|
||||
Signer() (jose.Signer, error)
|
||||
JWKs() ([]jose.JWK, error)
|
||||
PublicKeys() ([]PublicKey, error)
|
||||
|
||||
WritableKeySetRepo
|
||||
health.Checkable
|
||||
}
|
||||
|
||||
func NewPrivateKeyManager() PrivateKeyManager {
|
||||
return &privateKeyManager{
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
type privateKeyManager struct {
|
||||
keySet *PrivateKeySet
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) ExpiresAt() time.Time {
|
||||
if m.keySet == nil {
|
||||
return m.clock.Now().UTC()
|
||||
}
|
||||
|
||||
return m.keySet.ExpiresAt()
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) Signer() (jose.Signer, error) {
|
||||
if err := m.Healthy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.keySet.Active().Signer(), nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) JWKs() ([]jose.JWK, error) {
|
||||
if err := m.Healthy(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keys := m.keySet.Keys()
|
||||
jwks := make([]jose.JWK, len(keys))
|
||||
for i, k := range keys {
|
||||
jwks[i] = k.JWK()
|
||||
}
|
||||
return jwks, nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) PublicKeys() ([]PublicKey, error) {
|
||||
jwks, err := m.JWKs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]PublicKey, len(jwks))
|
||||
for i, jwk := range jwks {
|
||||
keys[i] = *NewPublicKey(jwk)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) Healthy() error {
|
||||
if m.keySet == nil {
|
||||
return errors.New("private key manager uninitialized")
|
||||
}
|
||||
|
||||
if len(m.keySet.Keys()) == 0 {
|
||||
return errors.New("private key manager zero keys")
|
||||
}
|
||||
|
||||
if m.keySet.ExpiresAt().Before(m.clock.Now().UTC()) {
|
||||
return errors.New("private key manager keys expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *privateKeyManager) Set(keySet KeySet) error {
|
||||
privKeySet, ok := keySet.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PrivateKeySet")
|
||||
}
|
||||
|
||||
m.keySet = privKeySet
|
||||
return nil
|
||||
}
|
225
Godeps/_workspace/src/github.com/coreos/go-oidc/key/manager_test.go
generated
vendored
Normal file
225
Godeps/_workspace/src/github.com/coreos/go-oidc/key/manager_test.go
generated
vendored
Normal file
|
@ -0,0 +1,225 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
var (
|
||||
jwk1 jose.JWK
|
||||
jwk2 jose.JWK
|
||||
jwk3 jose.JWK
|
||||
)
|
||||
|
||||
func init() {
|
||||
jwk1 = jose.JWK{
|
||||
ID: "1",
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Modulus: big.NewInt(1),
|
||||
Exponent: 65537,
|
||||
}
|
||||
|
||||
jwk2 = jose.JWK{
|
||||
ID: "2",
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Modulus: big.NewInt(2),
|
||||
Exponent: 65537,
|
||||
}
|
||||
|
||||
jwk3 = jose.JWK{
|
||||
ID: "3",
|
||||
Type: "RSA",
|
||||
Alg: "RS256",
|
||||
Use: "sig",
|
||||
Modulus: big.NewInt(3),
|
||||
Exponent: 65537,
|
||||
}
|
||||
}
|
||||
|
||||
func generatePrivateKeyStatic(t *testing.T, idAndN int) *PrivateKey {
|
||||
n := big.NewInt(int64(idAndN))
|
||||
if n == nil {
|
||||
t.Fatalf("Call to NewInt(%d) failed", idAndN)
|
||||
}
|
||||
|
||||
pk := &rsa.PrivateKey{
|
||||
PublicKey: rsa.PublicKey{N: n, E: 65537},
|
||||
}
|
||||
|
||||
return &PrivateKey{
|
||||
KeyID: strconv.Itoa(idAndN),
|
||||
PrivateKey: pk,
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyManagerJWKsRotate(t *testing.T) {
|
||||
k1 := generatePrivateKeyStatic(t, 1)
|
||||
k2 := generatePrivateKeyStatic(t, 2)
|
||||
k3 := generatePrivateKeyStatic(t, 3)
|
||||
km := NewPrivateKeyManager()
|
||||
err := km.Set(&PrivateKeySet{
|
||||
keys: []*PrivateKey{k1, k2, k3},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: time.Now().Add(time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want := []jose.JWK{jwk1, jwk2, jwk3}
|
||||
got, err := km.JWKs()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("JWK mismatch: want=%#v got=%#v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyManagerSigner(t *testing.T) {
|
||||
k := generatePrivateKeyStatic(t, 13)
|
||||
|
||||
km := NewPrivateKeyManager()
|
||||
err := km.Set(&PrivateKeySet{
|
||||
keys: []*PrivateKey{k},
|
||||
ActiveKeyID: k.KeyID,
|
||||
expiresAt: time.Now().Add(time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
signer, err := km.Signer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
wantID := "13"
|
||||
gotID := signer.ID()
|
||||
if wantID != gotID {
|
||||
t.Fatalf("Signer has incorrect ID: want=%s got=%s", wantID, gotID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyManagerHealthyFail(t *testing.T) {
|
||||
keyFixture := generatePrivateKeyStatic(t, 1)
|
||||
tests := []*privateKeyManager{
|
||||
// keySet nil
|
||||
&privateKeyManager{
|
||||
keySet: nil,
|
||||
clock: clockwork.NewRealClock(),
|
||||
},
|
||||
// zero keys
|
||||
&privateKeyManager{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{},
|
||||
expiresAt: time.Now().Add(time.Minute),
|
||||
},
|
||||
clock: clockwork.NewRealClock(),
|
||||
},
|
||||
// key set expired
|
||||
&privateKeyManager{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{keyFixture},
|
||||
expiresAt: time.Now().Add(-1 * time.Minute),
|
||||
},
|
||||
clock: clockwork.NewRealClock(),
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
if err := tt.Healthy(); err == nil {
|
||||
t.Errorf("case %d: nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyManagerHealthyFailsOtherMethods(t *testing.T) {
|
||||
km := NewPrivateKeyManager()
|
||||
if _, err := km.JWKs(); err == nil {
|
||||
t.Fatalf("Expected non-nil error")
|
||||
}
|
||||
if _, err := km.Signer(); err == nil {
|
||||
t.Fatalf("Expected non-nil error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyManagerExpiresAt(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
k := generatePrivateKeyStatic(t, 17)
|
||||
km := &privateKeyManager{
|
||||
clock: fc,
|
||||
}
|
||||
|
||||
want := fc.Now().UTC()
|
||||
got := km.ExpiresAt()
|
||||
if want != got {
|
||||
t.Fatalf("Incorrect expiration time: want=%v got=%v", want, got)
|
||||
}
|
||||
|
||||
err := km.Set(&PrivateKeySet{
|
||||
keys: []*PrivateKey{k},
|
||||
ActiveKeyID: k.KeyID,
|
||||
expiresAt: now.Add(2 * time.Minute),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
want = fc.Now().UTC().Add(2 * time.Minute)
|
||||
got = km.ExpiresAt()
|
||||
if want != got {
|
||||
t.Fatalf("Incorrect expiration time: want=%v got=%v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicKeys(t *testing.T) {
|
||||
km := NewPrivateKeyManager()
|
||||
k1 := generatePrivateKeyStatic(t, 1)
|
||||
k2 := generatePrivateKeyStatic(t, 2)
|
||||
k3 := generatePrivateKeyStatic(t, 3)
|
||||
|
||||
tests := [][]*PrivateKey{
|
||||
[]*PrivateKey{k1},
|
||||
[]*PrivateKey{k1, k2},
|
||||
[]*PrivateKey{k1, k2, k3},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
ks := &PrivateKeySet{
|
||||
keys: tt,
|
||||
expiresAt: time.Now().Add(time.Hour),
|
||||
}
|
||||
km.Set(ks)
|
||||
|
||||
jwks, err := km.JWKs()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
pks := NewPublicKeySet(jwks, time.Now().Add(time.Hour))
|
||||
want := pks.Keys()
|
||||
got, err := km.PublicKeys()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("case %d: Invalid public keys: want=%v got=%v", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package key
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrorNoKeys = errors.New("no keys found")
|
||||
|
||||
type WritableKeySetRepo interface {
|
||||
Set(KeySet) error
|
||||
}
|
||||
|
||||
type ReadableKeySetRepo interface {
|
||||
Get() (KeySet, error)
|
||||
}
|
||||
|
||||
type PrivateKeySetRepo interface {
|
||||
WritableKeySetRepo
|
||||
ReadableKeySetRepo
|
||||
}
|
||||
|
||||
func NewPrivateKeySetRepo() PrivateKeySetRepo {
|
||||
return &memPrivateKeySetRepo{}
|
||||
}
|
||||
|
||||
type memPrivateKeySetRepo struct {
|
||||
pks PrivateKeySet
|
||||
}
|
||||
|
||||
func (r *memPrivateKeySetRepo) Set(ks KeySet) error {
|
||||
pks, ok := ks.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PrivateKeySet")
|
||||
} else if pks == nil {
|
||||
return errors.New("nil KeySet")
|
||||
}
|
||||
|
||||
r.pks = *pks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *memPrivateKeySetRepo) Get() (KeySet, error) {
|
||||
if r.pks.keys == nil {
|
||||
return nil, ErrorNoKeys
|
||||
}
|
||||
return KeySet(&r.pks), nil
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
ptime "github.com/coreos/pkg/timeutil"
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
||||
var (
|
||||
log = capnslog.NewPackageLogger("github.com/coreos/go-oidc", "key")
|
||||
|
||||
ErrorPrivateKeysExpired = errors.New("private keys have expired")
|
||||
)
|
||||
|
||||
func NewPrivateKeyRotator(repo PrivateKeySetRepo, ttl time.Duration) *PrivateKeyRotator {
|
||||
return &PrivateKeyRotator{
|
||||
repo: repo,
|
||||
ttl: ttl,
|
||||
|
||||
keep: 2,
|
||||
generateKey: GeneratePrivateKey,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
type PrivateKeyRotator struct {
|
||||
repo PrivateKeySetRepo
|
||||
generateKey GeneratePrivateKeyFunc
|
||||
clock clockwork.Clock
|
||||
keep int
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) expiresAt() time.Time {
|
||||
return r.clock.Now().UTC().Add(r.ttl)
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) Healthy() error {
|
||||
pks, err := r.privateKeySet()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.clock.Now().After(pks.ExpiresAt()) {
|
||||
return ErrorPrivateKeysExpired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) privateKeySet() (*PrivateKeySet, error) {
|
||||
ks, err := r.repo.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pks, ok := ks.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to cast to PrivateKeySet")
|
||||
}
|
||||
return pks, nil
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) nextRotation() (time.Duration, error) {
|
||||
pks, err := r.privateKeySet()
|
||||
if err == ErrorNoKeys {
|
||||
log.Infof("No keys in private key set; must rotate immediately")
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
now := r.clock.Now()
|
||||
|
||||
// Ideally, we want to rotate after half the TTL has elapsed.
|
||||
idealRotationTime := pks.ExpiresAt().Add(-r.ttl / 2)
|
||||
|
||||
// If we are past the ideal rotation time, rotate immediatly.
|
||||
return max(0, idealRotationTime.Sub(now)), nil
|
||||
}
|
||||
|
||||
func max(a, b time.Duration) time.Duration {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (r *PrivateKeyRotator) Run() chan struct{} {
|
||||
attempt := func() {
|
||||
k, err := r.generateKey()
|
||||
if err != nil {
|
||||
log.Errorf("Failed generating signing key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
exp := r.expiresAt()
|
||||
if err := rotatePrivateKeys(r.repo, k, r.keep, exp); err != nil {
|
||||
log.Errorf("Failed key rotation: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Rotated signing keys: id=%s expiresAt=%s", k.ID(), exp)
|
||||
}
|
||||
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
var nextRotation time.Duration
|
||||
var sleep time.Duration
|
||||
var err error
|
||||
for {
|
||||
if nextRotation, err = r.nextRotation(); err == nil {
|
||||
break
|
||||
}
|
||||
sleep = ptime.ExpBackoff(sleep, time.Minute)
|
||||
log.Errorf("error getting nextRotation, retrying in %v: %v", sleep, err)
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
|
||||
log.Infof("will rotate keys in %v", nextRotation)
|
||||
select {
|
||||
case <-r.clock.After(nextRotation):
|
||||
attempt()
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func rotatePrivateKeys(repo PrivateKeySetRepo, k *PrivateKey, keep int, exp time.Time) error {
|
||||
ks, err := repo.Get()
|
||||
if err != nil && err != ErrorNoKeys {
|
||||
return err
|
||||
}
|
||||
|
||||
var keys []*PrivateKey
|
||||
if ks != nil {
|
||||
pks, ok := ks.(*PrivateKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PrivateKeySet")
|
||||
}
|
||||
keys = pks.Keys()
|
||||
}
|
||||
|
||||
keys = append([]*PrivateKey{k}, keys...)
|
||||
if l := len(keys); l > keep {
|
||||
keys = keys[0:keep]
|
||||
}
|
||||
|
||||
nks := PrivateKeySet{
|
||||
keys: keys,
|
||||
ActiveKeyID: k.ID(),
|
||||
expiresAt: exp,
|
||||
}
|
||||
|
||||
return repo.Set(KeySet(&nks))
|
||||
}
|
311
Godeps/_workspace/src/github.com/coreos/go-oidc/key/rotate_test.go
generated
vendored
Normal file
311
Godeps/_workspace/src/github.com/coreos/go-oidc/key/rotate_test.go
generated
vendored
Normal file
|
@ -0,0 +1,311 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
||||
func generatePrivateKeySerialFunc(t *testing.T) GeneratePrivateKeyFunc {
|
||||
var n int
|
||||
return func() (*PrivateKey, error) {
|
||||
n++
|
||||
return generatePrivateKeyStatic(t, n), nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotate(t *testing.T) {
|
||||
now := time.Now()
|
||||
k1 := generatePrivateKeyStatic(t, 1)
|
||||
k2 := generatePrivateKeyStatic(t, 2)
|
||||
k3 := generatePrivateKeyStatic(t, 3)
|
||||
|
||||
tests := []struct {
|
||||
start *PrivateKeySet
|
||||
key *PrivateKey
|
||||
keep int
|
||||
exp time.Time
|
||||
want *PrivateKeySet
|
||||
}{
|
||||
// start with nil keys
|
||||
{
|
||||
start: nil,
|
||||
key: k1,
|
||||
keep: 2,
|
||||
exp: now.Add(time.Second),
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(time.Second),
|
||||
},
|
||||
},
|
||||
// start with zero keys
|
||||
{
|
||||
start: &PrivateKeySet{},
|
||||
key: k1,
|
||||
keep: 2,
|
||||
exp: now.Add(time.Second),
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(time.Second),
|
||||
},
|
||||
},
|
||||
// add second key
|
||||
{
|
||||
start: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now,
|
||||
},
|
||||
key: k2,
|
||||
keep: 2,
|
||||
exp: now.Add(time.Second),
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(time.Second),
|
||||
},
|
||||
},
|
||||
// rotate in third key
|
||||
{
|
||||
start: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now,
|
||||
},
|
||||
key: k3,
|
||||
keep: 2,
|
||||
exp: now.Add(time.Second),
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k3, k2},
|
||||
ActiveKeyID: k3.KeyID,
|
||||
expiresAt: now.Add(time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
repo := NewPrivateKeySetRepo()
|
||||
if tt.start != nil {
|
||||
err := repo.Set(tt.start)
|
||||
if err != nil {
|
||||
log.Fatalf("case %d: unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
rotatePrivateKeys(repo, tt.key, tt.keep, tt.exp)
|
||||
got, err := repo.Get()
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want, got) {
|
||||
t.Errorf("case %d: unexpected result: want=%#v got=%#v", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyRotatorRun(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
k1 := generatePrivateKeyStatic(t, 1)
|
||||
k2 := generatePrivateKeyStatic(t, 2)
|
||||
k3 := generatePrivateKeyStatic(t, 3)
|
||||
k4 := generatePrivateKeyStatic(t, 4)
|
||||
|
||||
kRepo := NewPrivateKeySetRepo()
|
||||
krot := NewPrivateKeyRotator(kRepo, 4*time.Second)
|
||||
krot.clock = fc
|
||||
krot.generateKey = generatePrivateKeySerialFunc(t)
|
||||
|
||||
steps := []*PrivateKeySet{
|
||||
&PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(4 * time.Second),
|
||||
},
|
||||
&PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(6 * time.Second),
|
||||
},
|
||||
&PrivateKeySet{
|
||||
keys: []*PrivateKey{k3, k2},
|
||||
ActiveKeyID: k3.KeyID,
|
||||
expiresAt: now.Add(8 * time.Second),
|
||||
},
|
||||
&PrivateKeySet{
|
||||
keys: []*PrivateKey{k4, k3},
|
||||
ActiveKeyID: k4.KeyID,
|
||||
expiresAt: now.Add(10 * time.Second),
|
||||
},
|
||||
}
|
||||
|
||||
stop := krot.Run()
|
||||
defer close(stop)
|
||||
|
||||
for i, st := range steps {
|
||||
// wait for the rotater to get sleepy
|
||||
fc.BlockUntil(1)
|
||||
|
||||
got, err := kRepo.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("step %d: unexpected error: %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(st, got) {
|
||||
t.Fatalf("step %d: unexpected state: want=%#v got=%#v", i, st, got)
|
||||
}
|
||||
fc.Advance(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrivateKeyRotatorExpiresAt(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
krot := &PrivateKeyRotator{
|
||||
clock: fc,
|
||||
ttl: time.Minute,
|
||||
}
|
||||
got := krot.expiresAt()
|
||||
want := fc.Now().UTC().Add(time.Minute)
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("Incorrect expiration time: want=%v got=%v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextRotation(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
tests := []struct {
|
||||
expiresAt time.Time
|
||||
ttl time.Duration
|
||||
numKeys int
|
||||
expected time.Duration
|
||||
}{
|
||||
{
|
||||
// closest to prod
|
||||
expiresAt: now.Add(time.Hour * 24),
|
||||
ttl: time.Hour * 24,
|
||||
numKeys: 2,
|
||||
expected: time.Hour * 12,
|
||||
},
|
||||
{
|
||||
expiresAt: now.Add(time.Hour * 2),
|
||||
ttl: time.Hour * 4,
|
||||
numKeys: 2,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
// No keys.
|
||||
expiresAt: now.Add(time.Hour * 2),
|
||||
ttl: time.Hour * 4,
|
||||
numKeys: 0,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
// Nil keyset.
|
||||
expiresAt: now.Add(time.Hour * 2),
|
||||
ttl: time.Hour * 4,
|
||||
numKeys: -1,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
// KeySet expired.
|
||||
expiresAt: now.Add(time.Hour * -2),
|
||||
ttl: time.Hour * 4,
|
||||
numKeys: 2,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
// Expiry past now + TTL
|
||||
expiresAt: now.Add(time.Hour * 5),
|
||||
ttl: time.Hour * 4,
|
||||
numKeys: 2,
|
||||
expected: 3 * time.Hour,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
kRepo := NewPrivateKeySetRepo()
|
||||
krot := NewPrivateKeyRotator(kRepo, tt.ttl)
|
||||
krot.clock = fc
|
||||
pks := &PrivateKeySet{
|
||||
expiresAt: tt.expiresAt,
|
||||
}
|
||||
if tt.numKeys != -1 {
|
||||
for n := 0; n < tt.numKeys; n++ {
|
||||
pks.keys = append(pks.keys, generatePrivateKeyStatic(t, n))
|
||||
}
|
||||
err := kRepo.Set(pks)
|
||||
if err != nil {
|
||||
log.Fatalf("case %d: unexpected error: %v", i, err)
|
||||
}
|
||||
|
||||
}
|
||||
actual, err := krot.nextRotation()
|
||||
if err != nil {
|
||||
t.Errorf("case %d: error calling shouldRotate(): %v", i, err)
|
||||
}
|
||||
if actual != tt.expected {
|
||||
t.Errorf("case %d: actual == %v, want %v", i, actual, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthy(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
tests := []struct {
|
||||
expiresAt time.Time
|
||||
numKeys int
|
||||
expected error
|
||||
}{
|
||||
{
|
||||
expiresAt: now.Add(time.Hour),
|
||||
numKeys: 2,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
expiresAt: now.Add(time.Hour),
|
||||
numKeys: -1,
|
||||
expected: ErrorNoKeys,
|
||||
},
|
||||
{
|
||||
expiresAt: now.Add(time.Hour),
|
||||
numKeys: 0,
|
||||
expected: ErrorNoKeys,
|
||||
},
|
||||
{
|
||||
expiresAt: now.Add(-time.Hour),
|
||||
numKeys: 2,
|
||||
expected: ErrorPrivateKeysExpired,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
kRepo := NewPrivateKeySetRepo()
|
||||
krot := NewPrivateKeyRotator(kRepo, time.Hour)
|
||||
krot.clock = fc
|
||||
pks := &PrivateKeySet{
|
||||
expiresAt: tt.expiresAt,
|
||||
}
|
||||
if tt.numKeys != -1 {
|
||||
for n := 0; n < tt.numKeys; n++ {
|
||||
pks.keys = append(pks.keys, generatePrivateKeyStatic(t, n))
|
||||
}
|
||||
err := kRepo.Set(pks)
|
||||
if err != nil {
|
||||
log.Fatalf("case %d: unexpected error: %v", i, err)
|
||||
}
|
||||
|
||||
}
|
||||
if err := krot.Healthy(); err != tt.expected {
|
||||
t.Errorf("case %d: got==%q, want==%q", i, err, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/pkg/timeutil"
|
||||
)
|
||||
|
||||
func NewKeySetSyncer(r ReadableKeySetRepo, w WritableKeySetRepo) *KeySetSyncer {
|
||||
return &KeySetSyncer{
|
||||
readable: r,
|
||||
writable: w,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
type KeySetSyncer struct {
|
||||
readable ReadableKeySetRepo
|
||||
writable WritableKeySetRepo
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func (s *KeySetSyncer) Run() chan struct{} {
|
||||
stop := make(chan struct{})
|
||||
go func() {
|
||||
var failing bool
|
||||
var next time.Duration
|
||||
for {
|
||||
exp, err := sync(s.readable, s.writable, s.clock)
|
||||
if err != nil || exp == 0 {
|
||||
if !failing {
|
||||
failing = true
|
||||
next = time.Second
|
||||
} else {
|
||||
next = timeutil.ExpBackoff(next, time.Minute)
|
||||
}
|
||||
if exp == 0 {
|
||||
log.Errorf("Synced to already expired key set, retrying in %v: %v", next, err)
|
||||
|
||||
} else {
|
||||
log.Errorf("Failed syncing key set, retrying in %v: %v", next, err)
|
||||
}
|
||||
} else {
|
||||
failing = false
|
||||
next = exp / 2
|
||||
log.Infof("Synced key set, checking again in %v", next)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.clock.After(next):
|
||||
continue
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func Sync(r ReadableKeySetRepo, w WritableKeySetRepo) (time.Duration, error) {
|
||||
return sync(r, w, clockwork.NewRealClock())
|
||||
}
|
||||
|
||||
// sync copies the keyset from r to the KeySet at w and returns the duration in which the KeySet will expire.
|
||||
// If keyset has already expired, returns a zero duration.
|
||||
func sync(r ReadableKeySetRepo, w WritableKeySetRepo, clock clockwork.Clock) (exp time.Duration, err error) {
|
||||
var ks KeySet
|
||||
ks, err = r.Get()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ks == nil {
|
||||
err = errors.New("no source KeySet")
|
||||
return
|
||||
}
|
||||
|
||||
if err = w.Set(ks); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := clock.Now()
|
||||
if ks.ExpiresAt().After(now) {
|
||||
exp = ks.ExpiresAt().Sub(now)
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
)
|
||||
|
||||
type staticReadableKeySetRepo struct {
|
||||
ks KeySet
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *staticReadableKeySetRepo) Get() (KeySet, error) {
|
||||
return r.ks, r.err
|
||||
}
|
||||
|
||||
func TestKeySyncerSync(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
k1 := generatePrivateKeyStatic(t, 1)
|
||||
k2 := generatePrivateKeyStatic(t, 2)
|
||||
k3 := generatePrivateKeyStatic(t, 3)
|
||||
|
||||
steps := []struct {
|
||||
fromKS KeySet
|
||||
fromErr error
|
||||
advance time.Duration
|
||||
want *PrivateKeySet
|
||||
}{
|
||||
// on startup, first sync should trigger within a second
|
||||
{
|
||||
fromKS: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(10 * time.Second),
|
||||
},
|
||||
advance: time.Second,
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(10 * time.Second),
|
||||
},
|
||||
},
|
||||
// advance halfway into TTL, triggering sync
|
||||
{
|
||||
fromKS: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(15 * time.Second),
|
||||
},
|
||||
advance: 5 * time.Second,
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(15 * time.Second),
|
||||
},
|
||||
},
|
||||
|
||||
// advance halfway into TTL, triggering sync that fails
|
||||
{
|
||||
fromErr: errors.New("fail!"),
|
||||
advance: 10 * time.Second,
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(15 * time.Second),
|
||||
},
|
||||
},
|
||||
|
||||
// sync retries quickly, and succeeds with fixed data
|
||||
{
|
||||
fromKS: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k3, k2, k1},
|
||||
ActiveKeyID: k3.KeyID,
|
||||
expiresAt: now.Add(25 * time.Second),
|
||||
},
|
||||
advance: 3 * time.Second,
|
||||
want: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k3, k2, k1},
|
||||
ActiveKeyID: k3.KeyID,
|
||||
expiresAt: now.Add(25 * time.Second),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
from := &staticReadableKeySetRepo{}
|
||||
to := NewPrivateKeySetRepo()
|
||||
|
||||
syncer := NewKeySetSyncer(from, to)
|
||||
syncer.clock = fc
|
||||
stop := syncer.Run()
|
||||
defer close(stop)
|
||||
|
||||
for i, st := range steps {
|
||||
from.ks = st.fromKS
|
||||
from.err = st.fromErr
|
||||
|
||||
fc.Advance(st.advance)
|
||||
fc.BlockUntil(1)
|
||||
|
||||
ks, err := to.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("step %d: unable to get keys: %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(st.want, ks) {
|
||||
t.Fatalf("step %d: incorrect state: want=%#v got=%#v", i, st.want, ks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSync(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
k1 := generatePrivateKeyStatic(t, 1)
|
||||
k2 := generatePrivateKeyStatic(t, 2)
|
||||
k3 := generatePrivateKeyStatic(t, 3)
|
||||
|
||||
tests := []struct {
|
||||
keySet *PrivateKeySet
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(time.Minute),
|
||||
},
|
||||
want: time.Minute,
|
||||
},
|
||||
{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(time.Minute),
|
||||
},
|
||||
want: time.Minute,
|
||||
},
|
||||
{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k3, k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(time.Minute),
|
||||
},
|
||||
want: time.Minute,
|
||||
},
|
||||
{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k2, k1},
|
||||
ActiveKeyID: k2.KeyID,
|
||||
expiresAt: now.Add(time.Hour),
|
||||
},
|
||||
want: time.Hour,
|
||||
},
|
||||
{
|
||||
keySet: &PrivateKeySet{
|
||||
keys: []*PrivateKey{k1},
|
||||
ActiveKeyID: k1.KeyID,
|
||||
expiresAt: now.Add(-time.Hour),
|
||||
},
|
||||
want: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
from := NewPrivateKeySetRepo()
|
||||
to := NewPrivateKeySetRepo()
|
||||
|
||||
err := from.Set(tt.keySet)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
exp, err := sync(from, to, fc)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if tt.want != exp {
|
||||
t.Errorf("case %d: want=%v got=%v", i, tt.want, exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncFail(t *testing.T) {
|
||||
tests := []error{
|
||||
nil,
|
||||
errors.New("fail!"),
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
from := &staticReadableKeySetRepo{ks: nil, err: tt}
|
||||
to := NewPrivateKeySetRepo()
|
||||
|
||||
if _, err := sync(from, to, clockwork.NewFakeClock()); err == nil {
|
||||
t.Errorf("case %d: expected non-nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ErrorAccessDenied = "access_denied"
|
||||
ErrorInvalidClient = "invalid_client"
|
||||
ErrorInvalidGrant = "invalid_grant"
|
||||
ErrorInvalidRequest = "invalid_request"
|
||||
ErrorServerError = "server_error"
|
||||
ErrorUnauthorizedClient = "unauthorized_client"
|
||||
ErrorUnsupportedGrantType = "unsupported_grant_type"
|
||||
ErrorUnsupportedResponseType = "unsupported_response_type"
|
||||
)
|
||||
|
||||
type Error struct {
|
||||
Type string `json:"error"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Type
|
||||
}
|
||||
|
||||
func NewError(typ string) *Error {
|
||||
return &Error{Type: typ}
|
||||
}
|
||||
|
||||
func unmarshalError(b []byte) error {
|
||||
var oerr Error
|
||||
err := json.Unmarshal(b, &oerr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unrecognized error: %s", string(b))
|
||||
}
|
||||
return &oerr
|
||||
}
|
79
Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/error_test.go
generated
vendored
Normal file
79
Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/error_test.go
generated
vendored
Normal file
|
@ -0,0 +1,79 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUnmarshalError(t *testing.T) {
|
||||
tests := []struct {
|
||||
b []byte
|
||||
e *Error
|
||||
o bool
|
||||
}{
|
||||
{
|
||||
b: []byte("{ \"error\": \"invalid_client\", \"state\": \"foo\" }"),
|
||||
e: &Error{Type: ErrorInvalidClient, State: "foo"},
|
||||
o: true,
|
||||
},
|
||||
{
|
||||
b: []byte("{ \"error\": \"invalid_grant\", \"state\": \"bar\" }"),
|
||||
e: &Error{Type: ErrorInvalidGrant, State: "bar"},
|
||||
o: true,
|
||||
},
|
||||
{
|
||||
b: []byte("{ \"error\": \"invalid_request\", \"state\": \"\" }"),
|
||||
e: &Error{Type: ErrorInvalidRequest, State: ""},
|
||||
o: true,
|
||||
},
|
||||
{
|
||||
b: []byte("{ \"error\": \"server_error\", \"state\": \"elroy\" }"),
|
||||
e: &Error{Type: ErrorServerError, State: "elroy"},
|
||||
o: true,
|
||||
},
|
||||
{
|
||||
b: []byte("{ \"error\": \"unsupported_grant_type\", \"state\": \"\" }"),
|
||||
e: &Error{Type: ErrorUnsupportedGrantType, State: ""},
|
||||
o: true,
|
||||
},
|
||||
{
|
||||
b: []byte("{ \"error\": \"unsupported_response_type\", \"state\": \"\" }"),
|
||||
e: &Error{Type: ErrorUnsupportedResponseType, State: ""},
|
||||
o: true,
|
||||
},
|
||||
// Should fail json unmarshal
|
||||
{
|
||||
b: nil,
|
||||
e: nil,
|
||||
o: false,
|
||||
},
|
||||
{
|
||||
b: []byte("random string"),
|
||||
e: nil,
|
||||
o: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
err := unmarshalError(tt.b)
|
||||
oerr, ok := err.(*Error)
|
||||
|
||||
if ok != tt.o {
|
||||
t.Errorf("%v != %v, %v", ok, tt.o, oerr)
|
||||
t.Errorf("case %d: want=%+v, got=%+v", i, tt.e, oerr)
|
||||
}
|
||||
|
||||
if ok && !reflect.DeepEqual(tt.e, oerr) {
|
||||
t.Errorf("case %d: want=%+v, got=%+v", i, tt.e, oerr)
|
||||
}
|
||||
|
||||
if !ok && tt.e != nil {
|
||||
want := fmt.Sprintf("unrecognized error: %s", string(tt.b))
|
||||
got := tt.e.Error()
|
||||
if want != got {
|
||||
t.Errorf("case %d: want=%+v, got=%+v", i, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
)
|
||||
|
||||
const (
|
||||
ResponseTypeCode = "code"
|
||||
)
|
||||
|
||||
const (
|
||||
GrantTypeAuthCode = "authorization_code"
|
||||
GrantTypeClientCreds = "client_credentials"
|
||||
GrantTypeImplicit = "implicit"
|
||||
GrantTypeRefreshToken = "refresh_token"
|
||||
|
||||
AuthMethodClientSecretPost = "client_secret_post"
|
||||
AuthMethodClientSecretBasic = "client_secret_basic"
|
||||
AuthMethodClientSecretJWT = "client_secret_jwt"
|
||||
AuthMethodPrivateKeyJWT = "private_key_jwt"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Credentials ClientCredentials
|
||||
Scope []string
|
||||
RedirectURL string
|
||||
AuthURL string
|
||||
TokenURL string
|
||||
|
||||
// Must be one of the AuthMethodXXX methods above. Right now, only
|
||||
// AuthMethodClientSecretPost and AuthMethodClientSecretBasic are supported.
|
||||
AuthMethod string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
hc phttp.Client
|
||||
creds ClientCredentials
|
||||
scope []string
|
||||
authURL *url.URL
|
||||
redirectURL *url.URL
|
||||
tokenURL *url.URL
|
||||
authMethod string
|
||||
}
|
||||
|
||||
type ClientCredentials struct {
|
||||
ID string
|
||||
Secret string
|
||||
}
|
||||
|
||||
func NewClient(hc phttp.Client, cfg Config) (c *Client, err error) {
|
||||
if len(cfg.Credentials.ID) == 0 {
|
||||
err = errors.New("missing client id")
|
||||
return
|
||||
}
|
||||
|
||||
if len(cfg.Credentials.Secret) == 0 {
|
||||
err = errors.New("missing client secret")
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.AuthMethod == "" {
|
||||
cfg.AuthMethod = AuthMethodClientSecretBasic
|
||||
} else if cfg.AuthMethod != AuthMethodClientSecretPost && cfg.AuthMethod != AuthMethodClientSecretBasic {
|
||||
err = fmt.Errorf("auth method %q is not supported", cfg.AuthMethod)
|
||||
return
|
||||
}
|
||||
|
||||
au, err := phttp.ParseNonEmptyURL(cfg.AuthURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tu, err := phttp.ParseNonEmptyURL(cfg.TokenURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Allow empty redirect URL in the case where the client
|
||||
// only needs to verify a given token.
|
||||
ru, err := url.Parse(cfg.RedirectURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c = &Client{
|
||||
creds: cfg.Credentials,
|
||||
scope: cfg.Scope,
|
||||
redirectURL: ru,
|
||||
authURL: au,
|
||||
tokenURL: tu,
|
||||
hc: hc,
|
||||
authMethod: cfg.AuthMethod,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Generate the url for initial redirect to oauth provider.
|
||||
func (c *Client) AuthCodeURL(state, accessType, prompt string) string {
|
||||
v := c.commonURLValues()
|
||||
v.Set("state", state)
|
||||
if strings.ToLower(accessType) == "offline" {
|
||||
v.Set("access_type", "offline")
|
||||
}
|
||||
|
||||
if prompt != "" {
|
||||
v.Set("prompt", prompt)
|
||||
}
|
||||
v.Set("response_type", "code")
|
||||
|
||||
q := v.Encode()
|
||||
u := *c.authURL
|
||||
if u.RawQuery == "" {
|
||||
u.RawQuery = q
|
||||
} else {
|
||||
u.RawQuery += "&" + q
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func (c *Client) commonURLValues() url.Values {
|
||||
return url.Values{
|
||||
"redirect_uri": {c.redirectURL.String()},
|
||||
"scope": {strings.Join(c.scope, " ")},
|
||||
"client_id": {c.creds.ID},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) newAuthenticatedRequest(url string, values url.Values) (*http.Request, error) {
|
||||
var req *http.Request
|
||||
var err error
|
||||
switch c.authMethod {
|
||||
case AuthMethodClientSecretPost:
|
||||
values.Set("client_secret", c.creds.Secret)
|
||||
req, err = http.NewRequest("POST", url, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case AuthMethodClientSecretBasic:
|
||||
req, err = http.NewRequest("POST", url, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(c.creds.ID, c.creds.Secret)
|
||||
default:
|
||||
panic("misconfigured client: auth method not supported")
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
return req, nil
|
||||
|
||||
}
|
||||
|
||||
// ClientCredsToken posts the client id and secret to obtain a token scoped to the OAuth2 client via the "client_credentials" grant type.
|
||||
// May not be supported by all OAuth2 servers.
|
||||
func (c *Client) ClientCredsToken(scope []string) (result TokenResponse, err error) {
|
||||
v := url.Values{
|
||||
"scope": {strings.Join(scope, " ")},
|
||||
"grant_type": {GrantTypeClientCreds},
|
||||
}
|
||||
|
||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return parseTokenResponse(resp)
|
||||
}
|
||||
|
||||
// RequestToken requests a token from the Token Endpoint with the specified grantType.
|
||||
// If 'grantType' == GrantTypeAuthCode, then 'value' should be the authorization code.
|
||||
// If 'grantType' == GrantTypeRefreshToken, then 'value' should be the refresh token.
|
||||
func (c *Client) RequestToken(grantType, value string) (result TokenResponse, err error) {
|
||||
v := c.commonURLValues()
|
||||
|
||||
v.Set("grant_type", grantType)
|
||||
v.Set("client_secret", c.creds.Secret)
|
||||
switch grantType {
|
||||
case GrantTypeAuthCode:
|
||||
v.Set("code", value)
|
||||
case GrantTypeRefreshToken:
|
||||
v.Set("refresh_token", value)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported grant_type: %v", grantType)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := c.newAuthenticatedRequest(c.tokenURL.String(), v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := c.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return parseTokenResponse(resp)
|
||||
}
|
||||
|
||||
func parseTokenResponse(resp *http.Response) (result TokenResponse, err error) {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
err = unmarshalError(body)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result = TokenResponse{
|
||||
RawBody: body,
|
||||
}
|
||||
|
||||
if contentType == "application/x-www-form-urlencoded" || contentType == "text/plain" {
|
||||
var vals url.Values
|
||||
vals, err = url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
result.AccessToken = vals.Get("access_token")
|
||||
result.TokenType = vals.Get("token_type")
|
||||
result.IDToken = vals.Get("id_token")
|
||||
result.RefreshToken = vals.Get("refresh_token")
|
||||
result.Scope = vals.Get("scope")
|
||||
e := vals.Get("expires_in")
|
||||
if e == "" {
|
||||
e = vals.Get("expires")
|
||||
}
|
||||
result.Expires, err = strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
b := make(map[string]interface{})
|
||||
if err = json.Unmarshal(body, &b); err != nil {
|
||||
return
|
||||
}
|
||||
result.AccessToken, _ = b["access_token"].(string)
|
||||
result.TokenType, _ = b["token_type"].(string)
|
||||
result.IDToken, _ = b["id_token"].(string)
|
||||
result.RefreshToken, _ = b["refresh_token"].(string)
|
||||
result.Scope, _ = b["scope"].(string)
|
||||
e, ok := b["expires_in"].(int)
|
||||
if !ok {
|
||||
e, _ = b["expires"].(int)
|
||||
}
|
||||
result.Expires = e
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string
|
||||
TokenType string
|
||||
Expires int
|
||||
IDToken string
|
||||
RefreshToken string // OPTIONAL.
|
||||
Scope string // OPTIONAL, if identical to the scope requested by the client, otherwise, REQUIRED.
|
||||
RawBody []byte // In case callers need some other non-standard info from the token response
|
||||
}
|
||||
|
||||
type AuthCodeRequest struct {
|
||||
ResponseType string
|
||||
ClientID string
|
||||
RedirectURL *url.URL
|
||||
Scope []string
|
||||
State string
|
||||
}
|
||||
|
||||
func ParseAuthCodeRequest(q url.Values) (AuthCodeRequest, error) {
|
||||
acr := AuthCodeRequest{
|
||||
ResponseType: q.Get("response_type"),
|
||||
ClientID: q.Get("client_id"),
|
||||
State: q.Get("state"),
|
||||
Scope: make([]string, 0),
|
||||
}
|
||||
|
||||
qs := strings.TrimSpace(q.Get("scope"))
|
||||
if qs != "" {
|
||||
acr.Scope = strings.Split(qs, " ")
|
||||
}
|
||||
|
||||
err := func() error {
|
||||
if acr.ClientID == "" {
|
||||
return NewError(ErrorInvalidRequest)
|
||||
}
|
||||
|
||||
redirectURL := q.Get("redirect_uri")
|
||||
if redirectURL != "" {
|
||||
ru, err := url.Parse(redirectURL)
|
||||
if err != nil {
|
||||
return NewError(ErrorInvalidRequest)
|
||||
}
|
||||
acr.RedirectURL = ru
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
|
||||
return acr, err
|
||||
}
|
262
Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2_test.go
generated
vendored
Normal file
262
Godeps/_workspace/src/github.com/coreos/go-oidc/oauth2/oauth2_test.go
generated
vendored
Normal file
|
@ -0,0 +1,262 @@
|
|||
package oauth2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
)
|
||||
|
||||
func TestParseAuthCodeRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
query url.Values
|
||||
wantACR AuthCodeRequest
|
||||
wantErr error
|
||||
}{
|
||||
// no redirect_uri
|
||||
{
|
||||
query: url.Values{
|
||||
"response_type": []string{"code"},
|
||||
"scope": []string{"foo bar baz"},
|
||||
"client_id": []string{"XXX"},
|
||||
"state": []string{"pants"},
|
||||
},
|
||||
wantACR: AuthCodeRequest{
|
||||
ResponseType: "code",
|
||||
ClientID: "XXX",
|
||||
Scope: []string{"foo", "bar", "baz"},
|
||||
State: "pants",
|
||||
RedirectURL: nil,
|
||||
},
|
||||
},
|
||||
|
||||
// with redirect_uri
|
||||
{
|
||||
query: url.Values{
|
||||
"response_type": []string{"code"},
|
||||
"redirect_uri": []string{"https://127.0.0.1:5555/callback?foo=bar"},
|
||||
"scope": []string{"foo bar baz"},
|
||||
"client_id": []string{"XXX"},
|
||||
"state": []string{"pants"},
|
||||
},
|
||||
wantACR: AuthCodeRequest{
|
||||
ResponseType: "code",
|
||||
ClientID: "XXX",
|
||||
Scope: []string{"foo", "bar", "baz"},
|
||||
State: "pants",
|
||||
RedirectURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "127.0.0.1:5555",
|
||||
Path: "/callback",
|
||||
RawQuery: "foo=bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// unsupported response_type doesn't trigger error
|
||||
{
|
||||
query: url.Values{
|
||||
"response_type": []string{"token"},
|
||||
"redirect_uri": []string{"https://127.0.0.1:5555/callback?foo=bar"},
|
||||
"scope": []string{"foo bar baz"},
|
||||
"client_id": []string{"XXX"},
|
||||
"state": []string{"pants"},
|
||||
},
|
||||
wantACR: AuthCodeRequest{
|
||||
ResponseType: "token",
|
||||
ClientID: "XXX",
|
||||
Scope: []string{"foo", "bar", "baz"},
|
||||
State: "pants",
|
||||
RedirectURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "127.0.0.1:5555",
|
||||
Path: "/callback",
|
||||
RawQuery: "foo=bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// unparseable redirect_uri
|
||||
{
|
||||
query: url.Values{
|
||||
"response_type": []string{"code"},
|
||||
"redirect_uri": []string{":"},
|
||||
"scope": []string{"foo bar baz"},
|
||||
"client_id": []string{"XXX"},
|
||||
"state": []string{"pants"},
|
||||
},
|
||||
wantACR: AuthCodeRequest{
|
||||
ResponseType: "code",
|
||||
ClientID: "XXX",
|
||||
Scope: []string{"foo", "bar", "baz"},
|
||||
State: "pants",
|
||||
},
|
||||
wantErr: NewError(ErrorInvalidRequest),
|
||||
},
|
||||
|
||||
// no client_id, redirect_uri not parsed
|
||||
{
|
||||
query: url.Values{
|
||||
"response_type": []string{"code"},
|
||||
"redirect_uri": []string{"https://127.0.0.1:5555/callback?foo=bar"},
|
||||
"scope": []string{"foo bar baz"},
|
||||
"client_id": []string{},
|
||||
"state": []string{"pants"},
|
||||
},
|
||||
wantACR: AuthCodeRequest{
|
||||
ResponseType: "code",
|
||||
ClientID: "",
|
||||
Scope: []string{"foo", "bar", "baz"},
|
||||
State: "pants",
|
||||
RedirectURL: nil,
|
||||
},
|
||||
wantErr: NewError(ErrorInvalidRequest),
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got, err := ParseAuthCodeRequest(tt.query)
|
||||
if !reflect.DeepEqual(tt.wantErr, err) {
|
||||
t.Errorf("case %d: incorrect error value: want=%q got=%q", i, tt.wantErr, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tt.wantACR, got) {
|
||||
t.Errorf("case %d: incorrect AuthCodeRequest value: want=%#v got=%#v", i, tt.wantACR, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCredsToken(t *testing.T) {
|
||||
hc := &phttp.RequestRecorder{Error: errors.New("error")}
|
||||
cfg := Config{
|
||||
Credentials: ClientCredentials{ID: "cid", Secret: "csecret"},
|
||||
Scope: []string{"foo-scope", "bar-scope"},
|
||||
TokenURL: "http://example.com/token",
|
||||
AuthMethod: AuthMethodClientSecretBasic,
|
||||
RedirectURL: "http://example.com/redirect",
|
||||
AuthURL: "http://example.com/auth",
|
||||
}
|
||||
|
||||
c, err := NewClient(hc, cfg)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
}
|
||||
|
||||
scope := []string{"openid"}
|
||||
c.ClientCredsToken(scope)
|
||||
if hc.Request == nil {
|
||||
t.Error("request is empty")
|
||||
}
|
||||
|
||||
tu := hc.Request.URL.String()
|
||||
if cfg.TokenURL != tu {
|
||||
t.Errorf("wrong token url, want=%v, got=%v", cfg.TokenURL, tu)
|
||||
}
|
||||
|
||||
ct := hc.Request.Header.Get("Content-Type")
|
||||
if ct != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("wrong content-type, want=application/x-www-form-urlencoded, got=%v", ct)
|
||||
}
|
||||
|
||||
cid, secret, ok := phttp.BasicAuth(hc.Request)
|
||||
if !ok {
|
||||
t.Error("unexpected error parsing basic auth")
|
||||
}
|
||||
|
||||
if cfg.Credentials.ID != cid {
|
||||
t.Errorf("wrong client ID, want=%v, got=%v", cfg.Credentials.ID, cid)
|
||||
}
|
||||
|
||||
if cfg.Credentials.Secret != secret {
|
||||
t.Errorf("wrong client secret, want=%v, got=%v", cfg.Credentials.Secret, secret)
|
||||
}
|
||||
|
||||
err = hc.Request.ParseForm()
|
||||
if err != nil {
|
||||
t.Error("unexpected error parsing form")
|
||||
}
|
||||
|
||||
gt := hc.Request.PostForm.Get("grant_type")
|
||||
if gt != GrantTypeClientCreds {
|
||||
t.Errorf("wrong grant_type, want=client_credentials, got=%v", gt)
|
||||
}
|
||||
|
||||
sc := strings.Split(hc.Request.PostForm.Get("scope"), " ")
|
||||
if !reflect.DeepEqual(scope, sc) {
|
||||
t.Errorf("wrong scope, want=%v, got=%v", scope, sc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAuthenticatedRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
authMethod string
|
||||
url string
|
||||
values url.Values
|
||||
}{
|
||||
{
|
||||
authMethod: AuthMethodClientSecretBasic,
|
||||
url: "http://example.com/token",
|
||||
values: url.Values{},
|
||||
},
|
||||
{
|
||||
authMethod: AuthMethodClientSecretPost,
|
||||
url: "http://example.com/token",
|
||||
values: url.Values{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
hc := &phttp.HandlerClient{}
|
||||
cfg := Config{
|
||||
Credentials: ClientCredentials{ID: "cid", Secret: "csecret"},
|
||||
Scope: []string{"foo-scope", "bar-scope"},
|
||||
TokenURL: "http://example.com/token",
|
||||
AuthURL: "http://example.com/auth",
|
||||
RedirectURL: "http://example.com/redirect",
|
||||
AuthMethod: tt.authMethod,
|
||||
}
|
||||
c, err := NewClient(hc, cfg)
|
||||
req, err := c.newAuthenticatedRequest(tt.url, tt.values)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
err = req.ParseForm()
|
||||
if err != nil {
|
||||
t.Errorf("case %d: want nil err, got %v", i, err)
|
||||
}
|
||||
|
||||
if tt.authMethod == AuthMethodClientSecretBasic {
|
||||
cid, secret, ok := phttp.BasicAuth(req)
|
||||
if !ok {
|
||||
t.Errorf("case %d: !ok parsing Basic Auth headers", i)
|
||||
continue
|
||||
}
|
||||
if cid != cfg.Credentials.ID {
|
||||
t.Errorf("case %d: want CID == %q, got CID == %q", i, cfg.Credentials.ID, cid)
|
||||
}
|
||||
if secret != cfg.Credentials.Secret {
|
||||
t.Errorf("case %d: want secret == %q, got secret == %q", i, cfg.Credentials.Secret, secret)
|
||||
}
|
||||
} else if tt.authMethod == AuthMethodClientSecretPost {
|
||||
if req.PostFormValue("client_secret") != cfg.Credentials.Secret {
|
||||
t.Errorf("case %d: want client_secret == %q, got client_secret == %q",
|
||||
i, cfg.Credentials.Secret, req.PostFormValue("client_secret"))
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range tt.values {
|
||||
if !reflect.DeepEqual(v, req.PostForm[k]) {
|
||||
t.Errorf("case %d: key:%q want==%q, got==%q", i, k, v, req.PostForm[k])
|
||||
}
|
||||
}
|
||||
|
||||
if req.URL.String() != tt.url {
|
||||
t.Errorf("case %d: want URL==%q, got URL==%q", i, tt.url, req.URL.String())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
// amount of time that must pass after the last key sync
|
||||
// completes before another attempt may begin
|
||||
keySyncWindow = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultScope = []string{"openid", "email", "profile"}
|
||||
|
||||
supportedAuthMethods = map[string]struct{}{
|
||||
oauth2.AuthMethodClientSecretBasic: struct{}{},
|
||||
oauth2.AuthMethodClientSecretPost: struct{}{},
|
||||
}
|
||||
)
|
||||
|
||||
type ClientCredentials oauth2.ClientCredentials
|
||||
|
||||
type ClientIdentity struct {
|
||||
Credentials ClientCredentials
|
||||
Metadata ClientMetadata
|
||||
}
|
||||
|
||||
type ClientMetadata struct {
|
||||
RedirectURLs []url.URL
|
||||
}
|
||||
|
||||
func (m *ClientMetadata) Valid() error {
|
||||
if len(m.RedirectURLs) == 0 {
|
||||
return errors.New("zero redirect URLs")
|
||||
}
|
||||
|
||||
for _, u := range m.RedirectURLs {
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return errors.New("invalid redirect URL: scheme not http/https")
|
||||
} else if u.Host == "" {
|
||||
return errors.New("invalid redirect URL: host empty")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
HTTPClient phttp.Client
|
||||
Credentials ClientCredentials
|
||||
Scope []string
|
||||
RedirectURL string
|
||||
ProviderConfig ProviderConfig
|
||||
KeySet key.PublicKeySet
|
||||
}
|
||||
|
||||
func NewClient(cfg ClientConfig) (*Client, error) {
|
||||
// Allow empty redirect URL in the case where the client
|
||||
// only needs to verify a given token.
|
||||
ru, err := url.Parse(cfg.RedirectURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid redirect URL: %v", err)
|
||||
}
|
||||
|
||||
c := Client{
|
||||
credentials: cfg.Credentials,
|
||||
httpClient: cfg.HTTPClient,
|
||||
scope: cfg.Scope,
|
||||
redirectURL: ru.String(),
|
||||
providerConfig: cfg.ProviderConfig,
|
||||
keySet: cfg.KeySet,
|
||||
}
|
||||
|
||||
if c.httpClient == nil {
|
||||
c.httpClient = http.DefaultClient
|
||||
}
|
||||
|
||||
if c.scope == nil {
|
||||
c.scope = make([]string, len(DefaultScope))
|
||||
copy(c.scope, DefaultScope)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
httpClient phttp.Client
|
||||
providerConfig ProviderConfig
|
||||
credentials ClientCredentials
|
||||
redirectURL string
|
||||
scope []string
|
||||
keySet key.PublicKeySet
|
||||
|
||||
keySetSyncMutex sync.RWMutex
|
||||
lastKeySetSync time.Time
|
||||
}
|
||||
|
||||
func (c *Client) Healthy() error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
if c.providerConfig.Empty() {
|
||||
return errors.New("oidc client provider config empty")
|
||||
}
|
||||
|
||||
if !c.providerConfig.ExpiresAt.IsZero() && c.providerConfig.ExpiresAt.Before(now) {
|
||||
return errors.New("oidc client provider config expired")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) OAuthClient() (*oauth2.Client, error) {
|
||||
authMethod, err := c.chooseAuthMethod()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ocfg := oauth2.Config{
|
||||
Credentials: oauth2.ClientCredentials(c.credentials),
|
||||
RedirectURL: c.redirectURL,
|
||||
AuthURL: c.providerConfig.AuthEndpoint,
|
||||
TokenURL: c.providerConfig.TokenEndpoint,
|
||||
Scope: c.scope,
|
||||
AuthMethod: authMethod,
|
||||
}
|
||||
|
||||
return oauth2.NewClient(c.httpClient, ocfg)
|
||||
}
|
||||
|
||||
func (c *Client) chooseAuthMethod() (string, error) {
|
||||
if len(c.providerConfig.TokenEndpointAuthMethodsSupported) == 0 {
|
||||
return oauth2.AuthMethodClientSecretBasic, nil
|
||||
}
|
||||
|
||||
for _, authMethod := range c.providerConfig.TokenEndpointAuthMethodsSupported {
|
||||
if _, ok := supportedAuthMethods[authMethod]; ok {
|
||||
return authMethod, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no supported auth methods")
|
||||
}
|
||||
|
||||
func (c *Client) SyncProviderConfig(discoveryURL string) chan struct{} {
|
||||
rp := &providerConfigRepo{c}
|
||||
r := NewHTTPProviderConfigGetter(c.httpClient, discoveryURL)
|
||||
return NewProviderConfigSyncer(r, rp).Run()
|
||||
}
|
||||
|
||||
func (c *Client) maybeSyncKeys() error {
|
||||
tooSoon := func() bool {
|
||||
return time.Now().UTC().Before(c.lastKeySetSync.Add(keySyncWindow))
|
||||
}
|
||||
|
||||
// ignore request to sync keys if a sync operation has been
|
||||
// attempted too recently
|
||||
if tooSoon() {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.keySetSyncMutex.Lock()
|
||||
defer c.keySetSyncMutex.Unlock()
|
||||
|
||||
// check again, as another goroutine may have been holding
|
||||
// the lock while updating the keys
|
||||
if tooSoon() {
|
||||
return nil
|
||||
}
|
||||
|
||||
r := NewRemotePublicKeyRepo(c.httpClient, c.providerConfig.KeysEndpoint)
|
||||
w := &clientKeyRepo{client: c}
|
||||
_, err := key.Sync(r, w)
|
||||
c.lastKeySetSync = time.Now().UTC()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type providerConfigRepo struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (r *providerConfigRepo) Set(cfg ProviderConfig) error {
|
||||
r.client.providerConfig = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
type clientKeyRepo struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (r *clientKeyRepo) Set(ks key.KeySet) error {
|
||||
pks, ok := ks.(*key.PublicKeySet)
|
||||
if !ok {
|
||||
return errors.New("unable to cast to PublicKey")
|
||||
}
|
||||
r.client.keySet = *pks
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ClientCredsToken(scope []string) (jose.JWT, error) {
|
||||
if !c.providerConfig.SupportsGrantType(oauth2.GrantTypeClientCreds) {
|
||||
return jose.JWT{}, fmt.Errorf("%v grant type is not supported", oauth2.GrantTypeClientCreds)
|
||||
}
|
||||
|
||||
oac, err := c.OAuthClient()
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
t, err := oac.ClientCredsToken(scope)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(t.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
return jwt, c.VerifyJWT(jwt)
|
||||
}
|
||||
|
||||
// ExchangeAuthCode exchanges an OAuth2 auth code for an OIDC JWT ID token.
|
||||
func (c *Client) ExchangeAuthCode(code string) (jose.JWT, error) {
|
||||
oac, err := c.OAuthClient()
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
t, err := oac.RequestToken(oauth2.GrantTypeAuthCode, code)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(t.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
return jwt, c.VerifyJWT(jwt)
|
||||
}
|
||||
|
||||
// RefreshToken uses a refresh token to exchange for a new OIDC JWT ID Token.
|
||||
func (c *Client) RefreshToken(refreshToken string) (jose.JWT, error) {
|
||||
oac, err := c.OAuthClient()
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
t, err := oac.RequestToken(oauth2.GrantTypeRefreshToken, refreshToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(t.IDToken)
|
||||
if err != nil {
|
||||
return jose.JWT{}, err
|
||||
}
|
||||
|
||||
return jwt, c.VerifyJWT(jwt)
|
||||
}
|
||||
|
||||
func (c *Client) VerifyJWT(jwt jose.JWT) error {
|
||||
var keysFunc func() []key.PublicKey
|
||||
if kID, ok := jwt.KeyID(); ok {
|
||||
keysFunc = c.keysFuncWithID(kID)
|
||||
} else {
|
||||
keysFunc = c.keysFuncAll()
|
||||
}
|
||||
|
||||
v := NewJWTVerifier(
|
||||
c.providerConfig.Issuer,
|
||||
c.credentials.ID,
|
||||
c.maybeSyncKeys, keysFunc)
|
||||
|
||||
return v.Verify(jwt)
|
||||
}
|
||||
|
||||
// keysFuncWithID returns a function that retrieves at most unexpired
|
||||
// public key from the Client that matches the provided ID
|
||||
func (c *Client) keysFuncWithID(kID string) func() []key.PublicKey {
|
||||
return func() []key.PublicKey {
|
||||
c.keySetSyncMutex.RLock()
|
||||
defer c.keySetSyncMutex.RUnlock()
|
||||
|
||||
if c.keySet.ExpiresAt().Before(time.Now()) {
|
||||
return []key.PublicKey{}
|
||||
}
|
||||
|
||||
k := c.keySet.Key(kID)
|
||||
if k == nil {
|
||||
return []key.PublicKey{}
|
||||
}
|
||||
|
||||
return []key.PublicKey{*k}
|
||||
}
|
||||
}
|
||||
|
||||
// keysFuncAll returns a function that retrieves all unexpired public
|
||||
// keys from the Client
|
||||
func (c *Client) keysFuncAll() func() []key.PublicKey {
|
||||
return func() []key.PublicKey {
|
||||
c.keySetSyncMutex.RLock()
|
||||
defer c.keySetSyncMutex.RUnlock()
|
||||
|
||||
if c.keySet.ExpiresAt().Before(time.Now()) {
|
||||
return []key.PublicKey{}
|
||||
}
|
||||
|
||||
return c.keySet.Keys()
|
||||
}
|
||||
}
|
367
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/client_test.go
generated
vendored
Normal file
367
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/client_test.go
generated
vendored
Normal file
|
@ -0,0 +1,367 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
)
|
||||
|
||||
func TestNewClientScopeDefault(t *testing.T) {
|
||||
tests := []struct {
|
||||
c ClientConfig
|
||||
e []string
|
||||
}{
|
||||
{
|
||||
// No scope
|
||||
c: ClientConfig{RedirectURL: "http://example.com/redirect"},
|
||||
e: DefaultScope,
|
||||
},
|
||||
{
|
||||
// Nil scope
|
||||
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: nil},
|
||||
e: DefaultScope,
|
||||
},
|
||||
{
|
||||
// Empty scope
|
||||
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{}},
|
||||
e: []string{},
|
||||
},
|
||||
{
|
||||
// Custom scope equal to default
|
||||
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{"openid", "email", "profile"}},
|
||||
e: DefaultScope,
|
||||
},
|
||||
{
|
||||
// Custom scope not including defaults
|
||||
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{"foo", "bar"}},
|
||||
e: []string{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
// Custom scopes overlapping with defaults
|
||||
c: ClientConfig{RedirectURL: "http://example.com/redirect", Scope: []string{"openid", "foo"}},
|
||||
e: []string{"openid", "foo"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
c, err := NewClient(tt.c)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error from NewClient: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(tt.e, c.scope) {
|
||||
t.Errorf("case %d: want: %v, got: %v", i, tt.e, c.scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthy(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
|
||||
tests := []struct {
|
||||
c *Client
|
||||
h bool
|
||||
}{
|
||||
// all ok
|
||||
{
|
||||
c: &Client{
|
||||
providerConfig: ProviderConfig{
|
||||
Issuer: "http://example.com",
|
||||
ExpiresAt: now.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
h: true,
|
||||
},
|
||||
// zero-value ProviderConfig.ExpiresAt
|
||||
{
|
||||
c: &Client{
|
||||
providerConfig: ProviderConfig{
|
||||
Issuer: "http://example.com",
|
||||
},
|
||||
},
|
||||
h: true,
|
||||
},
|
||||
// expired ProviderConfig
|
||||
{
|
||||
c: &Client{
|
||||
providerConfig: ProviderConfig{
|
||||
Issuer: "http://example.com",
|
||||
ExpiresAt: now.Add(time.Hour * -1),
|
||||
},
|
||||
},
|
||||
h: false,
|
||||
},
|
||||
// empty ProviderConfig
|
||||
{
|
||||
c: &Client{},
|
||||
h: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
err := tt.c.Healthy()
|
||||
want := tt.h
|
||||
got := (err == nil)
|
||||
|
||||
if want != got {
|
||||
t.Errorf("case %d: want: healthy=%v, got: healhty=%v, err: %v", i, want, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientKeysFuncAll(t *testing.T) {
|
||||
priv1, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate private key, error=%v", err)
|
||||
}
|
||||
|
||||
priv2, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate private key, error=%v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
future := now.Add(time.Hour)
|
||||
past := now.Add(-1 * time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
keySet *key.PublicKeySet
|
||||
want []key.PublicKey
|
||||
}{
|
||||
// two keys, non-expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, future),
|
||||
want: []key.PublicKey{*key.NewPublicKey(priv2.JWK()), *key.NewPublicKey(priv1.JWK())},
|
||||
},
|
||||
|
||||
// no keys, non-expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{}, future),
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
|
||||
// two keys, expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, past),
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
|
||||
// no keys, expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{}, past),
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
var c Client
|
||||
c.keySet = *tt.keySet
|
||||
keysFunc := c.keysFuncAll()
|
||||
got := keysFunc()
|
||||
if !reflect.DeepEqual(tt.want, got) {
|
||||
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientKeysFuncWithID(t *testing.T) {
|
||||
priv1, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate private key, error=%v", err)
|
||||
}
|
||||
|
||||
priv2, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate private key, error=%v", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
future := now.Add(time.Hour)
|
||||
past := now.Add(-1 * time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
keySet *key.PublicKeySet
|
||||
argID string
|
||||
want []key.PublicKey
|
||||
}{
|
||||
// two keys, match, non-expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, future),
|
||||
argID: priv2.ID(),
|
||||
want: []key.PublicKey{*key.NewPublicKey(priv2.JWK())},
|
||||
},
|
||||
|
||||
// two keys, no match, non-expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, future),
|
||||
argID: "XXX",
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
|
||||
// no keys, no match, non-expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{}, future),
|
||||
argID: priv2.ID(),
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
|
||||
// two keys, match, expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{priv2.JWK(), priv1.JWK()}, past),
|
||||
argID: priv2.ID(),
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
|
||||
// no keys, no match, expired set
|
||||
{
|
||||
keySet: key.NewPublicKeySet([]jose.JWK{}, past),
|
||||
argID: priv2.ID(),
|
||||
want: []key.PublicKey{},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
var c Client
|
||||
c.keySet = *tt.keySet
|
||||
keysFunc := c.keysFuncWithID(tt.argID)
|
||||
got := keysFunc()
|
||||
if !reflect.DeepEqual(tt.want, got) {
|
||||
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientMetadataValid(t *testing.T) {
|
||||
tests := []ClientMetadata{
|
||||
// one RedirectURL
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{Scheme: "http", Host: "example.com"}},
|
||||
},
|
||||
|
||||
// one RedirectURL w/ nonempty path
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{Scheme: "http", Host: "example.com", Path: "/foo"}},
|
||||
},
|
||||
|
||||
// two RedirectURLs
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{
|
||||
url.URL{Scheme: "http", Host: "foo.example.com"},
|
||||
url.URL{Scheme: "http", Host: "bar.example.com"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
if err := tt.Valid(); err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientMetadataInvalid(t *testing.T) {
|
||||
tests := []ClientMetadata{
|
||||
// nil RedirectURls slice
|
||||
ClientMetadata{
|
||||
RedirectURLs: nil,
|
||||
},
|
||||
|
||||
// empty RedirectURLs slice
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{},
|
||||
},
|
||||
|
||||
// empty url.URL
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{}},
|
||||
},
|
||||
|
||||
// empty url.URL following OK item
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{Scheme: "http", Host: "example.com"}, url.URL{}},
|
||||
},
|
||||
|
||||
// url.URL with empty Host
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{Scheme: "http", Host: ""}},
|
||||
},
|
||||
|
||||
// url.URL with empty Scheme
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{Scheme: "", Host: "example.com"}},
|
||||
},
|
||||
|
||||
// url.URL with non-HTTP(S) Scheme
|
||||
ClientMetadata{
|
||||
RedirectURLs: []url.URL{url.URL{Scheme: "tcp", Host: "127.0.0.1"}},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
if err := tt.Valid(); err == nil {
|
||||
t.Errorf("case %d: expected non-nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChooseAuthMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
supported []string
|
||||
chosen string
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
supported: []string{},
|
||||
chosen: oauth2.AuthMethodClientSecretBasic,
|
||||
},
|
||||
{
|
||||
supported: []string{oauth2.AuthMethodClientSecretBasic},
|
||||
chosen: oauth2.AuthMethodClientSecretBasic,
|
||||
},
|
||||
{
|
||||
supported: []string{oauth2.AuthMethodClientSecretPost},
|
||||
chosen: oauth2.AuthMethodClientSecretPost,
|
||||
},
|
||||
{
|
||||
supported: []string{oauth2.AuthMethodClientSecretPost, oauth2.AuthMethodClientSecretBasic},
|
||||
chosen: oauth2.AuthMethodClientSecretPost,
|
||||
},
|
||||
{
|
||||
supported: []string{oauth2.AuthMethodClientSecretBasic, oauth2.AuthMethodClientSecretPost},
|
||||
chosen: oauth2.AuthMethodClientSecretBasic,
|
||||
},
|
||||
{
|
||||
supported: []string{oauth2.AuthMethodClientSecretJWT, oauth2.AuthMethodClientSecretPost},
|
||||
chosen: oauth2.AuthMethodClientSecretPost,
|
||||
},
|
||||
{
|
||||
supported: []string{oauth2.AuthMethodClientSecretJWT},
|
||||
chosen: "",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
client := Client{
|
||||
providerConfig: ProviderConfig{
|
||||
TokenEndpointAuthMethodsSupported: tt.supported,
|
||||
},
|
||||
}
|
||||
got, err := client.chooseAuthMethod()
|
||||
if tt.err {
|
||||
if err == nil {
|
||||
t.Errorf("case %d: expected non-nil err", i)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if got != tt.chosen {
|
||||
t.Errorf("case %d: want=%q, got=%q", i, tt.chosen, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
type Identity struct {
|
||||
ID string
|
||||
Name string
|
||||
Email string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func IdentityFromClaims(claims jose.Claims) (*Identity, error) {
|
||||
if claims == nil {
|
||||
return nil, errors.New("nil claim set")
|
||||
}
|
||||
|
||||
var ident Identity
|
||||
var err error
|
||||
var ok bool
|
||||
|
||||
if ident.ID, ok, err = claims.StringClaim("sub"); err != nil {
|
||||
return nil, err
|
||||
} else if !ok {
|
||||
return nil, errors.New("missing required claim: sub")
|
||||
}
|
||||
|
||||
if ident.Email, _, err = claims.StringClaim("email"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exp, ok, err := claims.TimeClaim("exp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
ident.ExpiresAt = exp
|
||||
}
|
||||
|
||||
return &ident, nil
|
||||
}
|
113
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/identity_test.go
generated
vendored
Normal file
113
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/identity_test.go
generated
vendored
Normal file
|
@ -0,0 +1,113 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
func TestIdentityFromClaims(t *testing.T) {
|
||||
tests := []struct {
|
||||
claims jose.Claims
|
||||
want Identity
|
||||
}{
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"sub": "123850281",
|
||||
"name": "Elroy",
|
||||
"email": "elroy@example.com",
|
||||
"exp": float64(1.416935146e+09),
|
||||
},
|
||||
want: Identity{
|
||||
ID: "123850281",
|
||||
Name: "",
|
||||
Email: "elroy@example.com",
|
||||
ExpiresAt: time.Date(2014, time.November, 25, 17, 05, 46, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"sub": "123850281",
|
||||
"name": "Elroy",
|
||||
"exp": float64(1.416935146e+09),
|
||||
},
|
||||
want: Identity{
|
||||
ID: "123850281",
|
||||
Name: "",
|
||||
Email: "",
|
||||
ExpiresAt: time.Date(2014, time.November, 25, 17, 05, 46, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"sub": "123850281",
|
||||
"name": "Elroy",
|
||||
"email": "elroy@example.com",
|
||||
"exp": int64(1416935146),
|
||||
},
|
||||
want: Identity{
|
||||
ID: "123850281",
|
||||
Name: "",
|
||||
Email: "elroy@example.com",
|
||||
ExpiresAt: time.Date(2014, time.November, 25, 17, 05, 46, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"sub": "123850281",
|
||||
"name": "Elroy",
|
||||
"email": "elroy@example.com",
|
||||
},
|
||||
want: Identity{
|
||||
ID: "123850281",
|
||||
Name: "",
|
||||
Email: "elroy@example.com",
|
||||
ExpiresAt: time.Time{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got, err := IdentityFromClaims(tt.claims)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error: %v", i, err)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want, *got) {
|
||||
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, *got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityFromClaimsFail(t *testing.T) {
|
||||
tests := []jose.Claims{
|
||||
// sub incorrect type
|
||||
jose.Claims{
|
||||
"sub": 123,
|
||||
"name": "foo",
|
||||
"email": "elroy@example.com",
|
||||
},
|
||||
// email incorrect type
|
||||
jose.Claims{
|
||||
"sub": "123850281",
|
||||
"name": "Elroy",
|
||||
"email": false,
|
||||
},
|
||||
// exp incorrect type
|
||||
jose.Claims{
|
||||
"sub": "123850281",
|
||||
"name": "Elroy",
|
||||
"email": "elroy@example.com",
|
||||
"exp": "2014-11-25 18:05:46 +0000 UTC",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, err := IdentityFromClaims(tt)
|
||||
if err == nil {
|
||||
t.Errorf("case %d: expected non-nil error", i)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package oidc
|
||||
|
||||
type LoginFunc func(ident Identity, sessionKey string) (redirectURL string, err error)
|
|
@ -0,0 +1,57 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
)
|
||||
|
||||
func NewRemotePublicKeyRepo(hc phttp.Client, ep string) *remotePublicKeyRepo {
|
||||
return &remotePublicKeyRepo{hc: hc, ep: ep}
|
||||
}
|
||||
|
||||
type remotePublicKeyRepo struct {
|
||||
hc phttp.Client
|
||||
ep string
|
||||
}
|
||||
|
||||
func (r *remotePublicKeyRepo) Get() (key.KeySet, error) {
|
||||
req, err := http.NewRequest("GET", r.ep, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := r.hc.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var d struct {
|
||||
Keys []jose.JWK `json:"keys"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(d.Keys) == 0 {
|
||||
return nil, errors.New("zero keys in response")
|
||||
}
|
||||
|
||||
ttl, ok, err := phttp.Cacheable(resp.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("HTTP cache headers not set")
|
||||
}
|
||||
|
||||
exp := time.Now().UTC().Add(ttl)
|
||||
ks := key.NewPublicKeySet(d.Keys, exp)
|
||||
return ks, nil
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/coreos/pkg/timeutil"
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
)
|
||||
|
||||
var (
|
||||
log = capnslog.NewPackageLogger("github.com/coreos/go-oidc", "http")
|
||||
)
|
||||
|
||||
const (
|
||||
MaximumProviderConfigSyncInterval = 24 * time.Hour
|
||||
MinimumProviderConfigSyncInterval = time.Minute
|
||||
|
||||
discoveryConfigPath = "/.well-known/openid-configuration"
|
||||
)
|
||||
|
||||
type ProviderConfig struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
KeysEndpoint string `json:"jwks_uri"`
|
||||
ResponseTypesSupported []string `json:"response_types_supported"`
|
||||
GrantTypesSupported []string `json:"grant_types_supported"`
|
||||
SubjectTypesSupported []string `json:"subject_types_supported"`
|
||||
IDTokenAlgValuesSupported []string `json:"id_token_alg_values_supported"`
|
||||
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
|
||||
ExpiresAt time.Time `json:"-"`
|
||||
}
|
||||
|
||||
func (p ProviderConfig) Empty() bool {
|
||||
return p.Issuer == ""
|
||||
}
|
||||
|
||||
func (p ProviderConfig) SupportsGrantType(grantType string) bool {
|
||||
var supported []string
|
||||
if len(p.GrantTypesSupported) == 0 {
|
||||
// If omitted, the default value is ["authorization_code", "implicit"].
|
||||
// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
|
||||
supported = []string{oauth2.GrantTypeAuthCode, oauth2.GrantTypeImplicit}
|
||||
} else {
|
||||
supported = p.GrantTypesSupported
|
||||
}
|
||||
|
||||
for _, t := range supported {
|
||||
if t == grantType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type ProviderConfigGetter interface {
|
||||
Get() (ProviderConfig, error)
|
||||
}
|
||||
|
||||
type ProviderConfigSetter interface {
|
||||
Set(ProviderConfig) error
|
||||
}
|
||||
|
||||
type ProviderConfigSyncer struct {
|
||||
from ProviderConfigGetter
|
||||
to ProviderConfigSetter
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewProviderConfigSyncer(from ProviderConfigGetter, to ProviderConfigSetter) *ProviderConfigSyncer {
|
||||
return &ProviderConfigSyncer{
|
||||
from: from,
|
||||
to: to,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ProviderConfigSyncer) Run() chan struct{} {
|
||||
stop := make(chan struct{})
|
||||
|
||||
var next pcsStepper
|
||||
next = &pcsStepNext{aft: time.Duration(0)}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.clock.After(next.after()):
|
||||
next = next.step(s.sync)
|
||||
case <-stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stop
|
||||
}
|
||||
|
||||
func (s *ProviderConfigSyncer) sync() (time.Duration, error) {
|
||||
cfg, err := s.from.Get()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err = s.to.Set(cfg); err != nil {
|
||||
return 0, fmt.Errorf("error setting provider config: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("Updating provider config: config=%#v", cfg)
|
||||
|
||||
return nextSyncAfter(cfg.ExpiresAt, s.clock), nil
|
||||
}
|
||||
|
||||
type pcsStepFunc func() (time.Duration, error)
|
||||
|
||||
type pcsStepper interface {
|
||||
after() time.Duration
|
||||
step(pcsStepFunc) pcsStepper
|
||||
}
|
||||
|
||||
type pcsStepNext struct {
|
||||
aft time.Duration
|
||||
}
|
||||
|
||||
func (n *pcsStepNext) after() time.Duration {
|
||||
return n.aft
|
||||
}
|
||||
|
||||
func (n *pcsStepNext) step(fn pcsStepFunc) (next pcsStepper) {
|
||||
ttl, err := fn()
|
||||
if err == nil {
|
||||
next = &pcsStepNext{aft: ttl}
|
||||
log.Debugf("Synced provider config, next attempt in %v", next.after())
|
||||
} else {
|
||||
next = &pcsStepRetry{aft: time.Second}
|
||||
log.Errorf("Provider config sync failed, retrying in %v: %v", next.after(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type pcsStepRetry struct {
|
||||
aft time.Duration
|
||||
}
|
||||
|
||||
func (r *pcsStepRetry) after() time.Duration {
|
||||
return r.aft
|
||||
}
|
||||
|
||||
func (r *pcsStepRetry) step(fn pcsStepFunc) (next pcsStepper) {
|
||||
ttl, err := fn()
|
||||
if err == nil {
|
||||
next = &pcsStepNext{aft: ttl}
|
||||
log.Infof("Provider config sync no longer failing")
|
||||
} else {
|
||||
next = &pcsStepRetry{aft: timeutil.ExpBackoff(r.aft, time.Minute)}
|
||||
log.Errorf("Provider config sync still failing, retrying in %v: %v", next.after(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func nextSyncAfter(exp time.Time, clock clockwork.Clock) time.Duration {
|
||||
if exp.IsZero() {
|
||||
return MaximumProviderConfigSyncInterval
|
||||
}
|
||||
|
||||
t := exp.Sub(clock.Now()) / 2
|
||||
if t > MaximumProviderConfigSyncInterval {
|
||||
t = MaximumProviderConfigSyncInterval
|
||||
} else if t < MinimumProviderConfigSyncInterval {
|
||||
t = MinimumProviderConfigSyncInterval
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
type httpProviderConfigGetter struct {
|
||||
hc phttp.Client
|
||||
issuerURL string
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewHTTPProviderConfigGetter(hc phttp.Client, issuerURL string) *httpProviderConfigGetter {
|
||||
return &httpProviderConfigGetter{
|
||||
hc: hc,
|
||||
issuerURL: issuerURL,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *httpProviderConfigGetter) Get() (cfg ProviderConfig, err error) {
|
||||
req, err := http.NewRequest("GET", r.issuerURL+discoveryConfigPath, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := r.hc.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err = json.NewDecoder(resp.Body).Decode(&cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var ttl time.Duration
|
||||
var ok bool
|
||||
ttl, ok, err = phttp.Cacheable(resp.Header)
|
||||
if err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
cfg.ExpiresAt = r.clock.Now().UTC().Add(ttl)
|
||||
}
|
||||
|
||||
// The issuer value returned MUST be identical to the Issuer URL that was directly used to retrieve the configuration information.
|
||||
// http://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation
|
||||
if !urlEqual(cfg.Issuer, r.issuerURL) {
|
||||
err = fmt.Errorf(`"issuer" in config (%v) does not match provided issuer URL (%v)`, cfg.Issuer, r.issuerURL)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func FetchProviderConfig(hc phttp.Client, issuerURL string) (ProviderConfig, error) {
|
||||
if hc == nil {
|
||||
hc = http.DefaultClient
|
||||
}
|
||||
|
||||
g := NewHTTPProviderConfigGetter(hc, issuerURL)
|
||||
return g.Get()
|
||||
}
|
||||
|
||||
func WaitForProviderConfig(hc phttp.Client, issuerURL string) (pcfg ProviderConfig) {
|
||||
return waitForProviderConfig(hc, issuerURL, clockwork.NewRealClock())
|
||||
}
|
||||
|
||||
func waitForProviderConfig(hc phttp.Client, issuerURL string, clock clockwork.Clock) (pcfg ProviderConfig) {
|
||||
var sleep time.Duration
|
||||
var err error
|
||||
for {
|
||||
pcfg, err = FetchProviderConfig(hc, issuerURL)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
sleep = timeutil.ExpBackoff(sleep, time.Minute)
|
||||
fmt.Printf("Failed fetching provider config, trying again in %v: %v\n", sleep, err)
|
||||
time.Sleep(sleep)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
466
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/provider_test.go
generated
vendored
Normal file
466
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/provider_test.go
generated
vendored
Normal file
|
@ -0,0 +1,466 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/oauth2"
|
||||
)
|
||||
|
||||
type fakeProviderConfigGetterSetter struct {
|
||||
cfg *ProviderConfig
|
||||
getCount int
|
||||
setCount int
|
||||
}
|
||||
|
||||
func (g *fakeProviderConfigGetterSetter) Get() (ProviderConfig, error) {
|
||||
g.getCount++
|
||||
return *g.cfg, nil
|
||||
}
|
||||
|
||||
func (g *fakeProviderConfigGetterSetter) Set(cfg ProviderConfig) error {
|
||||
g.cfg = &cfg
|
||||
g.setCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeProviderConfigHandler struct {
|
||||
cfg ProviderConfig
|
||||
maxAge time.Duration
|
||||
}
|
||||
|
||||
func (s *fakeProviderConfigHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := json.Marshal(s.cfg)
|
||||
if s.maxAge.Seconds() >= 0 {
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(s.maxAge.Seconds())))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func TestHTTPProviderConfigGetter(t *testing.T) {
|
||||
svr := &fakeProviderConfigHandler{}
|
||||
hc := &phttp.HandlerClient{Handler: svr}
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
|
||||
tests := []struct {
|
||||
dsc string
|
||||
age time.Duration
|
||||
cfg ProviderConfig
|
||||
ok bool
|
||||
}{
|
||||
// everything is good
|
||||
{
|
||||
dsc: "https://example.com",
|
||||
age: time.Minute,
|
||||
cfg: ProviderConfig{
|
||||
Issuer: "https://example.com",
|
||||
ExpiresAt: now.Add(time.Minute),
|
||||
},
|
||||
ok: true,
|
||||
},
|
||||
// iss and disco url differ by scheme only (how google works)
|
||||
{
|
||||
dsc: "https://example.com",
|
||||
age: time.Minute,
|
||||
cfg: ProviderConfig{
|
||||
Issuer: "example.com",
|
||||
ExpiresAt: now.Add(time.Minute),
|
||||
},
|
||||
ok: true,
|
||||
},
|
||||
// issuer and discovery URL mismatch
|
||||
{
|
||||
dsc: "https://foo.com",
|
||||
age: time.Minute,
|
||||
cfg: ProviderConfig{
|
||||
Issuer: "https://example.com",
|
||||
ExpiresAt: now.Add(time.Minute),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// missing cache header results in zero ExpiresAt
|
||||
{
|
||||
dsc: "https://example.com",
|
||||
age: -1,
|
||||
cfg: ProviderConfig{
|
||||
Issuer: "https://example.com",
|
||||
},
|
||||
ok: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
svr.cfg = tt.cfg
|
||||
svr.maxAge = tt.age
|
||||
getter := NewHTTPProviderConfigGetter(hc, tt.dsc)
|
||||
getter.clock = fc
|
||||
|
||||
got, err := getter.Get()
|
||||
if err != nil {
|
||||
if tt.ok {
|
||||
t.Fatalf("test %d: unexpected error: %v", i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !tt.ok {
|
||||
t.Fatalf("test %d: expected error", i)
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tt.cfg, got) {
|
||||
t.Fatalf("test %d: want: %#v, got: %#v", i, tt.cfg, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderConfigSyncerRun(t *testing.T) {
|
||||
c1 := &ProviderConfig{
|
||||
Issuer: "http://first.example.com",
|
||||
}
|
||||
c2 := &ProviderConfig{
|
||||
Issuer: "http://second.example.com",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
first *ProviderConfig
|
||||
advance time.Duration
|
||||
second *ProviderConfig
|
||||
firstExp time.Duration
|
||||
secondExp time.Duration
|
||||
count int
|
||||
}{
|
||||
// exp is 10m, should have same config after 1s
|
||||
{
|
||||
first: c1,
|
||||
firstExp: time.Duration(10 * time.Minute),
|
||||
advance: time.Minute,
|
||||
second: c1,
|
||||
secondExp: time.Duration(10 * time.Minute),
|
||||
count: 1,
|
||||
},
|
||||
// exp is 10m, should have new config after 10/2 = 5m
|
||||
{
|
||||
first: c1,
|
||||
firstExp: time.Duration(10 * time.Minute),
|
||||
advance: time.Duration(5 * time.Minute),
|
||||
second: c2,
|
||||
secondExp: time.Duration(10 * time.Minute),
|
||||
count: 2,
|
||||
},
|
||||
// exp is 20m, should have new config after 20/2 = 10m
|
||||
{
|
||||
first: c1,
|
||||
firstExp: time.Duration(20 * time.Minute),
|
||||
advance: time.Duration(10 * time.Minute),
|
||||
second: c2,
|
||||
secondExp: time.Duration(30 * time.Minute),
|
||||
count: 2,
|
||||
},
|
||||
}
|
||||
|
||||
assertCfg := func(i int, to *fakeProviderConfigGetterSetter, want ProviderConfig) {
|
||||
got, err := to.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: unable to get config: %v", i, err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("test %d: incorrect state:\nwant=%#v\ngot=%#v", i, want, got)
|
||||
}
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
from := &fakeProviderConfigGetterSetter{}
|
||||
to := &fakeProviderConfigGetterSetter{}
|
||||
|
||||
fc := clockwork.NewFakeClock()
|
||||
now := fc.Now().UTC()
|
||||
syncer := NewProviderConfigSyncer(from, to)
|
||||
syncer.clock = fc
|
||||
|
||||
tt.first.ExpiresAt = now.Add(tt.firstExp)
|
||||
tt.second.ExpiresAt = now.Add(tt.secondExp)
|
||||
if err := from.Set(*tt.first); err != nil {
|
||||
t.Fatalf("test %d: unexpected error: %v", i, err)
|
||||
}
|
||||
|
||||
stop := syncer.Run()
|
||||
defer close(stop)
|
||||
fc.BlockUntil(1)
|
||||
|
||||
// first sync
|
||||
assertCfg(i, to, *tt.first)
|
||||
|
||||
if err := from.Set(*tt.second); err != nil {
|
||||
t.Fatalf("test %d: unexpected error: %v", i, err)
|
||||
}
|
||||
|
||||
fc.Advance(tt.advance)
|
||||
fc.BlockUntil(1)
|
||||
|
||||
// second sync
|
||||
assertCfg(i, to, *tt.second)
|
||||
|
||||
if tt.count != from.getCount {
|
||||
t.Fatalf("test %d: want: %v, got: %v", i, tt.count, from.getCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type staticProviderConfigGetter struct {
|
||||
cfg ProviderConfig
|
||||
err error
|
||||
}
|
||||
|
||||
func (g *staticProviderConfigGetter) Get() (ProviderConfig, error) {
|
||||
return g.cfg, g.err
|
||||
}
|
||||
|
||||
type staticProviderConfigSetter struct {
|
||||
cfg *ProviderConfig
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *staticProviderConfigSetter) Set(cfg ProviderConfig) error {
|
||||
s.cfg = &cfg
|
||||
return s.err
|
||||
}
|
||||
|
||||
func TestProviderConfigSyncerSyncFailure(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
|
||||
tests := []struct {
|
||||
from *staticProviderConfigGetter
|
||||
to *staticProviderConfigSetter
|
||||
|
||||
// want indicates what ProviderConfig should be passed to Set.
|
||||
// If nil, the Set should not be called.
|
||||
want *ProviderConfig
|
||||
}{
|
||||
// generic Get failure
|
||||
{
|
||||
from: &staticProviderConfigGetter{err: errors.New("fail")},
|
||||
to: &staticProviderConfigSetter{},
|
||||
want: nil,
|
||||
},
|
||||
// generic Set failure
|
||||
{
|
||||
from: &staticProviderConfigGetter{cfg: ProviderConfig{ExpiresAt: fc.Now().Add(time.Minute)}},
|
||||
to: &staticProviderConfigSetter{err: errors.New("fail")},
|
||||
want: &ProviderConfig{ExpiresAt: fc.Now().Add(time.Minute)},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
pcs := &ProviderConfigSyncer{
|
||||
from: tt.from,
|
||||
to: tt.to,
|
||||
clock: fc,
|
||||
}
|
||||
_, err := pcs.sync()
|
||||
if err == nil {
|
||||
t.Errorf("case %d: expected non-nil error", i)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want, tt.to.cfg) {
|
||||
t.Errorf("case %d: Set mismatch: want=%#v got=%#v", i, tt.want, tt.to.cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextSyncAfter(t *testing.T) {
|
||||
fc := clockwork.NewFakeClock()
|
||||
|
||||
tests := []struct {
|
||||
exp time.Time
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
exp: fc.Now().Add(time.Hour),
|
||||
want: 30 * time.Minute,
|
||||
},
|
||||
// override large values with the maximum
|
||||
{
|
||||
exp: fc.Now().Add(168 * time.Hour), // one week
|
||||
want: 24 * time.Hour,
|
||||
},
|
||||
// override "now" values with the minimum
|
||||
{
|
||||
exp: fc.Now(),
|
||||
want: time.Minute,
|
||||
},
|
||||
// override negative values with the minimum
|
||||
{
|
||||
exp: fc.Now().Add(-1 * time.Minute),
|
||||
want: time.Minute,
|
||||
},
|
||||
// zero-value Time results in maximum sync interval
|
||||
{
|
||||
exp: time.Time{},
|
||||
want: 24 * time.Hour,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := nextSyncAfter(tt.exp, fc)
|
||||
if tt.want != got {
|
||||
t.Errorf("case %d: want=%v got=%v", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderConfigEmpty(t *testing.T) {
|
||||
cfg := ProviderConfig{}
|
||||
if !cfg.Empty() {
|
||||
t.Fatalf("Empty provider config reports non-empty")
|
||||
}
|
||||
cfg = ProviderConfig{Issuer: "http://example.com"}
|
||||
if cfg.Empty() {
|
||||
t.Fatalf("Non-empty provider config reports empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPCSStepAfter(t *testing.T) {
|
||||
pass := func() (time.Duration, error) { return 7 * time.Second, nil }
|
||||
fail := func() (time.Duration, error) { return 0, errors.New("fail") }
|
||||
|
||||
tests := []struct {
|
||||
stepper pcsStepper
|
||||
stepFunc pcsStepFunc
|
||||
want pcsStepper
|
||||
}{
|
||||
// good step results in retry at TTL
|
||||
{
|
||||
stepper: &pcsStepNext{},
|
||||
stepFunc: pass,
|
||||
want: &pcsStepNext{aft: 7 * time.Second},
|
||||
},
|
||||
|
||||
// good step after failed step results results in retry at TTL
|
||||
{
|
||||
stepper: &pcsStepRetry{aft: 2 * time.Second},
|
||||
stepFunc: pass,
|
||||
want: &pcsStepNext{aft: 7 * time.Second},
|
||||
},
|
||||
|
||||
// failed step results in a retry in 1s
|
||||
{
|
||||
stepper: &pcsStepNext{},
|
||||
stepFunc: fail,
|
||||
want: &pcsStepRetry{aft: time.Second},
|
||||
},
|
||||
|
||||
// failed retry backs off by a factor of 2
|
||||
{
|
||||
stepper: &pcsStepRetry{aft: time.Second},
|
||||
stepFunc: fail,
|
||||
want: &pcsStepRetry{aft: 2 * time.Second},
|
||||
},
|
||||
|
||||
// failed retry backs off by a factor of 2, up to 1m
|
||||
{
|
||||
stepper: &pcsStepRetry{aft: 32 * time.Second},
|
||||
stepFunc: fail,
|
||||
want: &pcsStepRetry{aft: 60 * time.Second},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := tt.stepper.step(tt.stepFunc)
|
||||
if !reflect.DeepEqual(tt.want, got) {
|
||||
t.Errorf("case %d: want=%#v got=%#v", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderConfigSupportsGrantType(t *testing.T) {
|
||||
tests := []struct {
|
||||
types []string
|
||||
typ string
|
||||
want bool
|
||||
}{
|
||||
// explicitly supported
|
||||
{
|
||||
types: []string{"foo_type"},
|
||||
typ: "foo_type",
|
||||
want: true,
|
||||
},
|
||||
|
||||
// explicitly unsupported
|
||||
{
|
||||
types: []string{"bar_type"},
|
||||
typ: "foo_type",
|
||||
want: false,
|
||||
},
|
||||
|
||||
// default type explicitly unsupported
|
||||
{
|
||||
types: []string{oauth2.GrantTypeImplicit},
|
||||
typ: oauth2.GrantTypeAuthCode,
|
||||
want: false,
|
||||
},
|
||||
|
||||
// type not found in default set
|
||||
{
|
||||
types: []string{},
|
||||
typ: "foo_type",
|
||||
want: false,
|
||||
},
|
||||
|
||||
// type found in default set
|
||||
{
|
||||
types: []string{},
|
||||
typ: oauth2.GrantTypeAuthCode,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
cfg := ProviderConfig{
|
||||
GrantTypesSupported: tt.types,
|
||||
}
|
||||
got := cfg.SupportsGrantType(tt.typ)
|
||||
if tt.want != got {
|
||||
t.Errorf("case %d: assert %v supports %v: want=%t got=%t", i, tt.types, tt.typ, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForProviderConfigImmediateSuccess(t *testing.T) {
|
||||
cfg := ProviderConfig{Issuer: "http://example.com"}
|
||||
b, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed marshaling provider config")
|
||||
}
|
||||
|
||||
resp := http.Response{Body: ioutil.NopCloser(bytes.NewBuffer(b))}
|
||||
hc := &phttp.RequestRecorder{Response: &resp}
|
||||
fc := clockwork.NewFakeClock()
|
||||
|
||||
reschan := make(chan ProviderConfig)
|
||||
go func() {
|
||||
reschan <- waitForProviderConfig(hc, cfg.Issuer, fc)
|
||||
}()
|
||||
|
||||
var got ProviderConfig
|
||||
select {
|
||||
case got = <-reschan:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("Did not receive result within 1s")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg, got) {
|
||||
t.Fatalf("Received incorrect provider config: want=%#v got=%#v", cfg, got)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
type TokenRefresher interface {
|
||||
// Verify checks if the provided token is currently valid or not.
|
||||
Verify(jose.JWT) error
|
||||
|
||||
// Refresh attempts to authenticate and retrieve a new token.
|
||||
Refresh() (jose.JWT, error)
|
||||
}
|
||||
|
||||
type ClientCredsTokenRefresher struct {
|
||||
Issuer string
|
||||
OIDCClient *Client
|
||||
}
|
||||
|
||||
func (c *ClientCredsTokenRefresher) Verify(jwt jose.JWT) (err error) {
|
||||
_, err = VerifyClientClaims(jwt, c.Issuer)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ClientCredsTokenRefresher) Refresh() (jwt jose.JWT, err error) {
|
||||
if err = c.OIDCClient.Healthy(); err != nil {
|
||||
err = fmt.Errorf("unable to authenticate, unhealthy OIDC client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
jwt, err = c.OIDCClient.ClientCredsToken([]string{"openid"})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("unable to verify auth code with issuer: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type AuthenticatedTransport struct {
|
||||
TokenRefresher
|
||||
http.RoundTripper
|
||||
|
||||
mu sync.Mutex
|
||||
jwt jose.JWT
|
||||
}
|
||||
|
||||
func (t *AuthenticatedTransport) verifiedJWT() (jose.JWT, error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.TokenRefresher.Verify(t.jwt) == nil {
|
||||
return t.jwt, nil
|
||||
}
|
||||
|
||||
jwt, err := t.TokenRefresher.Refresh()
|
||||
if err != nil {
|
||||
return jose.JWT{}, fmt.Errorf("unable to acquire valid JWT: %v", err)
|
||||
}
|
||||
|
||||
t.jwt = jwt
|
||||
return t.jwt, nil
|
||||
}
|
||||
|
||||
func (t *AuthenticatedTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
jwt, err := t.verifiedJWT()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := phttp.CopyRequest(r)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", jwt.Encode()))
|
||||
return t.RoundTripper.RoundTrip(req)
|
||||
}
|
167
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/transport_test.go
generated
vendored
Normal file
167
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/transport_test.go
generated
vendored
Normal file
|
@ -0,0 +1,167 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
phttp "github.com/coreos/go-oidc/http"
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
type staticTokenRefresher struct {
|
||||
verify func(jose.JWT) error
|
||||
refresh func() (jose.JWT, error)
|
||||
}
|
||||
|
||||
func (s *staticTokenRefresher) Verify(jwt jose.JWT) error {
|
||||
return s.verify(jwt)
|
||||
}
|
||||
|
||||
func (s *staticTokenRefresher) Refresh() (jose.JWT, error) {
|
||||
return s.refresh()
|
||||
}
|
||||
|
||||
func TestAuthenticatedTransportVerifiedJWT(t *testing.T) {
|
||||
tests := []struct {
|
||||
refresher TokenRefresher
|
||||
startJWT jose.JWT
|
||||
wantJWT jose.JWT
|
||||
wantError error
|
||||
}{
|
||||
// verification succeeds, so refresh is not called
|
||||
{
|
||||
refresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return nil },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "2"}, nil },
|
||||
},
|
||||
startJWT: jose.JWT{RawPayload: "1"},
|
||||
wantJWT: jose.JWT{RawPayload: "1"},
|
||||
},
|
||||
|
||||
// verification fails, refresh succeeds so cached JWT changes
|
||||
{
|
||||
refresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return errors.New("fail!") },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "2"}, nil },
|
||||
},
|
||||
startJWT: jose.JWT{RawPayload: "1"},
|
||||
wantJWT: jose.JWT{RawPayload: "2"},
|
||||
},
|
||||
|
||||
// verification succeeds, so failing refresh isn't attempted
|
||||
{
|
||||
refresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return nil },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{}, errors.New("fail!") },
|
||||
},
|
||||
startJWT: jose.JWT{RawPayload: "1"},
|
||||
wantJWT: jose.JWT{RawPayload: "1"},
|
||||
},
|
||||
|
||||
// verification fails, but refresh fails, too
|
||||
{
|
||||
refresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return errors.New("fail!") },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{}, errors.New("fail!") },
|
||||
},
|
||||
startJWT: jose.JWT{RawPayload: "1"},
|
||||
wantJWT: jose.JWT{},
|
||||
wantError: errors.New("unable to acquire valid JWT: fail!"),
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
at := &AuthenticatedTransport{
|
||||
TokenRefresher: tt.refresher,
|
||||
jwt: tt.startJWT,
|
||||
}
|
||||
|
||||
gotJWT, err := at.verifiedJWT()
|
||||
if !reflect.DeepEqual(tt.wantError, err) {
|
||||
t.Errorf("#%d: unexpected error: want=%#v got=%#v", i, tt.wantError, err)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.wantJWT, gotJWT) {
|
||||
t.Errorf("#%d: incorrect JWT returned from verifiedJWT: want=%#v got=%#v", i, tt.wantJWT, gotJWT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticatedTransportJWTCaching(t *testing.T) {
|
||||
at := &AuthenticatedTransport{
|
||||
TokenRefresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return errors.New("fail!") },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "2"}, nil },
|
||||
},
|
||||
jwt: jose.JWT{RawPayload: "1"},
|
||||
}
|
||||
|
||||
wantJWT := jose.JWT{RawPayload: "2"}
|
||||
gotJWT, err := at.verifiedJWT()
|
||||
if err != nil {
|
||||
t.Fatalf("got non-nil error: %#v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(wantJWT, gotJWT) {
|
||||
t.Fatalf("incorrect JWT returned from verifiedJWT: want=%#v got=%#v", wantJWT, gotJWT)
|
||||
}
|
||||
|
||||
at.TokenRefresher = &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return nil },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{RawPayload: "3"}, nil },
|
||||
}
|
||||
|
||||
// the previous JWT should still be cached on the AuthenticatedTransport since
|
||||
// it is still valid, even though there's a new token ready to refresh
|
||||
gotJWT, err = at.verifiedJWT()
|
||||
if err != nil {
|
||||
t.Fatalf("got non-nil error: %#v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(wantJWT, gotJWT) {
|
||||
t.Fatalf("incorrect JWT returned from verifiedJWT: want=%#v got=%#v", wantJWT, gotJWT)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticatedTransportRoundTrip(t *testing.T) {
|
||||
rr := &phttp.RequestRecorder{Response: &http.Response{StatusCode: http.StatusOK}}
|
||||
at := &AuthenticatedTransport{
|
||||
TokenRefresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return nil },
|
||||
},
|
||||
RoundTripper: rr,
|
||||
jwt: jose.JWT{RawPayload: "1"},
|
||||
}
|
||||
|
||||
req := http.Request{}
|
||||
_, err := at.RoundTrip(&req)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(req, http.Request{}) {
|
||||
t.Errorf("http.Request object was modified")
|
||||
}
|
||||
|
||||
want := []string{"Bearer .1."}
|
||||
got := rr.Request.Header["Authorization"]
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Errorf("incorrect Authorization header: want=%#v got=%#v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticatedTransportRoundTripRefreshFail(t *testing.T) {
|
||||
rr := &phttp.RequestRecorder{Response: &http.Response{StatusCode: http.StatusOK}}
|
||||
at := &AuthenticatedTransport{
|
||||
TokenRefresher: &staticTokenRefresher{
|
||||
verify: func(jose.JWT) error { return errors.New("fail!") },
|
||||
refresh: func() (jose.JWT, error) { return jose.JWT{}, errors.New("fail!") },
|
||||
},
|
||||
RoundTripper: rr,
|
||||
jwt: jose.JWT{RawPayload: "1"},
|
||||
}
|
||||
|
||||
_, err := at.RoundTrip(&http.Request{})
|
||||
if err == nil {
|
||||
t.Errorf("expected non-nil error")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
// RequestTokenExtractor funcs extract a raw encoded token from a request.
|
||||
type RequestTokenExtractor func(r *http.Request) (string, error)
|
||||
|
||||
// ExtractBearerToken is a RequestTokenExtractor which extracts a bearer token from a request's
|
||||
// Authorization header.
|
||||
func ExtractBearerToken(r *http.Request) (string, error) {
|
||||
ah := r.Header.Get("Authorization")
|
||||
if ah == "" {
|
||||
return "", errors.New("missing Authorization header")
|
||||
}
|
||||
|
||||
if len(ah) <= 6 || strings.ToUpper(ah[0:6]) != "BEARER" {
|
||||
return "", errors.New("should be a bearer token")
|
||||
}
|
||||
|
||||
val := ah[7:]
|
||||
if len(val) == 0 {
|
||||
return "", errors.New("bearer token is empty")
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// CookieTokenExtractor returns a RequestTokenExtractor which extracts a token from the named cookie in a request.
|
||||
func CookieTokenExtractor(cookieName string) RequestTokenExtractor {
|
||||
return func(r *http.Request) (string, error) {
|
||||
ck, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("token cookie not found in request: %v", err)
|
||||
}
|
||||
|
||||
if ck.Value == "" {
|
||||
return "", errors.New("token cookie found but is empty")
|
||||
}
|
||||
|
||||
return ck.Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewClaims(iss, sub, aud string, iat, exp time.Time) jose.Claims {
|
||||
return jose.Claims{
|
||||
// required
|
||||
"iss": iss,
|
||||
"sub": sub,
|
||||
"aud": aud,
|
||||
"iat": float64(iat.Unix()),
|
||||
"exp": float64(exp.Unix()),
|
||||
}
|
||||
}
|
||||
|
||||
func GenClientID(hostport string) (string, error) {
|
||||
b, err := randBytes(32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var host string
|
||||
if strings.Contains(hostport, ":") {
|
||||
host, _, err = net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
host = hostport
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s@%s", base64.URLEncoding.EncodeToString(b), host), nil
|
||||
}
|
||||
|
||||
func randBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
got, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if n != got {
|
||||
return nil, errors.New("unable to generate enough random data")
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// urlEqual checks two urls for equality using only the host and path portions.
|
||||
func urlEqual(url1, url2 string) bool {
|
||||
u1, err := url.Parse(url1)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
u2, err := url.Parse(url2)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.ToLower(u1.Host+u1.Path) == strings.ToLower(u2.Host+u2.Path)
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
)
|
||||
|
||||
func TestCookieTokenExtractorInvalid(t *testing.T) {
|
||||
ckName := "tokenCookie"
|
||||
tests := []*http.Cookie{
|
||||
&http.Cookie{},
|
||||
&http.Cookie{Name: ckName},
|
||||
&http.Cookie{Name: ckName, Value: ""},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r, _ := http.NewRequest("", "", nil)
|
||||
r.AddCookie(tt)
|
||||
_, err := CookieTokenExtractor(ckName)(r)
|
||||
if err == nil {
|
||||
t.Errorf("case %d: want: error for invalid cookie token, got: no error.", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieTokenExtractorValid(t *testing.T) {
|
||||
validToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||
ckName := "tokenCookie"
|
||||
tests := []*http.Cookie{
|
||||
&http.Cookie{Name: ckName, Value: "some non-empty value"},
|
||||
&http.Cookie{Name: ckName, Value: validToken},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r, _ := http.NewRequest("", "", nil)
|
||||
r.AddCookie(tt)
|
||||
_, err := CookieTokenExtractor(ckName)(r)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: want: valid cookie with no error, got: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBearerTokenInvalid(t *testing.T) {
|
||||
tests := []string{"", "x", "Bearer", "xxxxxxx", "Bearer "}
|
||||
|
||||
for i, tt := range tests {
|
||||
r, _ := http.NewRequest("", "", nil)
|
||||
r.Header.Add("Authorization", tt)
|
||||
_, err := ExtractBearerToken(r)
|
||||
if err == nil {
|
||||
t.Errorf("case %d: want: invalid Authorization header, got: valid Authorization header.", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractBearerTokenValid(t *testing.T) {
|
||||
validToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
|
||||
tests := []string{
|
||||
fmt.Sprintf("Bearer %s", validToken),
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
r, _ := http.NewRequest("", "", nil)
|
||||
r.Header.Add("Authorization", tt)
|
||||
_, err := ExtractBearerToken(r)
|
||||
if err != nil {
|
||||
t.Errorf("case %d: want: valid Authorization header, got: invalid Authorization header: %v.", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClaims(t *testing.T) {
|
||||
issAt := time.Date(2, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
expAt := time.Date(2, time.January, 1, 1, 0, 0, 0, time.UTC)
|
||||
|
||||
want := jose.Claims{
|
||||
"iss": "https://example.com",
|
||||
"sub": "user-123",
|
||||
"aud": "client-abc",
|
||||
"iat": float64(issAt.Unix()),
|
||||
"exp": float64(expAt.Unix()),
|
||||
}
|
||||
|
||||
got := NewClaims("https://example.com", "user-123", "client-abc", issAt, expAt)
|
||||
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
t.Fatalf("want=%#v got=%#v", want, got)
|
||||
}
|
||||
}
|
167
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/verification.go
generated
vendored
Normal file
167
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/verification.go
generated
vendored
Normal file
|
@ -0,0 +1,167 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jonboulle/clockwork"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
)
|
||||
|
||||
func VerifySignature(jwt jose.JWT, keys []key.PublicKey) (bool, error) {
|
||||
jwtBytes := []byte(jwt.Data())
|
||||
for _, k := range keys {
|
||||
v, err := k.Verifier()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if v.Verify(jwt.Signature, jwtBytes) == nil {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Verify claims in accordance with OIDC spec
|
||||
// http://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation
|
||||
func VerifyClaims(jwt jose.JWT, issuer, clientID string) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ident, err := IdentityFromClaims(claims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ident.ExpiresAt.Before(now) {
|
||||
return errors.New("token is expired")
|
||||
}
|
||||
|
||||
// iss REQUIRED. Issuer Identifier for the Issuer of the response.
|
||||
// The iss value is a case sensitive URL using the https scheme that contains scheme, host, and optionally, port number and path components and no query or fragment components.
|
||||
if iss, exists := claims["iss"].(string); exists {
|
||||
if !urlEqual(iss, issuer) {
|
||||
return fmt.Errorf("invalid claim value: 'iss'. expected=%s, found=%s.", issuer, iss)
|
||||
}
|
||||
} else {
|
||||
return errors.New("missing claim: 'iss'")
|
||||
}
|
||||
|
||||
// iat REQUIRED. Time at which the JWT was issued.
|
||||
// Its value is a JSON number representing the number of seconds from 1970-01-01T0:0:0Z as measured in UTC until the date/time.
|
||||
if _, exists := claims["iat"].(float64); !exists {
|
||||
return errors.New("missing claim: 'iat'")
|
||||
}
|
||||
|
||||
// aud REQUIRED. Audience(s) that this ID Token is intended for.
|
||||
// It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. It MAY also contain identifiers for other audiences. In the general case, the aud value is an array of case sensitive strings. In the common special case when there is one audience, the aud value MAY be a single case sensitive string.
|
||||
if aud, exists := claims["aud"].(string); exists {
|
||||
if aud != clientID {
|
||||
return errors.New("invalid claim value: 'aud'")
|
||||
}
|
||||
} else {
|
||||
return errors.New("missing claim: 'aud'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyClientClaims verifies all the required claims are valid for a "client credentials" JWT.
|
||||
// Returns the client ID if valid, or an error if invalid.
|
||||
func VerifyClientClaims(jwt jose.JWT, issuer string) (string, error) {
|
||||
claims, err := jwt.Claims()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse JWT claims: %v", err)
|
||||
}
|
||||
|
||||
iss, ok, err := claims.StringClaim("iss")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'iss' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'iss' claim")
|
||||
} else if !urlEqual(iss, issuer) {
|
||||
return "", fmt.Errorf("'iss' claim does not match expected issuer, iss=%s", iss)
|
||||
}
|
||||
|
||||
sub, ok, err := claims.StringClaim("sub")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'sub' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'sub' claim")
|
||||
}
|
||||
|
||||
aud, ok, err := claims.StringClaim("aud")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'aud' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'aud' claim")
|
||||
}
|
||||
|
||||
if sub != aud {
|
||||
return "", fmt.Errorf("invalid claims, 'aud' claim and 'sub' claim do not match, aud=%s, sub=%s", aud, sub)
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
exp, ok, err := claims.TimeClaim("exp")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse 'exp' claim: %v", err)
|
||||
} else if !ok {
|
||||
return "", errors.New("missing required 'exp' claim")
|
||||
} else if exp.Before(now) {
|
||||
return "", fmt.Errorf("token already expired at: %v", exp)
|
||||
}
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
type JWTVerifier struct {
|
||||
issuer string
|
||||
clientID string
|
||||
syncFunc func() error
|
||||
keysFunc func() []key.PublicKey
|
||||
clock clockwork.Clock
|
||||
}
|
||||
|
||||
func NewJWTVerifier(issuer, clientID string, syncFunc func() error, keysFunc func() []key.PublicKey) JWTVerifier {
|
||||
return JWTVerifier{
|
||||
issuer: issuer,
|
||||
clientID: clientID,
|
||||
syncFunc: syncFunc,
|
||||
keysFunc: keysFunc,
|
||||
clock: clockwork.NewRealClock(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *JWTVerifier) Verify(jwt jose.JWT) error {
|
||||
ok, err := VerifySignature(jwt, v.keysFunc())
|
||||
if ok {
|
||||
goto SignatureVerified
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
|
||||
}
|
||||
|
||||
if err = v.syncFunc(); err != nil {
|
||||
return fmt.Errorf("oidc: failed syncing KeySet: %v", err)
|
||||
}
|
||||
|
||||
ok, err = VerifySignature(jwt, v.keysFunc())
|
||||
if err != nil {
|
||||
return fmt.Errorf("oidc: JWT signature verification failed: %v", err)
|
||||
} else if !ok {
|
||||
return errors.New("oidc: unable to verify JWT signature: no matching keys")
|
||||
}
|
||||
|
||||
SignatureVerified:
|
||||
if err := VerifyClaims(jwt, v.issuer, v.clientID); err != nil {
|
||||
return fmt.Errorf("oidc: JWT claims invalid: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
297
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/verification_test.go
generated
vendored
Normal file
297
Godeps/_workspace/src/github.com/coreos/go-oidc/oidc/verification_test.go
generated
vendored
Normal file
|
@ -0,0 +1,297 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/coreos/go-oidc/key"
|
||||
)
|
||||
|
||||
func TestVerifyClientClaims(t *testing.T) {
|
||||
validIss := "https://example.com"
|
||||
validClientID := "valid-client"
|
||||
now := time.Now()
|
||||
tomorrow := now.Add(24 * time.Hour)
|
||||
header := jose.JOSEHeader{
|
||||
jose.HeaderKeyAlgorithm: "test-alg",
|
||||
jose.HeaderKeyID: "1",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
claims jose.Claims
|
||||
ok bool
|
||||
}{
|
||||
// valid token
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": validIss,
|
||||
"sub": validClientID,
|
||||
"aud": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: true,
|
||||
},
|
||||
// missing 'iss' claim
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"sub": validClientID,
|
||||
"aud": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// invalid 'iss' claim
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": "INVALID",
|
||||
"sub": validClientID,
|
||||
"aud": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// missing 'sub' claim
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": validIss,
|
||||
"aud": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// invalid 'sub' claim
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": validIss,
|
||||
"sub": "INVALID",
|
||||
"aud": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// missing 'aud' claim
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": validIss,
|
||||
"sub": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// invalid 'aud' claim
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": validIss,
|
||||
"sub": validClientID,
|
||||
"aud": "INVALID",
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(tomorrow.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
// expired
|
||||
{
|
||||
claims: jose.Claims{
|
||||
"iss": validIss,
|
||||
"sub": validClientID,
|
||||
"aud": validClientID,
|
||||
"iat": float64(now.Unix()),
|
||||
"exp": float64(now.Unix()),
|
||||
},
|
||||
ok: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
jwt, err := jose.NewJWT(header, tt.claims)
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: Failed to generate JWT, error=%v", i, err)
|
||||
}
|
||||
|
||||
got, err := VerifyClientClaims(jwt, validIss)
|
||||
if tt.ok {
|
||||
if err != nil {
|
||||
t.Errorf("case %d: unexpected error, err=%v", i, err)
|
||||
}
|
||||
if got != validClientID {
|
||||
t.Errorf("case %d: incorrect client ID, want=%s, got=%s", i, validClientID, got)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Errorf("case %d: expected error but err is nil", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTVerifier(t *testing.T) {
|
||||
iss := "http://example.com"
|
||||
now := time.Now()
|
||||
future12 := now.Add(12 * time.Hour)
|
||||
past36 := now.Add(-36 * time.Hour)
|
||||
past12 := now.Add(-12 * time.Hour)
|
||||
|
||||
priv1, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate private key, error=%v", err)
|
||||
}
|
||||
pk1 := *key.NewPublicKey(priv1.JWK())
|
||||
|
||||
priv2, err := key.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate private key, error=%v", err)
|
||||
}
|
||||
pk2 := *key.NewPublicKey(priv2.JWK())
|
||||
|
||||
jwtPK1, err := jose.NewSignedJWT(NewClaims(iss, "XXX", "XXX", past12, future12), priv1.Signer())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
jwtPK1BadClaims, err := jose.NewSignedJWT(NewClaims(iss, "XXX", "YYY", past12, future12), priv1.Signer())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
jwtExpired, err := jose.NewSignedJWT(NewClaims(iss, "XXX", "XXX", past36, past12), priv1.Signer())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
jwtPK2, err := jose.NewSignedJWT(NewClaims(iss, "XXX", "XXX", past12, future12), priv2.Signer())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
verifier JWTVerifier
|
||||
jwt jose.JWT
|
||||
wantErr bool
|
||||
}{
|
||||
// JWT signed with available key
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() []key.PublicKey {
|
||||
return []key.PublicKey{pk1}
|
||||
},
|
||||
},
|
||||
jwt: *jwtPK1,
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// JWT signed with available key, with bad claims
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() []key.PublicKey {
|
||||
return []key.PublicKey{pk1}
|
||||
},
|
||||
},
|
||||
jwt: *jwtPK1BadClaims,
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// expired JWT signed with available key
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() []key.PublicKey {
|
||||
return []key.PublicKey{pk1}
|
||||
},
|
||||
},
|
||||
jwt: *jwtExpired,
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// JWT signed with unrecognized key, verifiable after sync
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() func() []key.PublicKey {
|
||||
var i int
|
||||
return func() []key.PublicKey {
|
||||
defer func() { i++ }()
|
||||
return [][]key.PublicKey{
|
||||
[]key.PublicKey{pk1},
|
||||
[]key.PublicKey{pk2},
|
||||
}[i]
|
||||
}
|
||||
}(),
|
||||
},
|
||||
jwt: *jwtPK2,
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// JWT signed with unrecognized key, not verifiable after sync
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() []key.PublicKey {
|
||||
return []key.PublicKey{pk1}
|
||||
},
|
||||
},
|
||||
jwt: *jwtPK2,
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// verifier gets no keys from keysFunc, still not verifiable after sync
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() []key.PublicKey {
|
||||
return []key.PublicKey{}
|
||||
},
|
||||
},
|
||||
jwt: *jwtPK1,
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
// verifier gets no keys from keysFunc, verifiable after sync
|
||||
{
|
||||
verifier: JWTVerifier{
|
||||
issuer: "example.com",
|
||||
clientID: "XXX",
|
||||
syncFunc: func() error { return nil },
|
||||
keysFunc: func() func() []key.PublicKey {
|
||||
var i int
|
||||
return func() []key.PublicKey {
|
||||
defer func() { i++ }()
|
||||
return [][]key.PublicKey{
|
||||
[]key.PublicKey{},
|
||||
[]key.PublicKey{pk2},
|
||||
}[i]
|
||||
}
|
||||
}(),
|
||||
},
|
||||
jwt: *jwtPK2,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
err := tt.verifier.Verify(tt.jwt)
|
||||
if tt.wantErr && (err == nil) {
|
||||
t.Errorf("case %d: wanted non-nil error", i)
|
||||
} else if !tt.wantErr && (err != nil) {
|
||||
t.Errorf("case %d: wanted nil error, got %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# CoreOS Log
|
||||
|
||||
There are far too many logging packages out there, with varying degrees of licenses, far too many features (colorization, all sorts of log frameworks) or are just a pain to use (lack of `Fatalln()`?)
|
||||
|
||||
## Design Principles
|
||||
|
||||
* `package main` is the place where logging gets turned on and routed
|
||||
|
||||
A library should not touch log options, only generate log entries. Libraries are silent until main lets them speak.
|
||||
|
||||
* All log options are runtime-configurable.
|
||||
|
||||
Still the job of `main` to expose these configurations. `main` may delegate this to, say, a configuration webhook, but does so explicitly.
|
||||
|
||||
* There is one log object per package. It is registered under its repository and package name.
|
||||
|
||||
`main` activates logging for its repository and any dependency repositories it would also like to have output in its logstream. `main` also dictates at which level each subpackage logs.
|
||||
|
||||
* There is *one* output stream, and it is an `io.Writer` composed with a formatter.
|
||||
|
||||
Splitting streams is probably not the job of your program, but rather, your log aggregation framework. If you must split output streams, again, `main` configures this and you can write a very simple two-output struct that satisfies io.Writer.
|
||||
|
||||
Fancy colorful formatting and JSON output are beyond the scope of a basic logging framework -- they're application/log-collector dependant. These are, at best, provided as options, but more likely, provided by your application.
|
||||
|
||||
* Log objects are an interface
|
||||
|
||||
An object knows best how to print itself. Log objects can collect more interesting metadata if they wish, however, because text isn't going away anytime soon, they must all be marshalable to text. The simplest log object is a string, which returns itself. If you wish to do more fancy tricks for printing your log objects, see also JSON output -- introspect and write a formatter which can handle your advanced log interface. Making strings is the only thing guaranteed.
|
||||
|
||||
* Log levels have specific meanings:
|
||||
|
||||
* Critical: Unrecoverable. Must fail.
|
||||
* Error: Data has been lost, a request has failed for a bad reason, or a required resource has been lost
|
||||
* Warning: (Hopefully) Temporary conditions that may cause errors, but may work fine. A replica disappearing (that may reconnect) is a warning.
|
||||
* Notice: Normal, but important (uncommon) log information.
|
||||
* Info: Normal, working log information, everything is fine, but helpful notices for auditing or common operations.
|
||||
* Debug: Everything is still fine, but even common operations may be logged, and less helpful but more quantity of notices.
|
||||
* Trace: Anything goes, from logging every function call as part of a common operation, to tracing execution of a query.
|
||||
|
59
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/example/hello_dolly.go
generated
vendored
Normal file
59
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/example/hello_dolly.go
generated
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
oldlog "log"
|
||||
"os"
|
||||
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
)
|
||||
|
||||
var logLevel = capnslog.INFO
|
||||
var log = capnslog.NewPackageLogger("github.com/coreos/pkg/capnslog/cmd", "main")
|
||||
var dlog = capnslog.NewPackageLogger("github.com/coreos/pkg/capnslog/cmd", "dolly")
|
||||
|
||||
func init() {
|
||||
flag.Var(&logLevel, "log-level", "Global log level.")
|
||||
}
|
||||
|
||||
func main() {
|
||||
rl := capnslog.MustRepoLogger("github.com/coreos/pkg/capnslog/cmd")
|
||||
capnslog.SetFormatter(capnslog.NewStringFormatter(os.Stderr))
|
||||
|
||||
// We can parse the log level configs from the command line
|
||||
flag.Parse()
|
||||
if flag.NArg() > 1 {
|
||||
cfg, err := rl.ParseLogLevelConfig(flag.Arg(1))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rl.SetLogLevel(cfg)
|
||||
log.Infof("Setting output to %s", flag.Arg(1))
|
||||
}
|
||||
|
||||
// Send some messages at different levels to the different packages
|
||||
dlog.Infof("Hello Dolly")
|
||||
dlog.Warningf("Well hello, Dolly")
|
||||
log.Errorf("It's so nice to have you back where you belong")
|
||||
dlog.Debugf("You're looking swell, Dolly")
|
||||
dlog.Tracef("I can tell, Dolly")
|
||||
|
||||
// We also have control over the built-in "log" package.
|
||||
capnslog.SetGlobalLogLevel(logLevel)
|
||||
oldlog.Println("You're still glowin', you're still crowin', you're still lookin' strong")
|
||||
log.Fatalf("Dolly'll never go away again")
|
||||
}
|
63
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/formatters.go
generated
vendored
Normal file
63
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/formatters.go
generated
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package capnslog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Formatter interface {
|
||||
Format(pkg string, level LogLevel, depth int, entries ...interface{})
|
||||
Flush()
|
||||
}
|
||||
|
||||
func NewStringFormatter(w io.Writer) *StringFormatter {
|
||||
return &StringFormatter{
|
||||
w: bufio.NewWriter(w),
|
||||
}
|
||||
}
|
||||
|
||||
type StringFormatter struct {
|
||||
w *bufio.Writer
|
||||
}
|
||||
|
||||
func (s *StringFormatter) Format(pkg string, l LogLevel, i int, entries ...interface{}) {
|
||||
now := time.Now()
|
||||
y, m, d := now.Date()
|
||||
h, min, sec := now.Clock()
|
||||
s.w.WriteString(fmt.Sprintf("%d/%02d/%d %02d:%02d:%02d ", y, m, d, h, min, sec))
|
||||
s.writeEntries(pkg, l, i, entries...)
|
||||
}
|
||||
|
||||
func (s *StringFormatter) writeEntries(pkg string, _ LogLevel, _ int, entries ...interface{}) {
|
||||
if pkg != "" {
|
||||
s.w.WriteString(pkg + ": ")
|
||||
}
|
||||
str := fmt.Sprint(entries...)
|
||||
endsInNL := strings.HasSuffix(str, "\n")
|
||||
s.w.WriteString(str)
|
||||
if !endsInNL {
|
||||
s.w.WriteString("\n")
|
||||
}
|
||||
s.Flush()
|
||||
}
|
||||
|
||||
func (s *StringFormatter) Flush() {
|
||||
s.w.Flush()
|
||||
}
|
95
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/glog_formatter.go
generated
vendored
Normal file
95
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/glog_formatter.go
generated
vendored
Normal file
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package capnslog
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var pid = os.Getpid()
|
||||
|
||||
type GlogFormatter struct {
|
||||
StringFormatter
|
||||
}
|
||||
|
||||
func NewGlogFormatter(w io.Writer) *GlogFormatter {
|
||||
g := &GlogFormatter{}
|
||||
g.w = bufio.NewWriter(w)
|
||||
return g
|
||||
}
|
||||
|
||||
func (g GlogFormatter) Format(pkg string, level LogLevel, depth int, entries ...interface{}) {
|
||||
g.w.Write(GlogHeader(level, depth+1))
|
||||
g.StringFormatter.Format(pkg, level, depth+1, entries...)
|
||||
}
|
||||
|
||||
func GlogHeader(level LogLevel, depth int) []byte {
|
||||
// Lmmdd hh:mm:ss.uuuuuu threadid file:line]
|
||||
now := time.Now()
|
||||
_, file, line, ok := runtime.Caller(depth) // It's always the same number of frames to the user's call.
|
||||
if !ok {
|
||||
file = "???"
|
||||
line = 1
|
||||
} else {
|
||||
slash := strings.LastIndex(file, "/")
|
||||
if slash >= 0 {
|
||||
file = file[slash+1:]
|
||||
}
|
||||
}
|
||||
if line < 0 {
|
||||
line = 0 // not a real line number
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
buf.Grow(30)
|
||||
_, month, day := now.Date()
|
||||
hour, minute, second := now.Clock()
|
||||
buf.WriteString(level.Char())
|
||||
twoDigits(buf, int(month))
|
||||
twoDigits(buf, day)
|
||||
buf.WriteByte(' ')
|
||||
twoDigits(buf, hour)
|
||||
buf.WriteByte(':')
|
||||
twoDigits(buf, minute)
|
||||
buf.WriteByte(':')
|
||||
twoDigits(buf, second)
|
||||
buf.WriteByte('.')
|
||||
buf.WriteString(strconv.Itoa(now.Nanosecond() / 1000))
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString(strconv.Itoa(pid))
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString(file)
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString(strconv.Itoa(line))
|
||||
buf.WriteByte(']')
|
||||
buf.WriteByte(' ')
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
const digits = "0123456789"
|
||||
|
||||
func twoDigits(b *bytes.Buffer, d int) {
|
||||
c2 := digits[d%10]
|
||||
d /= 10
|
||||
c1 := digits[d%10]
|
||||
b.WriteByte(c1)
|
||||
b.WriteByte(c2)
|
||||
}
|
39
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/log_hijack.go
generated
vendored
Normal file
39
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/log_hijack.go
generated
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package capnslog
|
||||
|
||||
import (
|
||||
"log"
|
||||
)
|
||||
|
||||
func init() {
|
||||
pkg := NewPackageLogger("log", "")
|
||||
w := packageWriter{pkg}
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("")
|
||||
log.SetOutput(w)
|
||||
}
|
||||
|
||||
type packageWriter struct {
|
||||
pl *PackageLogger
|
||||
}
|
||||
|
||||
func (p packageWriter) Write(b []byte) (int, error) {
|
||||
if p.pl.level < INFO {
|
||||
return 0, nil
|
||||
}
|
||||
p.pl.internalLog(calldepth+2, INFO, string(b))
|
||||
return len(b), nil
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package capnslog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LogLevel is the set of all log levels.
|
||||
type LogLevel int8
|
||||
|
||||
const (
|
||||
// CRITICAL is the lowest log level; only errors which will end the program will be propagated.
|
||||
CRITICAL LogLevel = iota - 1
|
||||
// ERROR is for errors that are not fatal but lead to troubling behavior.
|
||||
ERROR
|
||||
// WARNING is for errors which are not fatal and not errors, but are unusual. Often sourced from misconfigurations.
|
||||
WARNING
|
||||
// NOTICE is for normal but significant conditions.
|
||||
NOTICE
|
||||
// INFO is a log level for common, everyday log updates.
|
||||
INFO
|
||||
// DEBUG is the default hidden level for more verbose updates about internal processes.
|
||||
DEBUG
|
||||
// TRACE is for (potentially) call by call tracing of programs.
|
||||
TRACE
|
||||
)
|
||||
|
||||
// Char returns a single-character representation of the log level.
|
||||
func (l LogLevel) Char() string {
|
||||
switch l {
|
||||
case CRITICAL:
|
||||
return "C"
|
||||
case ERROR:
|
||||
return "E"
|
||||
case WARNING:
|
||||
return "W"
|
||||
case NOTICE:
|
||||
return "N"
|
||||
case INFO:
|
||||
return "I"
|
||||
case DEBUG:
|
||||
return "D"
|
||||
case TRACE:
|
||||
return "T"
|
||||
default:
|
||||
panic("Unhandled loglevel")
|
||||
}
|
||||
}
|
||||
|
||||
// String returns a multi-character representation of the log level.
|
||||
func (l LogLevel) String() string {
|
||||
switch l {
|
||||
case CRITICAL:
|
||||
return "CRITICAL"
|
||||
case ERROR:
|
||||
return "ERROR"
|
||||
case WARNING:
|
||||
return "WARNING"
|
||||
case NOTICE:
|
||||
return "NOTICE"
|
||||
case INFO:
|
||||
return "INFO"
|
||||
case DEBUG:
|
||||
return "DEBUG"
|
||||
case TRACE:
|
||||
return "TRACE"
|
||||
default:
|
||||
panic("Unhandled loglevel")
|
||||
}
|
||||
}
|
||||
|
||||
// Update using the given string value. Fulfills the flag.Value interface.
|
||||
func (l *LogLevel) Set(s string) error {
|
||||
value, err := ParseLevel(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = value
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseLevel translates some potential loglevel strings into their corresponding levels.
|
||||
func ParseLevel(s string) (LogLevel, error) {
|
||||
switch s {
|
||||
case "CRITICAL", "C":
|
||||
return CRITICAL, nil
|
||||
case "ERROR", "0", "E":
|
||||
return ERROR, nil
|
||||
case "WARNING", "1", "W":
|
||||
return WARNING, nil
|
||||
case "NOTICE", "2", "N":
|
||||
return NOTICE, nil
|
||||
case "INFO", "3", "I":
|
||||
return INFO, nil
|
||||
case "DEBUG", "4", "D":
|
||||
return DEBUG, nil
|
||||
case "TRACE", "5", "T":
|
||||
return TRACE, nil
|
||||
}
|
||||
return CRITICAL, errors.New("couldn't parse log level " + s)
|
||||
}
|
||||
|
||||
type RepoLogger map[string]*PackageLogger
|
||||
|
||||
type loggerStruct struct {
|
||||
sync.Mutex
|
||||
repoMap map[string]RepoLogger
|
||||
formatter Formatter
|
||||
}
|
||||
|
||||
// logger is the global logger
|
||||
var logger = new(loggerStruct)
|
||||
|
||||
// SetGlobalLogLevel sets the log level for all packages in all repositories
|
||||
// registered with capnslog.
|
||||
func SetGlobalLogLevel(l LogLevel) {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
for _, r := range logger.repoMap {
|
||||
r.setRepoLogLevelInternal(l)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRepoLogger may return the handle to the repository's set of packages' loggers.
|
||||
func GetRepoLogger(repo string) (RepoLogger, error) {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
r, ok := logger.repoMap[repo]
|
||||
if !ok {
|
||||
return nil, errors.New("no packages registered for repo " + repo)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// MustRepoLogger returns the handle to the repository's packages' loggers.
|
||||
func MustRepoLogger(repo string) RepoLogger {
|
||||
r, err := GetRepoLogger(repo)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SetRepoLogLevel sets the log level for all packages in the repository.
|
||||
func (r RepoLogger) SetRepoLogLevel(l LogLevel) {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
r.setRepoLogLevelInternal(l)
|
||||
}
|
||||
|
||||
func (r RepoLogger) setRepoLogLevelInternal(l LogLevel) {
|
||||
for _, v := range r {
|
||||
v.level = l
|
||||
}
|
||||
}
|
||||
|
||||
// ParseLogLevelConfig parses a comma-separated string of "package=loglevel", in
|
||||
// order, and returns a map of the results, for use in SetLogLevel.
|
||||
func (r RepoLogger) ParseLogLevelConfig(conf string) (map[string]LogLevel, error) {
|
||||
setlist := strings.Split(conf, ",")
|
||||
out := make(map[string]LogLevel)
|
||||
for _, setstring := range setlist {
|
||||
setting := strings.Split(setstring, "=")
|
||||
if len(setting) != 2 {
|
||||
return nil, errors.New("oddly structured `pkg=level` option: " + setstring)
|
||||
}
|
||||
l, err := ParseLevel(setting[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[setting[0]] = l
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SetLogLevel takes a map of package names within a repository to their desired
|
||||
// loglevel, and sets the levels appropriately. Unknown packages are ignored.
|
||||
// "*" is a special package name that corresponds to all packages, and will be
|
||||
// processed first.
|
||||
func (r RepoLogger) SetLogLevel(m map[string]LogLevel) {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
if l, ok := m["*"]; ok {
|
||||
r.setRepoLogLevelInternal(l)
|
||||
}
|
||||
for k, v := range m {
|
||||
l, ok := r[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
l.level = v
|
||||
}
|
||||
}
|
||||
|
||||
// SetFormatter sets the formatting function for all logs.
|
||||
func SetFormatter(f Formatter) {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
logger.formatter = f
|
||||
}
|
||||
|
||||
// NewPackageLogger creates a package logger object.
|
||||
// This should be defined as a global var in your package, referencing your repo.
|
||||
func NewPackageLogger(repo string, pkg string) (p *PackageLogger) {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
if logger.repoMap == nil {
|
||||
logger.repoMap = make(map[string]RepoLogger)
|
||||
}
|
||||
r, rok := logger.repoMap[repo]
|
||||
if !rok {
|
||||
logger.repoMap[repo] = make(RepoLogger)
|
||||
r = logger.repoMap[repo]
|
||||
}
|
||||
p, pok := r[pkg]
|
||||
if !pok {
|
||||
r[pkg] = &PackageLogger{
|
||||
pkg: pkg,
|
||||
level: INFO,
|
||||
}
|
||||
p = r[pkg]
|
||||
}
|
||||
return
|
||||
}
|
158
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/pkg_logger.go
generated
vendored
Normal file
158
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/pkg_logger.go
generated
vendored
Normal file
|
@ -0,0 +1,158 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package capnslog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type PackageLogger struct {
|
||||
pkg string
|
||||
level LogLevel
|
||||
}
|
||||
|
||||
const calldepth = 3
|
||||
|
||||
func (p *PackageLogger) internalLog(depth int, inLevel LogLevel, entries ...interface{}) {
|
||||
if inLevel != CRITICAL && p.level < inLevel {
|
||||
return
|
||||
}
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
if logger.formatter != nil {
|
||||
logger.formatter.Format(p.pkg, inLevel, depth+1, entries...)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PackageLogger) LevelAt(l LogLevel) bool {
|
||||
return p.level >= l
|
||||
}
|
||||
|
||||
// Log a formatted string at any level between ERROR and TRACE
|
||||
func (p *PackageLogger) Logf(l LogLevel, format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, l, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// Log a message at any level between ERROR and TRACE
|
||||
func (p *PackageLogger) Log(l LogLevel, args ...interface{}) {
|
||||
p.internalLog(calldepth, l, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// log stdlib compatibility
|
||||
|
||||
func (p *PackageLogger) Println(args ...interface{}) {
|
||||
p.internalLog(calldepth, INFO, fmt.Sprintln(args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Printf(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, INFO, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Print(args ...interface{}) {
|
||||
p.internalLog(calldepth, INFO, fmt.Sprint(args...))
|
||||
}
|
||||
|
||||
// Panic and fatal
|
||||
|
||||
func (p *PackageLogger) Panicf(format string, args ...interface{}) {
|
||||
s := fmt.Sprintf(format, args...)
|
||||
p.internalLog(calldepth, CRITICAL, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Panic(args ...interface{}) {
|
||||
s := fmt.Sprint(args...)
|
||||
p.internalLog(calldepth, CRITICAL, s)
|
||||
panic(s)
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Fatalf(format string, args ...interface{}) {
|
||||
s := fmt.Sprintf(format, args...)
|
||||
p.internalLog(calldepth, CRITICAL, s)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Fatal(args ...interface{}) {
|
||||
s := fmt.Sprint(args...)
|
||||
p.internalLog(calldepth, CRITICAL, s)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Error Functions
|
||||
|
||||
func (p *PackageLogger) Errorf(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, ERROR, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Error(entries ...interface{}) {
|
||||
p.internalLog(calldepth, ERROR, entries...)
|
||||
}
|
||||
|
||||
// Warning Functions
|
||||
|
||||
func (p *PackageLogger) Warningf(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, WARNING, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Warning(entries ...interface{}) {
|
||||
p.internalLog(calldepth, WARNING, entries...)
|
||||
}
|
||||
|
||||
// Notice Functions
|
||||
|
||||
func (p *PackageLogger) Noticef(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, NOTICE, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Notice(entries ...interface{}) {
|
||||
p.internalLog(calldepth, NOTICE, entries...)
|
||||
}
|
||||
|
||||
// Info Functions
|
||||
|
||||
func (p *PackageLogger) Infof(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, INFO, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Info(entries ...interface{}) {
|
||||
p.internalLog(calldepth, INFO, entries...)
|
||||
}
|
||||
|
||||
// Debug Functions
|
||||
|
||||
func (p *PackageLogger) Debugf(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, DEBUG, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Debug(entries ...interface{}) {
|
||||
p.internalLog(calldepth, DEBUG, entries...)
|
||||
}
|
||||
|
||||
// Trace Functions
|
||||
|
||||
func (p *PackageLogger) Tracef(format string, args ...interface{}) {
|
||||
p.internalLog(calldepth, TRACE, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Trace(entries ...interface{}) {
|
||||
p.internalLog(calldepth, TRACE, entries...)
|
||||
}
|
||||
|
||||
func (p *PackageLogger) Flush() {
|
||||
logger.Lock()
|
||||
defer logger.Unlock()
|
||||
logger.formatter.Flush()
|
||||
}
|
65
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/syslog_formatter.go
generated
vendored
Normal file
65
Godeps/_workspace/src/github.com/coreos/pkg/capnslog/syslog_formatter.go
generated
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2015 CoreOS, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// +build !windows
|
||||
|
||||
package capnslog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/syslog"
|
||||
)
|
||||
|
||||
func NewSyslogFormatter(w *syslog.Writer) Formatter {
|
||||
return &syslogFormatter{w}
|
||||
}
|
||||
|
||||
func NewDefaultSyslogFormatter(tag string) (Formatter, error) {
|
||||
w, err := syslog.New(syslog.LOG_DEBUG, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewSyslogFormatter(w), nil
|
||||
}
|
||||
|
||||
type syslogFormatter struct {
|
||||
w *syslog.Writer
|
||||
}
|
||||
|
||||
func (s *syslogFormatter) Format(pkg string, l LogLevel, _ int, entries ...interface{}) {
|
||||
for _, entry := range entries {
|
||||
str := fmt.Sprint(entry)
|
||||
switch l {
|
||||
case CRITICAL:
|
||||
s.w.Crit(str)
|
||||
case ERROR:
|
||||
s.w.Err(str)
|
||||
case WARNING:
|
||||
s.w.Warning(str)
|
||||
case NOTICE:
|
||||
s.w.Notice(str)
|
||||
case INFO:
|
||||
s.w.Info(str)
|
||||
case DEBUG:
|
||||
s.w.Debug(str)
|
||||
case TRACE:
|
||||
s.w.Debug(str)
|
||||
default:
|
||||
panic("Unhandled loglevel")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *syslogFormatter) Flush() {
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
health
|
||||
====
|
||||
|
||||
A simple framework for implementing an HTTP health check endpoint on servers.
|
||||
|
||||
Users implement their `health.Checkable` types, and create a `health.Checker`, from which they can get an `http.HandlerFunc` using `health.Checker.MakeHealthHandlerFunc`.
|
||||
|
||||
### Documentation
|
||||
|
||||
For more details, visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/health)
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/pkg/httputil"
|
||||
)
|
||||
|
||||
// Checkables should return nil when the thing they are checking is healthy, and an error otherwise.
|
||||
type Checkable interface {
|
||||
Healthy() error
|
||||
}
|
||||
|
||||
// Checker provides a way to make an endpoint which can be probed for system health.
|
||||
type Checker struct {
|
||||
// Checks are the Checkables to be checked when probing.
|
||||
Checks []Checkable
|
||||
|
||||
// Unhealthyhandler is called when one or more of the checks are unhealthy.
|
||||
// If not provided DefaultUnhealthyHandler is called.
|
||||
UnhealthyHandler UnhealthyHandler
|
||||
|
||||
// HealthyHandler is called when all checks are healthy.
|
||||
// If not provided, DefaultHealthyHandler is called.
|
||||
HealthyHandler http.HandlerFunc
|
||||
}
|
||||
|
||||
func (c Checker) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
unhealthyHandler := c.UnhealthyHandler
|
||||
if unhealthyHandler == nil {
|
||||
unhealthyHandler = DefaultUnhealthyHandler
|
||||
}
|
||||
|
||||
successHandler := c.HealthyHandler
|
||||
if successHandler == nil {
|
||||
successHandler = DefaultHealthyHandler
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
w.Header().Set("Allow", "GET")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Check(c.Checks); err != nil {
|
||||
unhealthyHandler(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
successHandler(w, r)
|
||||
}
|
||||
|
||||
type UnhealthyHandler func(w http.ResponseWriter, r *http.Request, err error)
|
||||
|
||||
type StatusResponse struct {
|
||||
Status string `json:"status"`
|
||||
Details *StatusResponseDetails `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type StatusResponseDetails struct {
|
||||
Code int `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func Check(checks []Checkable) (err error) {
|
||||
errs := []error{}
|
||||
for _, c := range checks {
|
||||
if e := c.Healthy(); e != nil {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
|
||||
switch len(errs) {
|
||||
case 0:
|
||||
err = nil
|
||||
case 1:
|
||||
err = errs[0]
|
||||
default:
|
||||
err = fmt.Errorf("multiple health check failure: %v", errs)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func DefaultHealthyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := httputil.WriteJSONResponse(w, http.StatusOK, StatusResponse{
|
||||
Status: "ok",
|
||||
})
|
||||
if err != nil {
|
||||
// TODO(bobbyrullo): replace with logging from new logging pkg,
|
||||
// once it lands.
|
||||
log.Printf("Failed to write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultUnhealthyHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||
writeErr := httputil.WriteJSONResponse(w, http.StatusInternalServerError, StatusResponse{
|
||||
Status: "error",
|
||||
Details: &StatusResponseDetails{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: err.Error(),
|
||||
},
|
||||
})
|
||||
if writeErr != nil {
|
||||
// TODO(bobbyrullo): replace with logging from new logging pkg,
|
||||
// once it lands.
|
||||
log.Printf("Failed to write JSON response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ExpvarHandler is copied from https://golang.org/src/expvar/expvar.go, where it's sadly unexported.
|
||||
func ExpvarHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
fmt.Fprintf(w, "{\n")
|
||||
first := true
|
||||
expvar.Do(func(kv expvar.KeyValue) {
|
||||
if !first {
|
||||
fmt.Fprintf(w, ",\n")
|
||||
}
|
||||
first = false
|
||||
fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
|
||||
})
|
||||
fmt.Fprintf(w, "\n}\n")
|
||||
}
|
198
Godeps/_workspace/src/github.com/coreos/pkg/health/health_test.go
generated
vendored
Normal file
198
Godeps/_workspace/src/github.com/coreos/pkg/health/health_test.go
generated
vendored
Normal file
|
@ -0,0 +1,198 @@
|
|||
package health
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/pkg/httputil"
|
||||
)
|
||||
|
||||
type boolChecker bool
|
||||
|
||||
func (b boolChecker) Healthy() error {
|
||||
if b {
|
||||
return nil
|
||||
}
|
||||
return errors.New("Unhealthy")
|
||||
}
|
||||
|
||||
func errString(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
checks []Checkable
|
||||
expected string
|
||||
}{
|
||||
{[]Checkable{}, ""},
|
||||
|
||||
{[]Checkable{boolChecker(true)}, ""},
|
||||
|
||||
{[]Checkable{boolChecker(true), boolChecker(true)}, ""},
|
||||
|
||||
{[]Checkable{boolChecker(true), boolChecker(false)}, "Unhealthy"},
|
||||
|
||||
{[]Checkable{boolChecker(true), boolChecker(false), boolChecker(false)}, "multiple health check failure: [Unhealthy Unhealthy]"},
|
||||
} {
|
||||
err := Check(test.checks)
|
||||
|
||||
if errString(err) != test.expected {
|
||||
t.Errorf("case %d: want %v, got %v", i, test.expected, errString(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlerFunc(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
checker Checker
|
||||
method string
|
||||
expectedStatus string
|
||||
expectedCode int
|
||||
expectedMessage string
|
||||
}{
|
||||
{
|
||||
Checker{
|
||||
Checks: []Checkable{
|
||||
boolChecker(true),
|
||||
},
|
||||
},
|
||||
"GET",
|
||||
"ok",
|
||||
http.StatusOK,
|
||||
"",
|
||||
},
|
||||
|
||||
// Wrong method.
|
||||
{
|
||||
Checker{
|
||||
Checks: []Checkable{
|
||||
boolChecker(true),
|
||||
},
|
||||
},
|
||||
"POST",
|
||||
"",
|
||||
http.StatusMethodNotAllowed,
|
||||
"GET only acceptable method",
|
||||
},
|
||||
|
||||
// Health check fails.
|
||||
{
|
||||
Checker{
|
||||
Checks: []Checkable{
|
||||
boolChecker(false),
|
||||
},
|
||||
},
|
||||
"GET",
|
||||
"error",
|
||||
http.StatusInternalServerError,
|
||||
"Unhealthy",
|
||||
},
|
||||
|
||||
// Health check fails, with overridden ErrorHandler.
|
||||
{
|
||||
Checker{
|
||||
Checks: []Checkable{
|
||||
boolChecker(false),
|
||||
},
|
||||
UnhealthyHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
httputil.WriteJSONResponse(w,
|
||||
http.StatusInternalServerError, StatusResponse{
|
||||
Status: "error",
|
||||
Details: &StatusResponseDetails{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "Override!",
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
"GET",
|
||||
"error",
|
||||
http.StatusInternalServerError,
|
||||
"Override!",
|
||||
},
|
||||
|
||||
// Health check succeeds, with overridden SuccessHandler.
|
||||
{
|
||||
Checker{
|
||||
Checks: []Checkable{
|
||||
boolChecker(true),
|
||||
},
|
||||
HealthyHandler: func(w http.ResponseWriter, r *http.Request) {
|
||||
httputil.WriteJSONResponse(w,
|
||||
http.StatusOK, StatusResponse{
|
||||
Status: "okey-dokey",
|
||||
})
|
||||
},
|
||||
},
|
||||
"GET",
|
||||
"okey-dokey",
|
||||
http.StatusOK,
|
||||
"",
|
||||
},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
r := &http.Request{}
|
||||
r.Method = test.method
|
||||
test.checker.ServeHTTP(w, r)
|
||||
if w.Code != test.expectedCode {
|
||||
t.Errorf("case %d: w.code == %v, want %v", i, w.Code, test.expectedCode)
|
||||
}
|
||||
|
||||
if test.expectedStatus == "" {
|
||||
// This is to handle the wrong-method case, when the
|
||||
// body of the response is empty.
|
||||
continue
|
||||
}
|
||||
|
||||
statusMap := make(map[string]interface{})
|
||||
err := json.Unmarshal(w.Body.Bytes(), &statusMap)
|
||||
if err != nil {
|
||||
t.Fatalf("case %d: failed to Unmarshal response body: %v", i, err)
|
||||
}
|
||||
|
||||
status, ok := statusMap["status"].(string)
|
||||
if !ok {
|
||||
t.Errorf("case %d: status not present or not a string in json: %q", i, w.Body.Bytes())
|
||||
}
|
||||
if status != test.expectedStatus {
|
||||
t.Errorf("case %d: status == %v, want %v", i, status, test.expectedStatus)
|
||||
}
|
||||
|
||||
detailMap, ok := statusMap["details"].(map[string]interface{})
|
||||
if test.expectedMessage != "" {
|
||||
if !ok {
|
||||
t.Fatalf("case %d: could not find/unmarshal detailMap", i)
|
||||
}
|
||||
message, ok := detailMap["message"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("case %d: message not present or not a string in json: %q",
|
||||
i, w.Body.Bytes())
|
||||
}
|
||||
if message != test.expectedMessage {
|
||||
t.Errorf("case %d: message == %v, want %v", i, message, test.expectedMessage)
|
||||
}
|
||||
|
||||
code, ok := detailMap["code"].(float64)
|
||||
if !ok {
|
||||
t.Fatalf("case %d: code not present or not an int in json: %q",
|
||||
i, w.Body.Bytes())
|
||||
}
|
||||
if int(code) != test.expectedCode {
|
||||
t.Errorf("case %d: code == %v, want %v", i, code, test.expectedCode)
|
||||
}
|
||||
|
||||
} else {
|
||||
if ok {
|
||||
t.Errorf("case %d: unwanted detailMap present: %q", i, detailMap)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
httputil
|
||||
====
|
||||
|
||||
Common code for dealing with HTTP.
|
||||
|
||||
Includes:
|
||||
|
||||
* Code for returning JSON responses.
|
||||
|
||||
### Documentation
|
||||
|
||||
Visit the docs on [gopkgdoc](http://godoc.org/github.com/coreos/pkg/httputil)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
JSONContentType = "application/json"
|
||||
)
|
||||
|
||||
func WriteJSONResponse(w http.ResponseWriter, code int, resp interface{}) error {
|
||||
enc, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", JSONContentType)
|
||||
w.WriteHeader(code)
|
||||
|
||||
_, err = w.Write(enc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteJSONResponse(t *testing.T) {
|
||||
for i, test := range []struct {
|
||||
code int
|
||||
resp interface{}
|
||||
expectedJSON string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
200,
|
||||
struct {
|
||||
A string
|
||||
B string
|
||||
}{A: "foo", B: "bar"},
|
||||
`{"A":"foo","B":"bar"}`,
|
||||
false,
|
||||
},
|
||||
{
|
||||
500,
|
||||
// Something that json.Marshal cannot serialize.
|
||||
make(chan int),
|
||||
"",
|
||||
true,
|
||||
},
|
||||
} {
|
||||
w := httptest.NewRecorder()
|
||||
err := WriteJSONResponse(w, test.code, test.resp)
|
||||
|
||||
if w.Code != test.code {
|
||||
t.Errorf("case %d: w.code == %v, want %v", i, w.Code, test.code)
|
||||
}
|
||||
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("case %d: (err != nil) == %v, want %v. err: %v", i, err != nil, test.expectErr, err)
|
||||
}
|
||||
|
||||
if string(w.Body.Bytes()) != test.expectedJSON {
|
||||
t.Errorf("case %d: w.Body.Bytes()) == %q, want %q", i,
|
||||
string(w.Body.Bytes()), test.expectedJSON)
|
||||
}
|
||||
|
||||
if !test.expectErr {
|
||||
contentType := w.Header()["Content-Type"][0]
|
||||
if contentType != JSONContentType {
|
||||
t.Errorf("case %d: contentType == %v, want %v", i, contentType, JSONContentType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package timeutil
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func ExpBackoff(prev, max time.Duration) time.Duration {
|
||||
if prev == 0 {
|
||||
return time.Second
|
||||
}
|
||||
if prev > max/2 {
|
||||
return max
|
||||
}
|
||||
return 2 * prev
|
||||
}
|
52
Godeps/_workspace/src/github.com/coreos/pkg/timeutil/backoff_test.go
generated
vendored
Normal file
52
Godeps/_workspace/src/github.com/coreos/pkg/timeutil/backoff_test.go
generated
vendored
Normal file
|
@ -0,0 +1,52 @@
|
|||
package timeutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExpBackoff(t *testing.T) {
|
||||
tests := []struct {
|
||||
prev time.Duration
|
||||
max time.Duration
|
||||
want time.Duration
|
||||
}{
|
||||
{
|
||||
prev: time.Duration(0),
|
||||
max: time.Minute,
|
||||
want: time.Second,
|
||||
},
|
||||
{
|
||||
prev: time.Second,
|
||||
max: time.Minute,
|
||||
want: 2 * time.Second,
|
||||
},
|
||||
{
|
||||
prev: 16 * time.Second,
|
||||
max: time.Minute,
|
||||
want: 32 * time.Second,
|
||||
},
|
||||
{
|
||||
prev: 32 * time.Second,
|
||||
max: time.Minute,
|
||||
want: time.Minute,
|
||||
},
|
||||
{
|
||||
prev: time.Minute,
|
||||
max: time.Minute,
|
||||
want: time.Minute,
|
||||
},
|
||||
{
|
||||
prev: 2 * time.Minute,
|
||||
max: time.Minute,
|
||||
want: time.Minute,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
got := ExpBackoff(tt.prev, tt.max)
|
||||
if tt.want != got {
|
||||
t.Errorf("case %d: want=%v got=%v", i, tt.want, got)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
|
||||
*.swp
|
|
@ -0,0 +1,3 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.3
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,61 @@
|
|||
clockwork
|
||||
=========
|
||||
|
||||
[![Build Status](https://travis-ci.org/jonboulle/clockwork.png?branch=master)](https://travis-ci.org/jonboulle/clockwork)
|
||||
[![godoc](https://godoc.org/github.com/jonboulle/clockwork?status.svg)](http://godoc.org/github.com/jonboulle/clockwork)
|
||||
|
||||
a simple fake clock for golang
|
||||
|
||||
# Usage
|
||||
|
||||
Replace uses of the `time` package with the `clockwork.Clock` interface instead.
|
||||
|
||||
For example, instead of using `time.Sleep` directly:
|
||||
|
||||
```
|
||||
func my_func() {
|
||||
time.Sleep(3 * time.Second)
|
||||
do_something()
|
||||
}
|
||||
```
|
||||
|
||||
inject a clock and use its `Sleep` method instead:
|
||||
|
||||
```
|
||||
func my_func(clock clockwork.Clock) {
|
||||
clock.Sleep(3 * time.Second)
|
||||
do_something()
|
||||
}
|
||||
```
|
||||
|
||||
Now you can easily test `my_func` with a `FakeClock`:
|
||||
|
||||
```
|
||||
func TestMyFunc(t *testing.T) {
|
||||
c := clockwork.NewFakeClock()
|
||||
|
||||
// Start our sleepy function
|
||||
my_func(c)
|
||||
|
||||
// Ensure we wait until my_func is sleeping
|
||||
c.BlockUntil(1)
|
||||
|
||||
assert_state()
|
||||
|
||||
// Advance the FakeClock forward in time
|
||||
c.Advance(3)
|
||||
|
||||
assert_state()
|
||||
}
|
||||
```
|
||||
|
||||
and in production builds, simply inject the real clock instead:
|
||||
```
|
||||
my_func(clockwork.NewRealClock())
|
||||
```
|
||||
|
||||
See [example_test.go](example_test.go) for a full example.
|
||||
|
||||
# Credits
|
||||
|
||||
clockwork is inspired by @wickman's [threaded fake clock](https://gist.github.com/wickman/3840816), and the [Golang playground](http://blog.golang.org/playground#Faking time)
|
164
Godeps/_workspace/src/github.com/jonboulle/clockwork/clockwork.go
generated
vendored
Normal file
164
Godeps/_workspace/src/github.com/jonboulle/clockwork/clockwork.go
generated
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
package clockwork
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock provides an interface that packages can use instead of directly
|
||||
// using the time module, so that chronology-related behavior can be tested
|
||||
type Clock interface {
|
||||
After(d time.Duration) <-chan time.Time
|
||||
Sleep(d time.Duration)
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// FakeClock provides an interface for a clock which can be
|
||||
// manually advanced through time
|
||||
type FakeClock interface {
|
||||
Clock
|
||||
// Advance advances the FakeClock to a new point in time, ensuring any existing
|
||||
// sleepers are notified appropriately before returning
|
||||
Advance(d time.Duration)
|
||||
// BlockUntil will block until the FakeClock has the given number of
|
||||
// sleepers (callers of Sleep or After)
|
||||
BlockUntil(n int)
|
||||
}
|
||||
|
||||
// NewRealClock returns a Clock which simply delegates calls to the actual time
|
||||
// package; it should be used by packages in production.
|
||||
func NewRealClock() Clock {
|
||||
return &realClock{}
|
||||
}
|
||||
|
||||
// NewFakeClock returns a FakeClock implementation which can be
|
||||
// manually advanced through time for testing.
|
||||
func NewFakeClock() FakeClock {
|
||||
return &fakeClock{
|
||||
l: sync.RWMutex{},
|
||||
|
||||
// use a fixture that does not fulfill Time.IsZero()
|
||||
time: time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
|
||||
type realClock struct{}
|
||||
|
||||
func (rc *realClock) After(d time.Duration) <-chan time.Time {
|
||||
return time.After(d)
|
||||
}
|
||||
|
||||
func (rc *realClock) Sleep(d time.Duration) {
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
func (rc *realClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
type fakeClock struct {
|
||||
sleepers []*sleeper
|
||||
blockers []*blocker
|
||||
time time.Time
|
||||
|
||||
l sync.RWMutex
|
||||
}
|
||||
|
||||
// sleeper represents a caller of After or Sleep
|
||||
type sleeper struct {
|
||||
until time.Time
|
||||
done chan time.Time
|
||||
}
|
||||
|
||||
// blocker represents a caller of BlockUntil
|
||||
type blocker struct {
|
||||
count int
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
// After mimics time.After; it waits for the given duration to elapse on the
|
||||
// fakeClock, then sends the current time on the returned channel.
|
||||
func (fc *fakeClock) After(d time.Duration) <-chan time.Time {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
now := fc.time
|
||||
done := make(chan time.Time, 1)
|
||||
if d.Nanoseconds() == 0 {
|
||||
// special case - trigger immediately
|
||||
done <- now
|
||||
} else {
|
||||
// otherwise, add to the set of sleepers
|
||||
s := &sleeper{
|
||||
until: now.Add(d),
|
||||
done: done,
|
||||
}
|
||||
fc.sleepers = append(fc.sleepers, s)
|
||||
// and notify any blockers
|
||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||
}
|
||||
return done
|
||||
}
|
||||
|
||||
// notifyBlockers notifies all the blockers waiting until the
|
||||
// given number of sleepers are waiting on the fakeClock. It
|
||||
// returns an updated slice of blockers (i.e. those still waiting)
|
||||
func notifyBlockers(blockers []*blocker, count int) (newBlockers []*blocker) {
|
||||
for _, b := range blockers {
|
||||
if b.count == count {
|
||||
close(b.ch)
|
||||
} else {
|
||||
newBlockers = append(newBlockers, b)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Sleep blocks until the given duration has passed on the fakeClock
|
||||
func (fc *fakeClock) Sleep(d time.Duration) {
|
||||
<-fc.After(d)
|
||||
}
|
||||
|
||||
// Time returns the current time of the fakeClock
|
||||
func (fc *fakeClock) Now() time.Time {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
return fc.time
|
||||
}
|
||||
|
||||
// Advance advances fakeClock to a new point in time, ensuring channels from any
|
||||
// previous invocations of After are notified appropriately before returning
|
||||
func (fc *fakeClock) Advance(d time.Duration) {
|
||||
fc.l.Lock()
|
||||
defer fc.l.Unlock()
|
||||
end := fc.time.Add(d)
|
||||
var newSleepers []*sleeper
|
||||
for _, s := range fc.sleepers {
|
||||
if end.Sub(s.until) >= 0 {
|
||||
s.done <- end
|
||||
} else {
|
||||
newSleepers = append(newSleepers, s)
|
||||
}
|
||||
}
|
||||
fc.sleepers = newSleepers
|
||||
fc.blockers = notifyBlockers(fc.blockers, len(fc.sleepers))
|
||||
fc.time = end
|
||||
}
|
||||
|
||||
// BlockUntil will block until the fakeClock has the given number of sleepers
|
||||
// (callers of Sleep or After)
|
||||
func (fc *fakeClock) BlockUntil(n int) {
|
||||
fc.l.Lock()
|
||||
// Fast path: current number of sleepers is what we're looking for
|
||||
if len(fc.sleepers) == n {
|
||||
fc.l.Unlock()
|
||||
return
|
||||
}
|
||||
// Otherwise, set up a new blocker
|
||||
b := &blocker{
|
||||
count: n,
|
||||
ch: make(chan struct{}),
|
||||
}
|
||||
fc.blockers = append(fc.blockers, b)
|
||||
fc.l.Unlock()
|
||||
<-b.ch
|
||||
}
|
120
Godeps/_workspace/src/github.com/jonboulle/clockwork/clockwork_test.go
generated
vendored
Normal file
120
Godeps/_workspace/src/github.com/jonboulle/clockwork/clockwork_test.go
generated
vendored
Normal file
|
@ -0,0 +1,120 @@
|
|||
package clockwork
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFakeClockAfter(t *testing.T) {
|
||||
fc := &fakeClock{}
|
||||
|
||||
zero := fc.After(0)
|
||||
select {
|
||||
case <-zero:
|
||||
default:
|
||||
t.Errorf("zero did not return!")
|
||||
}
|
||||
one := fc.After(1)
|
||||
two := fc.After(2)
|
||||
six := fc.After(6)
|
||||
ten := fc.After(10)
|
||||
fc.Advance(1)
|
||||
select {
|
||||
case <-one:
|
||||
default:
|
||||
t.Errorf("one did not return!")
|
||||
}
|
||||
select {
|
||||
case <-two:
|
||||
t.Errorf("two returned prematurely!")
|
||||
case <-six:
|
||||
t.Errorf("six returned prematurely!")
|
||||
case <-ten:
|
||||
t.Errorf("ten returned prematurely!")
|
||||
default:
|
||||
}
|
||||
fc.Advance(1)
|
||||
select {
|
||||
case <-two:
|
||||
default:
|
||||
t.Errorf("two did not return!")
|
||||
}
|
||||
select {
|
||||
case <-six:
|
||||
t.Errorf("six returned prematurely!")
|
||||
case <-ten:
|
||||
t.Errorf("ten returned prematurely!")
|
||||
default:
|
||||
}
|
||||
fc.Advance(1)
|
||||
select {
|
||||
case <-six:
|
||||
t.Errorf("six returned prematurely!")
|
||||
case <-ten:
|
||||
t.Errorf("ten returned prematurely!")
|
||||
default:
|
||||
}
|
||||
fc.Advance(3)
|
||||
select {
|
||||
case <-six:
|
||||
default:
|
||||
t.Errorf("six did not return!")
|
||||
}
|
||||
select {
|
||||
case <-ten:
|
||||
t.Errorf("ten returned prematurely!")
|
||||
default:
|
||||
}
|
||||
fc.Advance(100)
|
||||
select {
|
||||
case <-ten:
|
||||
default:
|
||||
t.Errorf("ten did not return!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyBlockers(t *testing.T) {
|
||||
b1 := &blocker{1, make(chan struct{})}
|
||||
b2 := &blocker{2, make(chan struct{})}
|
||||
b3 := &blocker{5, make(chan struct{})}
|
||||
b4 := &blocker{10, make(chan struct{})}
|
||||
b5 := &blocker{10, make(chan struct{})}
|
||||
bs := []*blocker{b1, b2, b3, b4, b5}
|
||||
bs1 := notifyBlockers(bs, 2)
|
||||
if n := len(bs1); n != 4 {
|
||||
t.Fatalf("got %d blockers, want %d", n, 4)
|
||||
}
|
||||
select {
|
||||
case <-b2.ch:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("timed out waiting for channel close!")
|
||||
}
|
||||
bs2 := notifyBlockers(bs1, 10)
|
||||
if n := len(bs2); n != 2 {
|
||||
t.Fatalf("got %d blockers, want %d", n, 2)
|
||||
}
|
||||
select {
|
||||
case <-b4.ch:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("timed out waiting for channel close!")
|
||||
}
|
||||
select {
|
||||
case <-b5.ch:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("timed out waiting for channel close!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFakeClock(t *testing.T) {
|
||||
fc := NewFakeClock()
|
||||
now := fc.Now()
|
||||
if now.IsZero() {
|
||||
t.Fatalf("fakeClock.Now() fulfills IsZero")
|
||||
}
|
||||
|
||||
now2 := fc.Now()
|
||||
if !reflect.DeepEqual(now, now2) {
|
||||
t.Fatalf("fakeClock.Now() returned different value: want=%#v got=%#v", now, now2)
|
||||
}
|
||||
}
|
49
Godeps/_workspace/src/github.com/jonboulle/clockwork/example_test.go
generated
vendored
Normal file
49
Godeps/_workspace/src/github.com/jonboulle/clockwork/example_test.go
generated
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
package clockwork
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// my_func is an example of a time-dependent function, using an
|
||||
// injected clock
|
||||
func my_func(clock Clock, i *int) {
|
||||
clock.Sleep(3 * time.Second)
|
||||
*i += 1
|
||||
}
|
||||
|
||||
// assert_state is an example of a state assertion in a test
|
||||
func assert_state(t *testing.T, i, j int) {
|
||||
if i != j {
|
||||
t.Fatalf("i %d, j %d", i, j)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMyFunc tests my_func's behaviour with a FakeClock
|
||||
func TestMyFunc(t *testing.T) {
|
||||
var i int
|
||||
c := NewFakeClock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
my_func(c, &i)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
// Wait until my_func is actually sleeping on the clock
|
||||
c.BlockUntil(1)
|
||||
|
||||
// Assert the initial state
|
||||
assert_state(t, i, 0)
|
||||
|
||||
// Now advance the clock forward in time
|
||||
c.Advance(1 * time.Hour)
|
||||
|
||||
// Wait until the function completes
|
||||
wg.Wait()
|
||||
|
||||
// Assert the final state
|
||||
assert_state(t, i, 1)
|
||||
}
|
Loading…
Reference in New Issue