Merge remote-tracking branch 'origin/master' into feature/auth-router
commit
e16a3cca6d
|
@ -8,6 +8,8 @@
|
|||
### Features
|
||||
1. [#1232](https://github.com/influxdata/chronograf/pull/1232): Fuse the query builder and raw query editor
|
||||
1. [#1265](https://github.com/influxdata/chronograf/pull/1265): Refactor router to use auth and force /login route when auth expired
|
||||
1. [#1286](https://github.com/influxdata/chronograf/pull/1286): Add refreshing JWTs for authentication
|
||||
|
||||
|
||||
### UI Improvements
|
||||
1. [#1259](https://github.com/influxdata/chronograf/pull/1259): Add default display for empty dashboard
|
||||
|
|
|
@ -9,27 +9,31 @@ import (
|
|||
const (
|
||||
// DefaultCookieName is the name of the stored cookie
|
||||
DefaultCookieName = "session"
|
||||
// DefaultInactivityDuration is the duration a token is valid without any new activity
|
||||
DefaultInactivityDuration = 5 * time.Minute
|
||||
)
|
||||
|
||||
var _ Authenticator = &cookie{}
|
||||
|
||||
// cookie represents the location and expiration time of new cookies.
|
||||
type cookie struct {
|
||||
Name string
|
||||
Duration time.Duration
|
||||
Now func() time.Time
|
||||
Tokens Tokenizer
|
||||
Name string // Name is the name of the cookie stored on the browser
|
||||
Lifespan time.Duration // Lifespan is the expiration date of the cookie. 0 means session cookie
|
||||
Inactivity time.Duration // Inactivity is the length of time a token is valid if there is no activity
|
||||
Now func() time.Time
|
||||
Tokens Tokenizer
|
||||
}
|
||||
|
||||
// NewCookieJWT creates an Authenticator that uses cookies for auth
|
||||
func NewCookieJWT(secret string, duration time.Duration) Authenticator {
|
||||
func NewCookieJWT(secret string, lifespan time.Duration) Authenticator {
|
||||
return &cookie{
|
||||
Name: DefaultCookieName,
|
||||
Duration: duration,
|
||||
Now: time.Now,
|
||||
Name: DefaultCookieName,
|
||||
Lifespan: lifespan,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Now: DefaultNowTime,
|
||||
Tokens: &JWT{
|
||||
Secret: secret,
|
||||
Now: time.Now,
|
||||
Now: DefaultNowTime,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -40,42 +44,76 @@ func (c *cookie) Validate(ctx context.Context, r *http.Request) (Principal, erro
|
|||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Duration)
|
||||
return c.Tokens.ValidPrincipal(ctx, Token(cookie.Value), c.Lifespan)
|
||||
}
|
||||
|
||||
// Extend will extend the lifetime of the Token by the Inactivity time. Assumes
|
||||
// Principal is already valid.
|
||||
func (c *cookie) Extend(ctx context.Context, w http.ResponseWriter, p Principal) (Principal, error) {
|
||||
// Refresh the token by extending its life another Inactivity duration
|
||||
p, err := c.Tokens.ExtendedPrincipal(ctx, p, c.Inactivity)
|
||||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
|
||||
// Creating a new token with the extended principal
|
||||
token, err := c.Tokens.Create(ctx, p)
|
||||
if err != nil {
|
||||
return Principal{}, ErrAuthentication
|
||||
}
|
||||
|
||||
// Cookie lifespan can be indirectly figured out by taking the token's
|
||||
// issued at time and adding the lifespan setting The token's issued at
|
||||
// time happens to correspond to the cookie's original issued at time.
|
||||
exp := p.IssuedAt.Add(c.Lifespan)
|
||||
// Once the token has been extended, write it out as a new cookie.
|
||||
c.setCookie(w, string(token), exp)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Authorize will create cookies containing token information. It'll create
|
||||
// a token with cookie.Duration of life to be stored as the cookie's value.
|
||||
func (c *cookie) Authorize(ctx context.Context, w http.ResponseWriter, p Principal) error {
|
||||
token, err := c.Tokens.Create(ctx, p, c.Duration)
|
||||
// Principal will be issued at Now() and will expire
|
||||
// c.Inactivity into the future
|
||||
now := c.Now()
|
||||
p.IssuedAt = now
|
||||
p.ExpiresAt = now.Add(c.Inactivity)
|
||||
|
||||
token, err := c.Tokens.Create(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The time when the cookie expires
|
||||
exp := now.Add(c.Lifespan)
|
||||
c.setCookie(w, string(token), exp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setCookie creates a cookie with value expiring at exp and writes it as a cookie into the response
|
||||
func (c *cookie) setCookie(w http.ResponseWriter, value string, exp time.Time) {
|
||||
// Cookie has a Token baked into it
|
||||
cookie := http.Cookie{
|
||||
Name: DefaultCookieName,
|
||||
Value: string(token),
|
||||
Value: value,
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
}
|
||||
|
||||
// Only set a cookie to be persistent (endure beyond the browser session)
|
||||
// if auth duration is greater than zero
|
||||
if c.Duration > 0 {
|
||||
cookie.Expires = c.Now().UTC().Add(c.Duration)
|
||||
if c.Lifespan > 0 || exp.Before(c.Now()) {
|
||||
cookie.Expires = exp
|
||||
}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expire returns a cookie that will expire an existing cookie
|
||||
func (c *cookie) Expire(w http.ResponseWriter) {
|
||||
cookie := http.Cookie{
|
||||
Name: DefaultCookieName,
|
||||
Value: "none",
|
||||
HttpOnly: true,
|
||||
Path: "/",
|
||||
Expires: c.Now().UTC().Add(-1 * time.Hour), // to expire cookie set the time in the past
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
// to expire cookie set the time in the past
|
||||
c.setCookie(w, "none", c.Now().Add(-1*time.Hour))
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -16,16 +17,21 @@ type MockTokenizer struct {
|
|||
ValidErr error
|
||||
Token Token
|
||||
CreateErr error
|
||||
ExtendErr error
|
||||
}
|
||||
|
||||
func (m *MockTokenizer) ValidPrincipal(ctx context.Context, token Token, duration time.Duration) (Principal, error) {
|
||||
return m.Principal, m.ValidErr
|
||||
}
|
||||
|
||||
func (m *MockTokenizer) Create(ctx context.Context, p Principal, t time.Duration) (Token, error) {
|
||||
func (m *MockTokenizer) Create(ctx context.Context, p Principal) (Token, error) {
|
||||
return m.Token, m.CreateErr
|
||||
}
|
||||
|
||||
func (m *MockTokenizer) ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error) {
|
||||
return principal, m.ExtendErr
|
||||
}
|
||||
|
||||
func TestCookieAuthorize(t *testing.T) {
|
||||
var test = []struct {
|
||||
Desc string
|
||||
|
@ -48,7 +54,7 @@ func TestCookieAuthorize(t *testing.T) {
|
|||
}
|
||||
for _, test := range test {
|
||||
cook := cookie{
|
||||
Duration: 1 * time.Second,
|
||||
Lifespan: 1 * time.Second,
|
||||
Now: func() time.Time {
|
||||
return time.Unix(0, 0)
|
||||
},
|
||||
|
@ -121,8 +127,9 @@ func TestCookieValidate(t *testing.T) {
|
|||
})
|
||||
|
||||
cook := cookie{
|
||||
Name: test.Lookup,
|
||||
Duration: 1 * time.Second,
|
||||
Name: test.Lookup,
|
||||
Lifespan: 1 * time.Second,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Now: func() time.Time {
|
||||
return time.Unix(0, 0)
|
||||
},
|
||||
|
@ -150,3 +157,118 @@ func TestNewCookieJWT(t *testing.T) {
|
|||
t.Errorf("NewCookieJWT() did not create cookie Authenticator")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieExtend(t *testing.T) {
|
||||
history := time.Unix(-446774400, 0)
|
||||
type fields struct {
|
||||
Name string
|
||||
Lifespan time.Duration
|
||||
Inactivity time.Duration
|
||||
Now func() time.Time
|
||||
Tokens Tokenizer
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
w *httptest.ResponseRecorder
|
||||
p Principal
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want Principal
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful extention",
|
||||
want: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
fields: fields{
|
||||
Name: "session",
|
||||
Lifespan: time.Second,
|
||||
Inactivity: time.Second,
|
||||
Now: func() time.Time {
|
||||
return history
|
||||
},
|
||||
Tokens: &MockTokenizer{
|
||||
Principal: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
Token: "token",
|
||||
ExtendErr: nil,
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
w: httptest.NewRecorder(),
|
||||
p: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unable to extend",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
Tokens: &MockTokenizer{
|
||||
ExtendErr: fmt.Errorf("bad extend"),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
w: httptest.NewRecorder(),
|
||||
p: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unable to create",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
Tokens: &MockTokenizer{
|
||||
CreateErr: fmt.Errorf("bad extend"),
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
w: httptest.NewRecorder(),
|
||||
p: Principal{
|
||||
Subject: "subject",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &cookie{
|
||||
Name: tt.fields.Name,
|
||||
Lifespan: tt.fields.Lifespan,
|
||||
Inactivity: tt.fields.Inactivity,
|
||||
Now: tt.fields.Now,
|
||||
Tokens: tt.fields.Tokens,
|
||||
}
|
||||
got, err := c.Extend(tt.args.ctx, tt.args.w, tt.args.p)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("cookie.Extend() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr == false {
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("cookie.Extend() = %v, want %v", got, tt.want)
|
||||
}
|
||||
|
||||
cookies := tt.args.w.HeaderMap["Set-Cookie"]
|
||||
if len(cookies) == 0 {
|
||||
t.Fatal("Expected some cookies but got zero")
|
||||
}
|
||||
log.Printf("%s", cookies)
|
||||
want := fmt.Sprintf("%s=%s", DefaultCookieName, "token")
|
||||
if !strings.Contains(cookies[0], want) {
|
||||
t.Errorf("cookie.Extend() = %v, want %v", cookies[0], want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
120
oauth2/jwt.go
120
oauth2/jwt.go
|
@ -21,7 +21,7 @@ type JWT struct {
|
|||
func NewJWT(secret string) *JWT {
|
||||
return &JWT{
|
||||
Secret: secret,
|
||||
Now: time.Now,
|
||||
Now: DefaultNowTime,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,47 +44,9 @@ func (c *Claims) Valid() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// EverlastingClaims extends jwt.StandardClaims' Valid to make sure claims has
|
||||
// a subject, and it ignores the expiration
|
||||
type EverlastingClaims struct {
|
||||
gojwt.StandardClaims
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// Valid time based claims "iat, nbf".
|
||||
// There is no accounting for clock skew.
|
||||
// As well, if any of the above claims are not in the token, it will still
|
||||
// be considered a valid claim.
|
||||
func (c *EverlastingClaims) Valid() error {
|
||||
vErr := new(gojwt.ValidationError)
|
||||
now := c.Now().Unix()
|
||||
|
||||
// The claims below are optional, by default, so if they are set to the
|
||||
// default value in Go, let's not fail the verification for them.
|
||||
|
||||
if c.VerifyIssuedAt(now, false) == false {
|
||||
vErr.Inner = fmt.Errorf("Token used before issued")
|
||||
vErr.Errors |= gojwt.ValidationErrorIssuedAt
|
||||
}
|
||||
|
||||
if c.VerifyNotBefore(now, false) == false {
|
||||
vErr.Inner = fmt.Errorf("token is not valid yet")
|
||||
vErr.Errors |= gojwt.ValidationErrorNotValidYet
|
||||
}
|
||||
|
||||
if c.Subject == "" {
|
||||
return fmt.Errorf("claim has no subject")
|
||||
}
|
||||
|
||||
if vErr.Errors > 0 {
|
||||
return vErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims.
|
||||
func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, duration time.Duration) (Principal, error) {
|
||||
// ValidPrincipal checks if the jwtToken is signed correctly and validates with Claims. lifespan is the
|
||||
// maximum valid lifetime of a token.
|
||||
func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.Duration) (Principal, error) {
|
||||
gojwt.TimeFunc = j.Now
|
||||
|
||||
// Check for expected signing method.
|
||||
|
@ -95,20 +57,16 @@ func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, duration time.
|
|||
return []byte(j.Secret), nil
|
||||
}
|
||||
|
||||
if duration == 0 {
|
||||
return j.ValidEverlastingClaims(jwtToken, alg)
|
||||
}
|
||||
|
||||
return j.ValidClaims(jwtToken, duration, alg)
|
||||
return j.ValidClaims(jwtToken, lifespan, alg)
|
||||
}
|
||||
|
||||
// ValidClaims validates a token with StandardClaims
|
||||
func (j *JWT) ValidClaims(jwtToken Token, duration time.Duration, alg gojwt.Keyfunc) (Principal, error) {
|
||||
func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyfunc) (Principal, error) {
|
||||
// 1. Checks for expired tokens
|
||||
// 2. Checks if time is after the issued at
|
||||
// 3. Check if time is after not before (nbf)
|
||||
// 4. Check if subject is not empty
|
||||
// 5. Check if duration matches auth duration
|
||||
// 5. Check if duration less than auth lifespan
|
||||
token, err := gojwt.ParseWithClaims(string(jwtToken), &Claims{}, alg)
|
||||
if err != nil {
|
||||
return Principal{}, err
|
||||
|
@ -123,56 +81,35 @@ func (j *JWT) ValidClaims(jwtToken Token, duration time.Duration, alg gojwt.Keyf
|
|||
return Principal{}, fmt.Errorf("unable to convert claims to standard claims")
|
||||
}
|
||||
|
||||
if time.Duration(claims.ExpiresAt-claims.IssuedAt)*time.Second != duration {
|
||||
return Principal{}, fmt.Errorf("claims duration is different from auth duration")
|
||||
exp := time.Unix(claims.ExpiresAt, 0)
|
||||
iat := time.Unix(claims.IssuedAt, 0)
|
||||
|
||||
// If the duration of the claim is longer than the auth lifespan then this is
|
||||
// an invalid claim because server assumes that lifespan is the maximum possible
|
||||
// duration
|
||||
if exp.Sub(iat) > lifespan {
|
||||
return Principal{}, fmt.Errorf("claims duration is different from auth lifespan")
|
||||
}
|
||||
|
||||
return Principal{
|
||||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
ExpiresAt: exp,
|
||||
IssuedAt: iat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidEverlastingClaims validates a token with EverlastingClaims
|
||||
func (j *JWT) ValidEverlastingClaims(jwtToken Token, alg gojwt.Keyfunc) (Principal, error) {
|
||||
// 1. Checks if time is after the issued at
|
||||
// 2. Check if time is after not before (nbf)
|
||||
// 3. Check if subject is not empty
|
||||
token, err := gojwt.ParseWithClaims(string(jwtToken), &EverlastingClaims{
|
||||
Now: j.Now,
|
||||
}, alg)
|
||||
if err != nil {
|
||||
return Principal{}, err
|
||||
// at time of this writing and researching the docs, token.Valid seems to be always true
|
||||
} else if !token.Valid {
|
||||
return Principal{}, err
|
||||
}
|
||||
|
||||
// at time of this writing and researching the docs, there will always be claims
|
||||
claims, ok := token.Claims.(*EverlastingClaims)
|
||||
if !ok {
|
||||
return Principal{}, fmt.Errorf("unable to convert claims to everlasting claims")
|
||||
}
|
||||
|
||||
return Principal{
|
||||
Subject: claims.Subject,
|
||||
Issuer: claims.Issuer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create creates a signed JWT token from user that expires at Now + duration
|
||||
func (j *JWT) Create(ctx context.Context, user Principal, duration time.Duration) (Token, error) {
|
||||
gojwt.TimeFunc = j.Now
|
||||
// Create creates a signed JWT token from user that expires at Principal's ExpireAt time.
|
||||
func (j *JWT) Create(ctx context.Context, user Principal) (Token, error) {
|
||||
// Create a new token object, specifying signing method and the claims
|
||||
// you would like it to contain.
|
||||
now := j.Now().UTC()
|
||||
claims := &Claims{
|
||||
gojwt.StandardClaims{
|
||||
Subject: user.Subject,
|
||||
Issuer: user.Issuer,
|
||||
ExpiresAt: now.Add(duration).Unix(),
|
||||
IssuedAt: now.Unix(),
|
||||
NotBefore: now.Unix(),
|
||||
ExpiresAt: user.ExpiresAt.Unix(),
|
||||
IssuedAt: user.IssuedAt.Unix(),
|
||||
NotBefore: user.IssuedAt.Unix(),
|
||||
},
|
||||
}
|
||||
token := gojwt.NewWithClaims(gojwt.SigningMethodHS256, claims)
|
||||
|
@ -184,3 +121,12 @@ func (j *JWT) Create(ctx context.Context, user Principal, duration time.Duration
|
|||
}
|
||||
return Token(t), nil
|
||||
}
|
||||
|
||||
// ExtendedPrincipal sets the expires at to be the current time plus the extention into the future
|
||||
func (j *JWT) ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error) {
|
||||
// Extend the time of expiration. Do not change IssuedAt as the
|
||||
// lifetime of the token is extended, but, NOT the original time
|
||||
// of issue. This is used to enforce a maximum lifetime of a token
|
||||
principal.ExpiresAt = j.Now().Add(extension)
|
||||
return principal, nil
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package oauth2_test
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
history := time.Unix(-446774400, 0)
|
||||
var tests = []struct {
|
||||
Desc string
|
||||
Secret string
|
||||
|
@ -33,7 +35,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0Mzk5LCJuYmYiOi00NDY3NzQ0MDB9.Ga0zGXWTT2CBVnnIhIO5tUAuBEVk4bKPaT4t4MU1ngo",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -42,7 +46,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAxLCJuYmYiOi00NDY3NzQ0MDB9.vWXdm0-XQ_pW62yBpSISFFJN_yz0vqT9_INcUKTp5Q8",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
Subject: "",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("token is expired by 1s"),
|
||||
},
|
||||
|
@ -52,7 +58,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQzOTl9.TMGAhv57u1aosjc4ywKC7cElP1tKyQH7GmRF2ToAxlE",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
Subject: "",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("token is not valid yet"),
|
||||
},
|
||||
|
@ -62,7 +70,9 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOi00NDY3NzQ0MDAsImV4cCI6LTQ0Njc3NDQwMCwibmJmIjotNDQ2Nzc0NDAwfQ.gxsA6_Ei3s0f2I1TAtrrb8FmGiO25OqVlktlF_ylhX4",
|
||||
Duration: time.Second,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "",
|
||||
Subject: "",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("claim has no subject"),
|
||||
},
|
||||
|
@ -72,18 +82,12 @@ func TestAuthenticate(t *testing.T) {
|
|||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0NDAwLCJuYmYiOi00NDY3NzQ0MDB9._rZ4gOIei9PizHOABH6kLcJTA3jm8ls0YnDxtz1qeUI",
|
||||
Duration: 500 * time.Hour,
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
ExpiresAt: history,
|
||||
IssuedAt: history,
|
||||
},
|
||||
Err: errors.New("claims duration is different from auth duration"),
|
||||
},
|
||||
{
|
||||
Desc: "Test valid EverlastingClaim",
|
||||
Secret: "secret",
|
||||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIiwibmFtZSI6IkRvYyBCcm93biIsImlhdCI6LTQ0Njc3NDQwMCwiZXhwIjotNDQ2Nzc0Mzk5LCJuYmYiOi00NDY3NzQ0MDB9.Ga0zGXWTT2CBVnnIhIO5tUAuBEVk4bKPaT4t4MU1ngo",
|
||||
Principal: oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
j := oauth2.JWT{
|
||||
|
@ -107,18 +111,20 @@ func TestAuthenticate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
duration := time.Second
|
||||
expected := oauth2.Token("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOi00NDY3NzQzOTksImlhdCI6LTQ0Njc3NDQwMCwibmJmIjotNDQ2Nzc0NDAwLCJzdWIiOiIvY2hyb25vZ3JhZi92MS91c2Vycy8xIn0.ofQM6yTmrmve5JeEE0RcK4_euLXuZ_rdh6bLAbtbC9M")
|
||||
history := time.Unix(-446774400, 0)
|
||||
j := oauth2.JWT{
|
||||
Secret: "secret",
|
||||
Now: func() time.Time {
|
||||
return time.Unix(-446774400, 0)
|
||||
return history
|
||||
},
|
||||
}
|
||||
p := oauth2.Principal{
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
Subject: "/chronograf/v1/users/1",
|
||||
ExpiresAt: history.Add(time.Second),
|
||||
IssuedAt: history,
|
||||
}
|
||||
if token, err := j.Create(context.Background(), p, duration); err != nil {
|
||||
if token, err := j.Create(context.Background(), p); err != nil {
|
||||
t.Errorf("Error creating token for principal: %v", err)
|
||||
} else if token != expected {
|
||||
t.Errorf("Error creating token; expected: %s actual: %s", expected, token)
|
||||
|
@ -134,3 +140,56 @@ func TestSigningMethod(t *testing.T) {
|
|||
t.Errorf("Error wanted 'unexpected signing method', got %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWT_ExtendedPrincipal(t *testing.T) {
|
||||
history := time.Unix(-446774400, 0)
|
||||
type fields struct {
|
||||
Now func() time.Time
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
principal oauth2.Principal
|
||||
extension time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want oauth2.Principal
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Extend principal by one hour",
|
||||
fields: fields{
|
||||
Now: func() time.Time {
|
||||
return history
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
principal: oauth2.Principal{
|
||||
ExpiresAt: history,
|
||||
},
|
||||
extension: time.Hour,
|
||||
},
|
||||
want: oauth2.Principal{
|
||||
ExpiresAt: history.Add(time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
j := &oauth2.JWT{
|
||||
Now: tt.fields.Now,
|
||||
}
|
||||
got, err := j.ExtendedPrincipal(tt.args.ctx, tt.args.principal, tt.args.extension)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("JWT.ExtendedPrincipal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("JWT.ExtendedPrincipal() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ func NewAuthMux(p Provider, a Authenticator, t Tokenizer, l chronograf.Logger) *
|
|||
Logger: l,
|
||||
SuccessURL: "/",
|
||||
FailureURL: "/login",
|
||||
Now: DefaultNowTime,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,6 +39,7 @@ type AuthMux struct {
|
|||
Logger chronograf.Logger // Logger is used to give some more information about the OAuth2 process
|
||||
SuccessURL string // SuccessURL is redirect location after successful authorization
|
||||
FailureURL string // FailureURL is redirect location after authorization failure
|
||||
Now func() time.Time // Now returns the current time (for testing)
|
||||
}
|
||||
|
||||
// Login uses a Cookie with a random string as the state validation method. JWTs are
|
||||
|
@ -52,12 +54,17 @@ func (j *AuthMux) Login() http.Handler {
|
|||
// oauth2 provider's password.
|
||||
// If the callback is not received within 10 minutes, then authorization will fail.
|
||||
csrf := randomString(32) // 32 is not important... just long
|
||||
p := Principal{
|
||||
Subject: csrf,
|
||||
}
|
||||
now := j.Now()
|
||||
|
||||
// This token will be valid for 10 minutes. Any chronograf server will
|
||||
// be able to validate this token.
|
||||
token, err := j.Tokens.Create(r.Context(), p, TenMinutes)
|
||||
p := Principal{
|
||||
Subject: csrf,
|
||||
IssuedAt: now,
|
||||
ExpiresAt: now.Add(TenMinutes),
|
||||
}
|
||||
token, err := j.Tokens.Create(r.Context(), p)
|
||||
|
||||
// This is likely an internal server error
|
||||
if err != nil {
|
||||
j.Logger.
|
||||
|
|
|
@ -30,10 +30,11 @@ func setupMuxTest(selector func(*AuthMux) http.Handler) (*http.Client, *httptest
|
|||
mp := &MockProvider{"biff@example.com", provider.URL}
|
||||
mt := &YesManTokenizer{}
|
||||
auth := &cookie{
|
||||
Name: DefaultCookieName,
|
||||
Duration: 1 * time.Hour,
|
||||
Now: now,
|
||||
Tokens: mt,
|
||||
Name: DefaultCookieName,
|
||||
Lifespan: 1 * time.Hour,
|
||||
Inactivity: DefaultInactivityDuration,
|
||||
Now: now,
|
||||
Tokens: mt,
|
||||
}
|
||||
|
||||
jm := NewAuthMux(mp, auth, mt, clog.New(clog.ParseLevel("debug")))
|
||||
|
|
|
@ -30,8 +30,10 @@ var (
|
|||
|
||||
// Principal is any entity that can be authenticated
|
||||
type Principal struct {
|
||||
Subject string
|
||||
Issuer string
|
||||
Subject string
|
||||
Issuer string
|
||||
ExpiresAt time.Time
|
||||
IssuedAt time.Time
|
||||
}
|
||||
|
||||
/* Interfaces */
|
||||
|
@ -66,6 +68,8 @@ type Authenticator interface {
|
|||
Validate(context.Context, *http.Request) (Principal, error)
|
||||
// Authorize will grant privileges to a Principal
|
||||
Authorize(context.Context, http.ResponseWriter, Principal) error
|
||||
// Extend will extend the lifetime of a already validated Principal
|
||||
Extend(context.Context, http.ResponseWriter, Principal) (Principal, error)
|
||||
// Expire revokes privileges from a Principal
|
||||
Expire(http.ResponseWriter)
|
||||
}
|
||||
|
@ -78,9 +82,11 @@ type Token string
|
|||
// non-sensitive equivalent, referred to as a token, that has no extrinsic
|
||||
// or exploitable meaning or value.
|
||||
type Tokenizer interface {
|
||||
// Create uses a token lasting duration with Principal data
|
||||
Create(context.Context, Principal, time.Duration) (Token, error)
|
||||
// Create issues a token at Principal's IssuedAt that lasts until Principal's ExpireAt
|
||||
Create(context.Context, Principal) (Token, error)
|
||||
// ValidPrincipal checks if the token has a valid Principal and requires
|
||||
// a duration to ensure it complies with possible server runtime arguments.
|
||||
ValidPrincipal(context.Context, Token, time.Duration) (Principal, error)
|
||||
// a lifespan duration to ensure it complies with possible server runtime arguments.
|
||||
ValidPrincipal(ctx context.Context, token Token, lifespan time.Duration) (Principal, error)
|
||||
// ExtendedPrincipal adds the extention to the principal's lifespan.
|
||||
ExtendedPrincipal(ctx context.Context, principal Principal, extension time.Duration) (Principal, error)
|
||||
}
|
||||
|
|
|
@ -63,10 +63,14 @@ func (y *YesManTokenizer) ValidPrincipal(ctx context.Context, token Token, durat
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (y *YesManTokenizer) Create(ctx context.Context, p Principal, t time.Duration) (Token, error) {
|
||||
func (y *YesManTokenizer) Create(ctx context.Context, p Principal) (Token, error) {
|
||||
return Token("HELLO?!MCFLY?!ANYONEINTHERE?!"), nil
|
||||
}
|
||||
|
||||
func (y *YesManTokenizer) ExtendedPrincipal(ctx context.Context, p Principal, ext time.Duration) (Principal, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func NewTestTripper(log chronograf.Logger, ts *httptest.Server, rt http.RoundTripper) (*TestTripper, error) {
|
||||
url, err := url.Parse(ts.URL)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package oauth2
|
||||
|
||||
import "time"
|
||||
|
||||
// DefaultNowTime returns UTC time at the present moment
|
||||
var DefaultNowTime = func() time.Time { return time.Now().UTC() }
|
|
@ -21,7 +21,7 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
|
|||
WithField("url", r.URL)
|
||||
|
||||
ctx := r.Context()
|
||||
// We do not check the validity of the principal. Those
|
||||
// We do not check the authorization of the principal. Those
|
||||
// served further down the chain should do so.
|
||||
principal, err := auth.Validate(ctx, r)
|
||||
if err != nil {
|
||||
|
@ -30,6 +30,15 @@ func AuthorizedToken(auth oauth2.Authenticator, logger chronograf.Logger, next h
|
|||
return
|
||||
}
|
||||
|
||||
// If the principal is valid we will extend its lifespan
|
||||
// into the future
|
||||
principal, err = auth.Extend(ctx, w, principal)
|
||||
if err != nil {
|
||||
log.Error("Unable to extend principal")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the principal to the next handler
|
||||
ctx = context.WithValue(ctx, oauth2.PrincipalKey, principal)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
|
|
@ -15,19 +15,30 @@ import (
|
|||
type MockAuthenticator struct {
|
||||
Principal oauth2.Principal
|
||||
ValidateErr error
|
||||
ExtendErr error
|
||||
Serialized string
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Validate(context.Context, *http.Request) (oauth2.Principal, error) {
|
||||
return m.Principal, m.ValidateErr
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Extend(ctx context.Context, w http.ResponseWriter, p oauth2.Principal) (oauth2.Principal, error) {
|
||||
cookie := http.Cookie{}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
return m.Principal, m.ExtendErr
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Authorize(ctx context.Context, w http.ResponseWriter, p oauth2.Principal) error {
|
||||
cookie := http.Cookie{}
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockAuthenticator) Expire(http.ResponseWriter) {}
|
||||
|
||||
func (m *MockAuthenticator) ValidAuthorization(ctx context.Context, serializedAuthorization string) (oauth2.Principal, error) {
|
||||
return oauth2.Principal{}, nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue