Merge branch 'master' into flip-table-graph-feature-flag

pull/10616/head
ebb-tide 2018-03-28 17:45:48 -07:00
commit 5a66eccd47
18 changed files with 455 additions and 72 deletions

View File

@ -1,3 +1,11 @@
## v1.5.0.0 [unreleased]
### Features
1. [#2526](https://github.com/influxdata/chronograf/pull/2526): Add support for RS256/JWKS verification, support for id_token parsing (as in ADFS)
### UI Improvements
### Bug Fixes
## v1.4.3.0 [unreleased]
### Features
@ -19,7 +27,10 @@
1. [#2970](https://github.com/influxdata/chronograf/pull/2970): Fix hanging browser on docker host dashboard
1. [#3006](https://github.com/influxdata/chronograf/pull/3006): Fix Kapacitor Rules task enabled checkboxes to only toggle exactly as clicked
1. [#3048](https://github.com/influxdata/chronograf/pull/3048): Prevent Multi-Select Dropdown in InfluxDB Admin Users and Roles tabs from losing selection state
1. [#3073](https://github.com/influxdata/chronograf/pull/3073): Fix Delete button in All Users admin page
1. [#3068](https://github.com/influxdata/chronograf/pull/3068): Fix intermittent missing fill from graphs
1. [#3087](https://github.com/influxdata/chronograf/pull/3087): Exit annotation edit mode when user navigates away from dashboard
1. [#3079](https://github.com/influxdata/chronograf/pull/3082): Support custom time range in annotations api wrapper
## v1.4.2.3 [2018-03-08]
@ -35,7 +46,6 @@
## v1.4.2.1 [2018-02-28]
### Features
1. [#2837](https://github.com/influxdata/chronograf/pull/2837): Prevent execution of queries in cells that are not in view on the dashboard page
1. [#2829](https://github.com/influxdata/chronograf/pull/2829): Add an optional persistent legend which can toggle series visibility to dashboard cells
1. [#2846](https://github.com/influxdata/chronograf/pull/2846): Allow user to annotate graphs via UI or API

View File

@ -2821,6 +2821,9 @@ func TestServer(t *testing.T) {
// This is so that we can use staticly generate jwts
tt.args.server.TokenSecret = "secret"
// Endpoint for validating RSA256 signatures when using id_token parsing for ADFS
tt.args.server.JwksURL = ""
boltFile := newBoltFile()
tt.args.server.BoltPath = boltFile
@ -2938,7 +2941,7 @@ func TestServer(t *testing.T) {
buf, _ := json.Marshal(tt.args.payload)
reqBody := ioutil.NopCloser(bytes.NewReader(buf))
req, _ := http.NewRequest(tt.args.method, serverURL, reqBody)
token, _ := oauth2.NewJWT(tt.args.server.TokenSecret).Create(ctx, tt.args.principal)
token, _ := oauth2.NewJWT(tt.args.server.TokenSecret, tt.args.server.JwksURL).Create(ctx, tt.args.principal)
req.AddCookie(&http.Cookie{
Name: "session",
Value: string(token),

View File

@ -3,6 +3,7 @@ package oauth2
import (
"context"
"fmt"
gojwt "github.com/dgrijalva/jwt-go"
"log"
"net/http"
"net/http/httptest"
@ -32,6 +33,10 @@ func (m *MockTokenizer) ExtendedPrincipal(ctx context.Context, principal Princip
return principal, m.ExtendErr
}
func (m *MockTokenizer) GetClaims(tokenString string) (gojwt.MapClaims, error) {
return gojwt.MapClaims{}, nil
}
func TestCookieAuthorize(t *testing.T) {
var test = []struct {
Desc string

View File

@ -7,11 +7,20 @@ import (
"net/http"
"strings"
gojwt "github.com/dgrijalva/jwt-go"
"github.com/influxdata/chronograf"
"golang.org/x/oauth2"
)
var _ Provider = &Generic{}
// ExtendedProvider extendts the base Provider interface with optional methods
type ExtendedProvider interface {
Provider
// get PrincipalID from id_token
PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error)
GroupFromClaims(claims gojwt.MapClaims) (string, error)
}
var _ ExtendedProvider = &Generic{}
// Generic provides OAuth Login and Callback server and is modeled
// after the Github OAuth2 provider. Callback will set an authentication
@ -197,3 +206,26 @@ func ofDomain(requiredDomains []string, email string) bool {
}
return false
}
// PrincipalIDFromClaims verifies an optional id_token and extracts email address of the user
func (g *Generic) PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error) {
if id, ok := claims[g.APIKey].(string); ok {
return id, nil
}
return "", fmt.Errorf("no claim for %s", g.APIKey)
}
// GroupFromClaims verifies an optional id_token, extracts the email address of the user and splits off the domain part
func (g *Generic) GroupFromClaims(claims gojwt.MapClaims) (string, error) {
if id, ok := claims[g.APIKey].(string); ok {
email := strings.Split(id, "@")
if len(email) != 2 {
g.Logger.Error("malformed email address, expected %q to contain @ symbol", id)
return "DEFAULT", nil
}
return email[1], nil
}
return "", fmt.Errorf("no claim for %s", g.APIKey)
}

View File

@ -2,7 +2,12 @@ package oauth2
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
gojwt "github.com/dgrijalva/jwt-go"
@ -13,15 +18,19 @@ var _ Tokenizer = &JWT{}
// JWT represents a javascript web token that can be validated or marshaled into string.
type JWT struct {
Secret string
Now func() time.Time
Secret string
Jwksurl string
Now func() time.Time
}
// NewJWT creates a new JWT using time.Now; secret is used for signing and validating.
func NewJWT(secret string) *JWT {
// NewJWT creates a new JWT using time.Now
// secret is used for signing and validating signatures (HS256/HMAC)
// jwksurl is used for validating RS256 signatures.
func NewJWT(secret string, jwksurl string) *JWT {
return &JWT{
Secret: secret,
Now: DefaultNowTime,
Secret: secret,
Jwksurl: jwksurl,
Now: DefaultNowTime,
}
}
@ -62,16 +71,98 @@ func (j *JWT) ValidPrincipal(ctx context.Context, jwtToken Token, lifespan time.
gojwt.TimeFunc = j.Now
// Check for expected signing method.
alg := func(token *gojwt.Token) (interface{}, error) {
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(j.Secret), nil
}
alg := j.KeyFunc
return j.ValidClaims(jwtToken, lifespan, alg)
}
// KeyFunc verifies HMAC or RSA/RS256 signatures
func (j *JWT) KeyFunc(token *gojwt.Token) (interface{}, error) {
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); ok {
return []byte(j.Secret), nil
} else if _, ok := token.Method.(*gojwt.SigningMethodRSA); ok {
return j.KeyFuncRS256(token)
}
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
// For the id_token, the recommended signature algorithm is RS256, which
// means we need to verify the token against a public key. This public key
// is available from the key discovery service in JSON Web Key (JWK).
// JWK is specified in RFC 7517.
//
// The location of the key discovery service (JWKSURL) is published in the
// OpenID Provider Configuration Information at /.well-known/openid-configuration
// implements rfc7517 section 4.7 "x5c" (X.509 Certificate Chain) Parameter
// JWK defines a JSON Web KEy nested struct
type JWK struct {
Kty string `json:"kty"`
Use string `json:"use"`
Alg string `json:"alg"`
Kid string `json:"kid"`
X5t string `json:"x5t"`
N string `json:"n"`
E string `json:"e"`
X5c []string `json:"x5c"`
}
// JWKS defines a JKW[]
type JWKS struct {
Keys []JWK `json:"keys"`
}
// KeyFuncRS256 verifies RS256 signed JWT tokens, it looks up the signing key in the key discovery service
func (j *JWT) KeyFuncRS256(token *gojwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*gojwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unsupported signing method: %v", token.Header["alg"])
}
// read JWKS document from key discovery service
if j.Jwksurl == "" {
return nil, fmt.Errorf("JWKSURL not specified, cannot validate RS256 signature")
}
rr, err := http.Get(j.Jwksurl)
if err != nil {
return nil, err
}
defer rr.Body.Close()
body, err := ioutil.ReadAll(rr.Body)
if err != nil {
return nil, err
}
// parse json to struct
var jwks JWKS
if err := json.Unmarshal([]byte(body), &jwks); err != nil {
return nil, err
}
// extract cert when kid and alg match
var certPkix []byte
for _, jwk := range jwks.Keys {
if token.Header["kid"] == jwk.Kid && token.Header["alg"] == jwk.Alg {
// FIXME: optionally walk the key chain, see rfc7517 section 4.7
certPkix, err = base64.StdEncoding.DecodeString(jwk.X5c[0])
if err != nil {
return nil, fmt.Errorf("base64 decode error for JWK kid %v", token.Header["kid"])
}
}
}
if certPkix == nil {
return nil, fmt.Errorf("no signing key found for kid %v", token.Header["kid"])
}
// parse certificate (from PKIX format) and return signing key
cert, err := x509.ParseCertificate(certPkix)
if err != nil {
return nil, err
}
return cert.PublicKey, nil
}
// ValidClaims validates a token with StandardClaims
func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyfunc) (Principal, error) {
// 1. Checks for expired tokens
@ -114,6 +205,28 @@ func (j *JWT) ValidClaims(jwtToken Token, lifespan time.Duration, alg gojwt.Keyf
}, nil
}
// GetClaims extracts claims from id_token
func (j *JWT) GetClaims(tokenString string) (gojwt.MapClaims, error) {
var claims gojwt.MapClaims
gojwt.TimeFunc = j.Now
token, err := gojwt.Parse(tokenString, j.KeyFunc)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("token is not valid")
}
claims, ok := token.Claims.(gojwt.MapClaims)
if !ok {
return nil, fmt.Errorf("token has no claims")
}
return claims, nil
}
// 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

View File

@ -3,6 +3,9 @@ package oauth2_test
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
@ -152,8 +155,82 @@ func TestSigningMethod(t *testing.T) {
j := oauth2.JWT{}
if _, err := j.ValidPrincipal(context.Background(), token, 0); err == nil {
t.Error("Error was expected while validating incorrectly signed token")
} else if err.Error() != "unexpected signing method: RS256" {
t.Errorf("Error wanted 'unexpected signing method', got %s", err.Error())
} else if err.Error() != "JWKSURL not specified, cannot validate RS256 signature" {
t.Errorf("Error wanted 'JWKSURL not specified, cannot validate RS256 signature', got %s", err.Error())
}
}
func TestGetClaims(t *testing.T) {
var tests = []struct {
Name string
TokenString string
JwksDocument string
Iat int64
Err error
}{
{
Name: "Valid Token with RS256 signature verified against correct JWKS document",
TokenString: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTMxNjU4ODksImV4cCI6MTUxMzE2OTQ4OSwiYXV0aF90aW1lIjoxNTEzMTY1ODg4LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.nK51Ui4XN45SVul9igNaKFQd-F63BNstBzW-T5LBVm_ANHCEHyP3_88C3ffkkQIi3PxYacRJGtfswP35ws7YJUcNp-GoGZARqz62NpMtbQyhos6mCaVXwPoxPbrZx4AkMQgxkZwJcOzceX7mpjcT3kCth30chN3lkhzSjGrXe4ZDOAV25liS-dsdBiqDiaTB91sS534GM76qJQxFUs51oSbYTRdCN1VJ0XopMcasfVDzFrtSbyvEIVXlpKK2HplnhheqF4QHrM_3cjV_NGRr3tYLe-AGTdDXKWlJD1GDz1ECXeMGQHPoz3U8cqNsFLYBstIlCgfnBWgWsPZSvJPJUg",
JwksDocument: `{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"YDBUhqdWksKXdGuX0sytjaUuxhA","x5t":"YDBUhqdWksKXdGuX0sytjaUuxhA","n":"uwVVrs5OJRKeLUk0H5N_b4Jbvff3rxlg3WIeOO-zSSPTC5oFOc5_te0rLgVoNJJB4rNM4A7BEXI885xLrjfL3l3LHqaJetvR0tdLAnkvbUKUiGxnuGnmOsgh491P95pHPIAniy2p64FQoBbTJ0a6cF5LRuPPHKVXgjXjTydvmKrt_IVaWUDgICRsw5Bbv290SahmxcdO3akSgfsZtRkR8SmaMzAPYINi2_8P2evaKAnMQLTgUVkctaEamO_6HJ5f5sWheV7trLekU35xPVkPwShDelefnhyJcO5yICXqXzuewBEni9LrxAEJYN2rYfiFQWJy-pDe5DPUBs-IFTpctQ","e":"AQAB","x5c":["MIIC6DCCAdCgAwIBAgIQPszqLhbrpZlE+jEJTyJg7jANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBkc3RjaW1hYWQxcC5kc3QtaXRzLmRlMB4XDTE3MTIwNDE0MDEwOFoXDTE4MTIwNDE0MDEwOFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZHN0Y2ltYWFkMXAuZHN0LWl0cy5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALsFVa7OTiUSni1JNB+Tf2+CW733968ZYN1iHjjvs0kj0wuaBTnOf7XtKy4FaDSSQeKzTOAOwRFyPPOcS643y95dyx6miXrb0dLXSwJ5L21ClIhsZ7hp5jrIIePdT\/eaRzyAJ4stqeuBUKAW0ydGunBeS0bjzxylV4I1408nb5iq7fyFWllA4CAkbMOQW79vdEmoZsXHTt2pEoH7GbUZEfEpmjMwD2CDYtv\/D9nr2igJzEC04FFZHLWhGpjv+hyeX+bFoXle7ay3pFN+cT1ZD8EoQ3pXn54ciXDuciAl6l87nsARJ4vS68QBCWDdq2H4hUFicvqQ3uQz1AbPiBU6XLUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPHCisZyPf\/fuuQEW5LyzZSYMwBRYVR6kk\/M2ZNx6TrUEwmOb10RQ3G97bLAshN44g5lWdPYz4EOt6d2o71etIjf79f+IR0MAjEgBB2HThaHcMU9KG229Ftcauie9XeurngMawTRu60YqH7+go8EMf6a1Kdnx37DMy\/1LRlsYJVfEoOCab3GgcIdXrRSYWqsY4SVJZiTPYdqz9vmNPSXXiDSOTl6qXHV\/f53WTS2V5aIQbuJJziXlceusuVNny0o5h+j6ovZ1HhEGAu3lpD+8kY8KUqA4kXMH3VNZqzHBYazJx\/QBB3bG45cZSOvV3gUOnGBgiv9NBWjhvmY0fC3J6Q=="]}]}`,
Iat: int64(1513165889),
},
{
Name: "Valid Token with RS256 signature verified against correct JWKS document but predated",
TokenString: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTMxNjU4ODksImV4cCI6MTUxMzE2OTQ4OSwiYXV0aF90aW1lIjoxNTEzMTY1ODg4LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.nK51Ui4XN45SVul9igNaKFQd-F63BNstBzW-T5LBVm_ANHCEHyP3_88C3ffkkQIi3PxYacRJGtfswP35ws7YJUcNp-GoGZARqz62NpMtbQyhos6mCaVXwPoxPbrZx4AkMQgxkZwJcOzceX7mpjcT3kCth30chN3lkhzSjGrXe4ZDOAV25liS-dsdBiqDiaTB91sS534GM76qJQxFUs51oSbYTRdCN1VJ0XopMcasfVDzFrtSbyvEIVXlpKK2HplnhheqF4QHrM_3cjV_NGRr3tYLe-AGTdDXKWlJD1GDz1ECXeMGQHPoz3U8cqNsFLYBstIlCgfnBWgWsPZSvJPJUg",
JwksDocument: `{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"YDBUhqdWksKXdGuX0sytjaUuxhA","x5t":"YDBUhqdWksKXdGuX0sytjaUuxhA","n":"uwVVrs5OJRKeLUk0H5N_b4Jbvff3rxlg3WIeOO-zSSPTC5oFOc5_te0rLgVoNJJB4rNM4A7BEXI885xLrjfL3l3LHqaJetvR0tdLAnkvbUKUiGxnuGnmOsgh491P95pHPIAniy2p64FQoBbTJ0a6cF5LRuPPHKVXgjXjTydvmKrt_IVaWUDgICRsw5Bbv290SahmxcdO3akSgfsZtRkR8SmaMzAPYINi2_8P2evaKAnMQLTgUVkctaEamO_6HJ5f5sWheV7trLekU35xPVkPwShDelefnhyJcO5yICXqXzuewBEni9LrxAEJYN2rYfiFQWJy-pDe5DPUBs-IFTpctQ","e":"AQAB","x5c":["MIIC6DCCAdCgAwIBAgIQPszqLhbrpZlE+jEJTyJg7jANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBkc3RjaW1hYWQxcC5kc3QtaXRzLmRlMB4XDTE3MTIwNDE0MDEwOFoXDTE4MTIwNDE0MDEwOFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZHN0Y2ltYWFkMXAuZHN0LWl0cy5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALsFVa7OTiUSni1JNB+Tf2+CW733968ZYN1iHjjvs0kj0wuaBTnOf7XtKy4FaDSSQeKzTOAOwRFyPPOcS643y95dyx6miXrb0dLXSwJ5L21ClIhsZ7hp5jrIIePdT\/eaRzyAJ4stqeuBUKAW0ydGunBeS0bjzxylV4I1408nb5iq7fyFWllA4CAkbMOQW79vdEmoZsXHTt2pEoH7GbUZEfEpmjMwD2CDYtv\/D9nr2igJzEC04FFZHLWhGpjv+hyeX+bFoXle7ay3pFN+cT1ZD8EoQ3pXn54ciXDuciAl6l87nsARJ4vS68QBCWDdq2H4hUFicvqQ3uQz1AbPiBU6XLUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPHCisZyPf\/fuuQEW5LyzZSYMwBRYVR6kk\/M2ZNx6TrUEwmOb10RQ3G97bLAshN44g5lWdPYz4EOt6d2o71etIjf79f+IR0MAjEgBB2HThaHcMU9KG229Ftcauie9XeurngMawTRu60YqH7+go8EMf6a1Kdnx37DMy\/1LRlsYJVfEoOCab3GgcIdXrRSYWqsY4SVJZiTPYdqz9vmNPSXXiDSOTl6qXHV\/f53WTS2V5aIQbuJJziXlceusuVNny0o5h+j6ovZ1HhEGAu3lpD+8kY8KUqA4kXMH3VNZqzHBYazJx\/QBB3bG45cZSOvV3gUOnGBgiv9NBWjhvmY0fC3J6Q=="]}]}`,
Iat: int64(1513165889) - 1,
Err: errors.New("Token used before issued"),
},
{
Name: "Valid Token with RS256 signature verified against correct JWKS document but outdated",
TokenString: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTMxNjU4ODksImV4cCI6MTUxMzE2OTQ4OSwiYXV0aF90aW1lIjoxNTEzMTY1ODg4LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.nK51Ui4XN45SVul9igNaKFQd-F63BNstBzW-T5LBVm_ANHCEHyP3_88C3ffkkQIi3PxYacRJGtfswP35ws7YJUcNp-GoGZARqz62NpMtbQyhos6mCaVXwPoxPbrZx4AkMQgxkZwJcOzceX7mpjcT3kCth30chN3lkhzSjGrXe4ZDOAV25liS-dsdBiqDiaTB91sS534GM76qJQxFUs51oSbYTRdCN1VJ0XopMcasfVDzFrtSbyvEIVXlpKK2HplnhheqF4QHrM_3cjV_NGRr3tYLe-AGTdDXKWlJD1GDz1ECXeMGQHPoz3U8cqNsFLYBstIlCgfnBWgWsPZSvJPJUg",
JwksDocument: `{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"YDBUhqdWksKXdGuX0sytjaUuxhA","x5t":"YDBUhqdWksKXdGuX0sytjaUuxhA","n":"uwVVrs5OJRKeLUk0H5N_b4Jbvff3rxlg3WIeOO-zSSPTC5oFOc5_te0rLgVoNJJB4rNM4A7BEXI885xLrjfL3l3LHqaJetvR0tdLAnkvbUKUiGxnuGnmOsgh491P95pHPIAniy2p64FQoBbTJ0a6cF5LRuPPHKVXgjXjTydvmKrt_IVaWUDgICRsw5Bbv290SahmxcdO3akSgfsZtRkR8SmaMzAPYINi2_8P2evaKAnMQLTgUVkctaEamO_6HJ5f5sWheV7trLekU35xPVkPwShDelefnhyJcO5yICXqXzuewBEni9LrxAEJYN2rYfiFQWJy-pDe5DPUBs-IFTpctQ","e":"AQAB","x5c":["MIIC6DCCAdCgAwIBAgIQPszqLhbrpZlE+jEJTyJg7jANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBkc3RjaW1hYWQxcC5kc3QtaXRzLmRlMB4XDTE3MTIwNDE0MDEwOFoXDTE4MTIwNDE0MDEwOFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZHN0Y2ltYWFkMXAuZHN0LWl0cy5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALsFVa7OTiUSni1JNB+Tf2+CW733968ZYN1iHjjvs0kj0wuaBTnOf7XtKy4FaDSSQeKzTOAOwRFyPPOcS643y95dyx6miXrb0dLXSwJ5L21ClIhsZ7hp5jrIIePdT\/eaRzyAJ4stqeuBUKAW0ydGunBeS0bjzxylV4I1408nb5iq7fyFWllA4CAkbMOQW79vdEmoZsXHTt2pEoH7GbUZEfEpmjMwD2CDYtv\/D9nr2igJzEC04FFZHLWhGpjv+hyeX+bFoXle7ay3pFN+cT1ZD8EoQ3pXn54ciXDuciAl6l87nsARJ4vS68QBCWDdq2H4hUFicvqQ3uQz1AbPiBU6XLUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPHCisZyPf\/fuuQEW5LyzZSYMwBRYVR6kk\/M2ZNx6TrUEwmOb10RQ3G97bLAshN44g5lWdPYz4EOt6d2o71etIjf79f+IR0MAjEgBB2HThaHcMU9KG229Ftcauie9XeurngMawTRu60YqH7+go8EMf6a1Kdnx37DMy\/1LRlsYJVfEoOCab3GgcIdXrRSYWqsY4SVJZiTPYdqz9vmNPSXXiDSOTl6qXHV\/f53WTS2V5aIQbuJJziXlceusuVNny0o5h+j6ovZ1HhEGAu3lpD+8kY8KUqA4kXMH3VNZqzHBYazJx\/QBB3bG45cZSOvV3gUOnGBgiv9NBWjhvmY0fC3J6Q=="]}]}`,
Iat: int64(1513165889) + 3601,
Err: errors.New("Token is expired"),
},
{
Name: "Valid Token with RS256 signature verified against empty JWKS document",
TokenString: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTMxNjU4ODksImV4cCI6MTUxMzE2OTQ4OSwiYXV0aF90aW1lIjoxNTEzMTY1ODg4LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.nK51Ui4XN45SVul9igNaKFQd-F63BNstBzW-T5LBVm_ANHCEHyP3_88C3ffkkQIi3PxYacRJGtfswP35ws7YJUcNp-GoGZARqz62NpMtbQyhos6mCaVXwPoxPbrZx4AkMQgxkZwJcOzceX7mpjcT3kCth30chN3lkhzSjGrXe4ZDOAV25liS-dsdBiqDiaTB91sS534GM76qJQxFUs51oSbYTRdCN1VJ0XopMcasfVDzFrtSbyvEIVXlpKK2HplnhheqF4QHrM_3cjV_NGRr3tYLe-AGTdDXKWlJD1GDz1ECXeMGQHPoz3U8cqNsFLYBstIlCgfnBWgWsPZSvJPJUg",
JwksDocument: "",
Iat: int64(1513165889),
Err: errors.New("unexpected end of JSON input"),
},
{
Name: "Invalid Token",
Err: errors.New("token contains an invalid number of segments"),
},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
// mock JWKS server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
io.WriteString(w, tt.JwksDocument)
}))
defer ts.Close()
j := oauth2.JWT{
Jwksurl: ts.URL,
Now: func() time.Time {
return time.Unix(tt.Iat, 0)
},
}
_, err := j.GetClaims(tt.TokenString)
if tt.Err != nil {
if err != nil {
if tt.Err.Error() != err.Error() {
t.Errorf("Error in test %s expected error: %v actual: %v", tt.Name, tt.Err, err)
} // else: that's what we expect
} else {
t.Errorf("Error in test %s expected error: %v actual: none", tt.Name, tt.Err)
}
} else {
if err != nil {
t.Errorf("Error in tt %s: %v", tt.Name, err)
} // else: that's what we expect
}
})
}
}

View File

@ -16,7 +16,7 @@ var _ Mux = &AuthMux{}
const TenMinutes = 10 * time.Minute
// NewAuthMux constructs a Mux handler that checks a cookie against the authenticator
func NewAuthMux(p Provider, a Authenticator, t Tokenizer, basepath string, l chronograf.Logger) *AuthMux {
func NewAuthMux(p Provider, a Authenticator, t Tokenizer, basepath string, l chronograf.Logger, UseIDToken bool) *AuthMux {
return &AuthMux{
Provider: p,
Auth: a,
@ -25,6 +25,7 @@ func NewAuthMux(p Provider, a Authenticator, t Tokenizer, basepath string, l chr
FailureURL: path.Join(basepath, "/login"),
Now: DefaultNowTime,
Logger: l,
UseIDToken: UseIDToken,
}
}
@ -41,6 +42,7 @@ type AuthMux struct {
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)
UseIDToken bool // UseIDToken enables OpenID id_token support
}
// Login uses a Cookie with a random string as the state validation method. JWTs are
@ -116,20 +118,61 @@ func (j *AuthMux) Callback() http.Handler {
return
}
// Using the token get the principal identifier from the provider
oauthClient := conf.Client(r.Context(), token)
id, err := j.Provider.PrincipalID(oauthClient)
if err != nil {
log.Error("Unable to get principal identifier ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
if token.Extra("id_token") != nil && !j.UseIDToken {
log.Info("found an extra id_token, but option --useidtoken is not set")
}
group, err := j.Provider.Group(oauthClient)
if err != nil {
log.Error("Unable to get OAuth Group", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
// if we received an extra id_token, inspect it
var id string
var group string
if j.UseIDToken && token.Extra("id_token") != nil && token.Extra("id_token") != "" {
log.Debug("found an extra id_token")
if provider, ok := j.Provider.(ExtendedProvider); ok {
log.Debug("provider implements PrincipalIDFromClaims()")
tokenString, ok := token.Extra("id_token").(string)
if !ok {
log.Error("cannot cast id_token as string")
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
claims, err := j.Tokens.GetClaims(tokenString)
if err != nil {
log.Error("parsing extra id_token failed:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
log.Debug("found claims: ", claims)
id, err = provider.PrincipalIDFromClaims(claims)
if err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
group, err = provider.GroupFromClaims(claims)
if err != nil {
log.Error("requested claim not found in id_token:", err)
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
} else {
log.Debug("provider does not implement PrincipalIDFromClaims()")
}
} else {
// otherwise perform an additional lookup
oauthClient := conf.Client(r.Context(), token)
// Using the token get the principal identifier from the provider
id, err = j.Provider.PrincipalID(oauthClient)
if err != nil {
log.Error("Unable to get principal identifier ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
group, err = j.Provider.Group(oauthClient)
if err != nil {
log.Error("Unable to get OAuth Group", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
}
p := Principal{

View File

@ -51,7 +51,9 @@ func setupMuxTest(response interface{}, selector func(*AuthMux) http.Handler) (*
Tokens: mt,
}
jm := NewAuthMux(mp, auth, mt, "", clog.New(clog.ParseLevel("debug")))
useidtoken := false
jm := NewAuthMux(mp, auth, mt, "", clog.New(clog.ParseLevel("debug")), useidtoken)
ts := httptest.NewServer(selector(jm))
jar, _ := cookiejar.New(nil)
hc := http.Client{
@ -177,3 +179,43 @@ func Test_AuthMux_Callback_SetsCookie(t *testing.T) {
t.Fatal("Expected cookie to be named", DefaultCookieName, "but was", c.Name)
}
}
func Test_AuthMux_Callback_HandlesIdToken(t *testing.T) {
// body taken from ADFS4
response := mockCallbackResponse{AccessToken: `eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJ1cm46bWljcm9zb2Z0OnVzZXJpbmZvIiwiaXNzIjoiaHR0cDovL2RzdGNpbWFhZDFwLmRzdC1pdHMuZGUvYWRmcy9zZXJ2aWNlcy90cnVzdCIsImlhdCI6MTUxNTcwMDU2NSwiZXhwIjoxNTE1NzA0MTY1LCJhcHB0eXBlIjoiQ29uZmlkZW50aWFsIiwiYXBwaWQiOiJjaHJvbm9ncmFmIiwiYXV0aG1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYXV0aF90aW1lIjoiMjAxOC0wMS0xMVQxOTo1MToyNS44MDZaIiwidmVyIjoiMS4wIiwic2NwIjoib3BlbmlkIiwic3ViIjoiZVlWM2pkbGROeUZZMXFGRkg0b0FkQnZERmZiVmZudUcyOUhpSGtTdWp3az0ifQ.sf1qJys9LMUp2S232IRK2aTXiPCE93O-cUdYQQz7kg2woyD46KLwwKIYJVqMaqLspTn3OmaIhKtgx5ZXyAEtihODB1GOBK7DBNRBYCS1iqY_v2-Qwjf7hgaNaCqBjs0DZJspfp5G9MTykvD1FOtQNjPOcBW-i2bblG9L9jlmMbOZ3F7wrZMrroTSkiSn_gRiw2SnN8K7w8WrMEXNK2_jg9ZJ7aSHeUSBwkRNFRds2QNho3HWHg-zcsZFdZ4UGSt-6Az_0LY3yENMLj5us5Rl6Qzk_Re2dhFrlnlXlY1v1DEp3icCvvjkv6AeZWjTfW4qETZaCXUKtSyZ7d5_V1CRDQ", "token_type": "bearer", "expires_in": 3600, "resource": "urn:microsoft:userinfo", "refresh_token": "X9ZGO4H1bMk2bFeOfpv18BzAuFBzUPKQNfOEfdp60FkAAQAALPEBfj23FPEzajle-hm4DrDXp8-Kj53OqoVGalyZeuR-lfJzxpQXQhRAXZOUTuuQ8AQycByh9AylQYDA0jdMFW4FL4WL_6JhNh2JrtXCv2HQ9ozbUq9F7u_O0cY7u0P2pfNujQfk3ckYn-CMVjXbuwJTve6bXUR0JDp5c195bAVA5eFWyI-2uh432t7viyaIjAVbWxQF4fvimcpF1Et9cGodZHVsrZzGxKRnzwjYkWHsqm9go4KOeSKN6MlcWbjvS1UdMjQXSvoqSI00JnSMC3hxJZFn5JcmAPB1AMnJf4VvXZ5b-aOnwdX09YT8KayWkWekAsuZqTAsFwhZPVCRGWAFAADy0e2fTe6l-U6Cj_2bWsq6Snm1QEpWHXuwOJKWZJH-9yQn8KK3KzRowSzRuACzEIpZS5skrqXs_-2aOaZibNpjCEVyw8fF8GTw3VRLufsSrMQ5pD0KL7TppTGFpaqgwIH1yq6T8aRY4DeyoJkNpnO9cw1wuqnY7oGF-J25sfZ4XNWhk6o5e9A45PXhTilClyDKDLqTfdoIsG1Koc2ywqTIb-XI_EbWR3e4ijy8Kmlehw1kU9_xAG0MmmD2HTyGHZCBRgrskYCcHd-UNgCMrNAb5dZQ8NwpKtEL46qIq4R0lheTRRK8sOWzzuJXmvDEoJiIxqSR3Ma4MOISi-vsIsAuiEL9G1aMOkDRj-kDVmqrdKRAwYnN78AWY5EFfkQJyVBbiG882wBh9S0q3HUUCxzFerOvl4eDlVn6m18rRMz7CVZYBBltGtHRhEOQ4gumICR5JRrXAC50aBmUlhDiiMdbEIwJrvWrkhKE0oAJznqC7gleP0E4EOEh9r6CEGZ7Oj8X9Cdzjbuq2G1JGBm_yUvkhAcV61DjOiIQl35BpOfshveNZf_caUtNMa2i07BBmezve17-2kWGzRunr1BD1vMTz41z-H62fy4McR47WJjdDJnuy4DH5AZYQ6ooVxWCtEqeqRPYpzO0XdOdJGXFqXs9JzDKVXTgnHU443hZBC5H-BJkZDuuJ_ZWNKXf03JhouWkxXcdaMbuaQYOZJsUySVyJ5X4usrBFjW4udZAzy7mua-nJncbvcwoyVXiFlRfZiySXolQ9865N7XUnEk_2PijMLoVDATDbA09XuRySvngNsdsQ27p21dPxChXdtpD5ofNqKJ2FBzFKmxCkuX7L01N1nDpWQTuxhHF0JfxSKG5m3jcTx8Bd7Un94mTuAB7RuglDqkdQB9o4X9NHNGSdqGQaK-xeKoNCFWevk3VZoDoY9w2NqSNV2VIuqhy7SxtDSMjZKC5kiQi5EfGeTYZAvTwMYwaXb7K4WWtscy_ZE15EOCVeYi0hM1Ma8iFFTANkSRyX83Ju4SRphxRKnpKcJ2pPYH784I5HOm5sclhUL3aLeAA161QgxRBSa9YVIZfyXHyWQTcbNucNdhmdUZnKfRv1xtXcS9VAx2yAkoKFehZivEINX0Y500-WZ1eT_RXp0BfCKmJQ8Fu50oTaI-c5h2Q3Gp_LTSODNnMrjJiJxCLD_LD1fd1e8jTYDV3NroGlpWTuTdjMUm-Z1SMXaaJzQGEnNT6F8b6un9228L6YrDC_3MJ5J80VAHL5EO1GesdEWblugCL7AQDtFjNXq0lK8Aoo8X9_hlvDwgfdR16l8QALPT1HJVzlHPG8G3dRe50TKZnl3obU0WXN1KYG1EC4Qa3LyaVCIuGJYOeFqjMINrf7PoM368nS9yhrY08nnoHZbQ7IeA1KsNq2kANeH1doCNfWrXDwn8KxjYxZPEnzvlQ5M1RIzArOqzWL8NbftW1q2yCZZ4RVg0vOTVXsqWFnQIvWK-mkELa7bvByFzbtVHOJpc_2EKBKBNv6IYUENRCu2TOf6w7u42yvng7ccoXRTiUFUlKgVmswf9FzISxFd-YKgrzp3bMhC3gReGqcJuqEwnXPvOAY_BAkVMSd_ZaCFuyclRjFvUxrAg1T_cqOvRIlJ2Qq7z4u7W3BAo9BtFdj8QNLKJXtvvzXTprglRPDNP_QEPAkwZ_Uxa13vdYFcG18WCx4GbWQXchl5B7DnISobcdCH34M-I0xDZN98VWQVmLAfPniDUD30C8pfiYF7tW_EVy958Eg_JWVy0SstYEhV-y-adrJ1Oimjv0ptsWv-yErKBUD14aex9A_QqdnTXZUg.tqMb72eWAkAIvInuLp57NDyGxfYvms3NnhN-mllkYb7Xpd8gVbQFc2mYdzOOhtnfGuakyXYF4rZdJonQwzBO6C9KYuARciUU1Ms4bWPC-aeNO5t-aO_bDZbwC9qMPmq5ZuxG633BARGaw26fr0Z7qhcJMiou_EuaIehYTKkPB-mxtRAhxxyX91qqe0-PJnCHWoxizC4hDCUwp9Jb54tNf34BG3vtkXFX-kUARNfGucgKUkh6RYkhWiMBsMVoyWmkFXB5fYxmCAH5c5wDW6srKdyIDEWZInliuKbYR0p66vg1FfoSi4bBfrsm5NtCtLKG9V6Q0FEIA6tRRgHmKUGpkw", "refresh_token_expires_in": 28519, "scope": "openid", "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSIsImtpZCI6IllEQlVocWRXa3NLWGRHdVgwc3l0amFVdXhoQSJ9.eyJhdWQiOiJjaHJvbm9ncmFmIiwiaXNzIjoiaHR0cHM6Ly9kc3RjaW1hYWQxcC5kc3QtaXRzLmRlL2FkZnMiLCJpYXQiOjE1MTU3MDA1NjUsImV4cCI6MTUxNTcwNDE2NSwiYXV0aF90aW1lIjoxNTE1NzAwMjg1LCJzdWIiOiJlWVYzamRsZE55RlkxcUZGSDRvQWRCdkRGZmJWZm51RzI5SGlIa1N1andrPSIsInVwbiI6ImJzY0Bkc3QtaXRzLmRlIiwidW5pcXVlX25hbWUiOiJEU1RcXGJzYyIsInNpZCI6IlMtMS01LTIxLTI1MDUxNTEzOTgtMjY2MTAyODEwOS0zNzU0MjY1ODIwLTExMDQifQ.XD873K6NVRTJY1700NsflLJGZKFHJfNBjB81SlADVdAHbhnq7wkAZbGEEm8wFqvTKKysUl9EALzmDa2tR9nzohVvmHftIYBO0E-wPBzdzWWX0coEgpVAc-SysP-eIQWLsj8EaodaMkCgKO0FbTWOf4GaGIBZGklrr9EEk8VRSdbXbm6Sv9WVphezEzxq6JJBRBlCVibCnZjR5OYh1Vw_7E7P38ESPbpLY3hYYl2hz4y6dQJqCwGr7YP8KrDlYtbosZYgT7ayxokEJI1udEbX5PbAq5G6mj5rLfSOl85rMg-psZiivoM8dn9lEl2P7oT8rAvMWvQp-FIRQQHwqf9cxw`}
hc, ts, prov := setupMuxTest(response, func(j *AuthMux) http.Handler {
return j.Callback()
})
defer teardownMuxTest(hc, ts, prov)
tsURL, _ := url.Parse(ts.URL)
v := url.Values{
"code": {"4815162342"},
"state": {"foobar"},
}
tsURL.RawQuery = v.Encode()
resp, err := hc.Get(tsURL.String())
if err != nil {
t.Fatal("Error communicating with Callback() handler: err", err)
}
// Ensure we were redirected
if resp.StatusCode < 300 || resp.StatusCode >= 400 {
t.Fatal("Expected to be redirected, but received status code", resp.StatusCode)
}
// Check that cookie was set
cookies := resp.Cookies()
if count := len(cookies); count != 1 {
t.Fatal("Expected exactly one cookie to be set but found", count)
}
c := cookies[0]
if c.Name != DefaultCookieName {
t.Fatal("Expected cookie to be named", DefaultCookieName, "but was", c.Name)
}
}

View File

@ -6,6 +6,7 @@ import (
"net/http"
"time"
gojwt "github.com/dgrijalva/jwt-go"
"golang.org/x/oauth2"
)
@ -95,4 +96,6 @@ type Tokenizer interface {
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)
// GetClaims returns a map with verified claims
GetClaims(tokenString string) (gojwt.MapClaims, error)
}

View File

@ -5,10 +5,12 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"time"
goauth "golang.org/x/oauth2"
gojwt "github.com/dgrijalva/jwt-go"
"github.com/influxdata/chronograf"
)
@ -45,6 +47,20 @@ func (mp *MockProvider) PrincipalID(provider *http.Client) (string, error) {
return mp.Email, nil
}
func (mp *MockProvider) PrincipalIDFromClaims(claims gojwt.MapClaims) (string, error) {
return mp.Email, nil
}
func (mp *MockProvider) GroupFromClaims(claims gojwt.MapClaims) (string, error) {
email := strings.Split(mp.Email, "@")
if len(email) != 2 {
//g.Logger.Error("malformed email address, expected %q to contain @ symbol", id)
return "DEFAULT", nil
}
return email[1], nil
}
func (mp *MockProvider) Group(provider *http.Client) (string, error) {
return mp.Orgs, nil
}
@ -76,6 +92,10 @@ func (y *YesManTokenizer) ExtendedPrincipal(ctx context.Context, p Principal, ex
return p, nil
}
func (y *YesManTokenizer) GetClaims(tokenString string) (gojwt.MapClaims, error) {
return gojwt.MapClaims{}, nil
}
func NewTestTripper(log chronograf.Logger, ts *httptest.Server, rt http.RoundTripper) (*TestTripper, error) {
url, err := url.Parse(ts.URL)
if err != nil {

View File

@ -57,6 +57,8 @@ type Server struct {
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
ResourcesPath string `long:"resources-path" description:"Path to directory of pre-canned dashboards, sources, kapacitors, and organizations (/usr/share/chronograf/resources)" env:"RESOURCES_PATH" default:"canned"`
TokenSecret string `short:"t" long:"token-secret" description:"Secret to sign tokens" env:"TOKEN_SECRET"`
JwksURL string `long:"jwks-url" description:"URL that returns OpenID Key Discovery JWKS document." env:"JWKS_URL"`
UseIDToken bool `long:"use-id-token" description:"Enable id_token processing." env:"USE_ID_TOKEN"`
AuthDuration time.Duration `long:"auth-duration" default:"720h" description:"Total duration of cookie life for authentication (in hours). 0 means authentication expires on browser close." env:"AUTH_DURATION"`
GithubClientID string `short:"i" long:"github-client-id" description:"Github Client ID for OAuth 2 support" env:"GH_CLIENT_ID"`
@ -144,8 +146,8 @@ func (s *Server) githubOAuth(logger chronograf.Logger, auth oauth2.Authenticator
Orgs: s.GithubOrgs,
Logger: logger,
}
jwt := oauth2.NewJWT(s.TokenSecret)
ghMux := oauth2.NewAuthMux(&gh, auth, jwt, s.Basepath, logger)
jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
ghMux := oauth2.NewAuthMux(&gh, auth, jwt, s.Basepath, logger, s.UseIDToken)
return &gh, ghMux, s.UseGithub
}
@ -158,8 +160,8 @@ func (s *Server) googleOAuth(logger chronograf.Logger, auth oauth2.Authenticator
RedirectURL: redirectURL,
Logger: logger,
}
jwt := oauth2.NewJWT(s.TokenSecret)
goMux := oauth2.NewAuthMux(&google, auth, jwt, s.Basepath, logger)
jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
goMux := oauth2.NewAuthMux(&google, auth, jwt, s.Basepath, logger, s.UseIDToken)
return &google, goMux, s.UseGoogle
}
@ -170,8 +172,8 @@ func (s *Server) herokuOAuth(logger chronograf.Logger, auth oauth2.Authenticator
Organizations: s.HerokuOrganizations,
Logger: logger,
}
jwt := oauth2.NewJWT(s.TokenSecret)
hMux := oauth2.NewAuthMux(&heroku, auth, jwt, s.Basepath, logger)
jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
hMux := oauth2.NewAuthMux(&heroku, auth, jwt, s.Basepath, logger, s.UseIDToken)
return &heroku, hMux, s.UseHeroku
}
@ -189,8 +191,8 @@ func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticato
APIKey: s.GenericAPIKey,
Logger: logger,
}
jwt := oauth2.NewJWT(s.TokenSecret)
genMux := oauth2.NewAuthMux(&gen, auth, jwt, s.Basepath, logger)
jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
genMux := oauth2.NewAuthMux(&gen, auth, jwt, s.Basepath, logger, s.UseIDToken)
return &gen, genMux, s.UseGenericOAuth2
}
@ -205,8 +207,8 @@ func (s *Server) auth0OAuth(logger chronograf.Logger, auth oauth2.Authenticator)
auth0, err := oauth2.NewAuth0(s.Auth0Domain, s.Auth0ClientID, s.Auth0ClientSecret, redirectURL.String(), s.Auth0Organizations, logger)
jwt := oauth2.NewJWT(s.TokenSecret)
genMux := oauth2.NewAuthMux(&auth0, auth, jwt, s.Basepath, logger)
jwt := oauth2.NewJWT(s.TokenSecret, s.JwksURL)
genMux := oauth2.NewAuthMux(&auth0, auth, jwt, s.Basepath, logger, s.UseIDToken)
if err != nil {
logger.Error("Error parsing Auth0 domain: err:", err)

View File

@ -1,6 +1,5 @@
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import Tags from 'shared/components/Tags'
import SlideToggle from 'shared/components/SlideToggle'
@ -38,7 +37,9 @@ const AllUsersTableRow = ({
name: organizations.find(o => r.organization === o.id).name,
}))
const wrappedDelete = _.curry(onDelete, user)
const wrappedDelete = () => {
onDelete(user)
}
const removeWarning = userIsMe
? 'Delete your user record\nand log yourself out?'

View File

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import Dropdown from 'shared/components/Dropdown'
import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
import ConfirmButton from 'shared/components/ConfirmButton'
import {USER_ROLES} from 'src/admin/constants/chronografAdmin'
import {USERS_TABLE} from 'src/admin/constants/chronografTableSizing'
@ -26,6 +26,12 @@ const UsersTableRow = ({
const userIsMe = user.id === meID
const wrappedDelete = () => {
onDelete(user)
}
const removeWarning = 'Remove this user\nfrom Current Org?'
return (
<tr className={'chronograf-admin-table--user'}>
<td>
@ -52,12 +58,13 @@ const UsersTableRow = ({
</td>
<td style={{width: colProvider}}>{user.provider}</td>
<td style={{width: colScheme}}>{user.scheme}</td>
<DeleteConfirmTableCell
<ConfirmButton
confirmText={removeWarning}
confirmAction={wrappedDelete}
size="btn-xs"
type="btn-danger"
text="Remove"
onDelete={onDelete}
item={user}
buttonSize="btn-xs"
disabled={userIsMe}
customClass="table--show-on-row-hover"
/>
</tr>
)

View File

@ -28,6 +28,8 @@ import {
hideCellEditorOverlay,
} from 'src/dashboards/actions/cellEditorOverlay'
import {dismissEditingAnnotation} from 'src/shared/actions/annotations'
import {
setAutoRefresh,
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
@ -78,10 +80,8 @@ class DashboardPage extends Component {
timeRange,
} = this.props
getAnnotationsAsync(
source.links.annotations,
Date.now() - timeRange.seconds * 1000
)
const annotationRange = this.millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
window.addEventListener('resize', this.handleWindowResize, true)
const dashboards = await getDashboardsAsync()
@ -107,8 +107,9 @@ class DashboardPage extends Component {
this.setState({windowHeight: window.innerHeight})
}
componentWillUnMount() {
componentWillUnmount() {
window.removeEventListener('resize', this.handleWindowResize, true)
this.props.handleDismissEditingAnnotation()
}
inView = cell => {
@ -159,10 +160,25 @@ class DashboardPage extends Component {
...timeRange,
format: FORMAT_INFLUXQL,
})
getAnnotationsAsync(
source.links.annotations,
Date.now() - timeRange.seconds * 1000
)
const annotationRange = this.millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
}
millisecondTimeRange({seconds, lower, upper}) {
// Is this a relative time range?
if (seconds) {
return {
since: Date.now() - seconds * 1000,
until: null,
}
}
// No, this is an absolute (custom) time range
return {
since: Date.parse(lower),
until: Date.parse(upper),
}
}
handleUpdatePosition = cells => {
@ -511,6 +527,7 @@ DashboardPage.propTypes = {
getAnnotationsAsync: func.isRequired,
handleShowCellEditorOverlay: func.isRequired,
handleHideCellEditorOverlay: func.isRequired,
handleDismissEditingAnnotation: func.isRequired,
selectedCell: shape({}),
thresholdsListType: string.isRequired,
thresholdsListColors: arrayOf(shape({}).isRequired).isRequired,
@ -587,6 +604,10 @@ const mapDispatchToProps = dispatch => ({
hideCellEditorOverlay,
dispatch
),
handleDismissEditingAnnotation: bindActionCreators(
dismissEditingAnnotation,
dispatch
),
})
export default connect(mapStateToProps, mapDispatchToProps)(

View File

@ -63,8 +63,11 @@ export const addAnnotationAsync = (createUrl, annotation) => async dispatch => {
dispatch(deleteAnnotation(annotation))
}
export const getAnnotationsAsync = (indexUrl, since) => async dispatch => {
const annotations = await api.getAnnotations(indexUrl, since)
export const getAnnotationsAsync = (
indexUrl,
{since, until}
) => async dispatch => {
const annotations = await api.getAnnotations(indexUrl, since, until)
dispatch(loadAnnotations(annotations))
}

View File

@ -19,11 +19,11 @@ export const createAnnotation = async (url, annotation) => {
return annoToMillisecond(response.data)
}
export const getAnnotations = async (url, since) => {
export const getAnnotations = async (url, since, until) => {
const {data} = await AJAX({
method: 'GET',
url,
params: {since: msToRFC(since)},
params: {since: msToRFC(since), until: msToRFC(until)},
})
return data.annotations.map(annoToMillisecond)
}

View File

@ -10,7 +10,7 @@ import * as actions from 'shared/actions/annotations'
const TimeStamp = ({time}) => (
<div className="annotation-tooltip--timestamp">
{`${moment(+time).format('YYYY/DD/MM HH:mm:ss.SS')}`}
{`${moment(+time).format('YYYY/MM/DD HH:mm:ss.SS')}`}
</div>
)

View File

@ -96,16 +96,17 @@ class LayoutCellMenu extends Component {
/>
</div>
)}
{mode === 'editing' && (
<div className="dash-graph-context--buttons">
<div
className="btn btn-xs btn-success"
onClick={onDismissEditingAnnotation}
>
Done Editing
{mode === 'editing' &&
cellSupportsAnnotations(cell.type) && (
<div className="dash-graph-context--buttons">
<div
className="btn btn-xs btn-success"
onClick={onDismissEditingAnnotation}
>
Done Editing
</div>
</div>
</div>
)}
)}
</div>
)
}