Reorganize OAuth2 Logic

Created an oauth2 package which encapsulates all oauth2 providers,
utility functions, types, and interfaces. Previously some methods of the
Github provider were used as http.HandlerFuncs. These have now been
pulled into a concrete type called a JWTMux to implement other Oauth2
providers.

JWTMux has all of the functionality required to take a token from any
provider and store it as a JWT in a browser, and that is the extent of
its responsibilities. It implements the oauth2.Mux interface which would
potentially allow other strategies of oauth2 credential storage.
pull/922/head
Tim Raymond 2017-02-14 16:14:24 -05:00
parent f0e8d0b3e8
commit d07c7ca1d6
16 changed files with 371 additions and 391 deletions

View File

@ -3,7 +3,6 @@ package chronograf
import (
"context"
"net/http"
"time"
)
// General errors.
@ -16,7 +15,6 @@ const (
ErrUserNotFound = Error("user not found")
ErrLayoutInvalid = Error("layout is invalid")
ErrAlertNotFound = Error("alert not found")
ErrAuthentication = Error("user not authenticated")
)
// Error is a domain error encountered while processing chronograf requests
@ -310,25 +308,3 @@ type LayoutStore interface {
// Update the dashboard in the store.
Update(context.Context, Layout) error
}
// Principal is any entity that can be authenticated
type Principal string
// PrincipalKey is used to pass principal
// via context.Context to request-scoped
// functions.
const PrincipalKey Principal = "principal"
// Authenticator represents a service for authenticating users.
type Authenticator interface {
// Authenticate returns User associated with token if successful.
Authenticate(ctx context.Context, token string) (Principal, error)
// Token generates a valid token for Principal lasting a duration
Token(context.Context, Principal, time.Duration) (string, error)
}
// TokenExtractor extracts tokens from http requests
type TokenExtractor interface {
// Extract will return the token or an error.
Extract(r *http.Request) (string, error)
}

76
oauth2/auth.go Normal file
View File

@ -0,0 +1,76 @@
package oauth2
import (
"context"
"net/http"
"strings"
"github.com/influxdata/chronograf"
)
// CookieExtractor extracts the token from the value of the Name cookie.
type CookieExtractor struct {
Name string
}
// Extract returns the value of cookie Name
func (c *CookieExtractor) Extract(r *http.Request) (string, error) {
cookie, err := r.Cookie(c.Name)
if err != nil {
return "", ErrAuthentication
}
return cookie.Value, nil
}
// BearerExtractor extracts the token from Authorization: Bearer header.
type BearerExtractor struct{}
// Extract returns the string following Authorization: Bearer
func (b *BearerExtractor) Extract(r *http.Request) (string, error) {
s := r.Header.Get("Authorization")
if s == "" {
return "", ErrAuthentication
}
// Check for Bearer token.
strs := strings.Split(s, " ")
if len(strs) != 2 || strs[0] != "Bearer" {
return "", ErrAuthentication
}
return strs[1], nil
}
// AuthorizedToken extracts the token and validates; if valid the next handler
// will be run. The principal will be sent to the next handler via the request's
// Context. It is up to the next handler to determine if the principal has access.
// On failure, will return http.StatusUnauthorized.
func AuthorizedToken(auth Authenticator, te TokenExtractor, logger chronograf.Logger, next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
token, err := te.Extract(r)
if err != nil {
log.Error("Unable to extract token")
w.WriteHeader(http.StatusUnauthorized)
return
}
// We do not check the validity of the principal. Those
// server further down the chain should do so.
principal, err := auth.Authenticate(r.Context(), token)
if err != nil {
log.Error("Invalid token")
w.WriteHeader(http.StatusUnauthorized)
return
}
// Send the principal to the next handler
ctx := context.WithValue(r.Context(), PrincipalKey, principal)
next.ServeHTTP(w, r.WithContext(ctx))
return
})
}

View File

@ -1,4 +1,4 @@
package server_test
package oauth2_test
import (
"context"
@ -28,7 +28,7 @@ func TestCookieExtractor(t *testing.T) {
Value: "reallyimportant",
Lookup: "Doesntexist",
Expected: "",
Err: chronograf.ErrAuthentication,
Err: ErrAuthentication,
},
{
Desc: "Cookie token extracted",
@ -46,7 +46,7 @@ func TestCookieExtractor(t *testing.T) {
Value: test.Value,
})
var e chronograf.TokenExtractor = &server.CookieExtractor{
var e TokenExtractor = &server.CookieExtractor{
Name: test.Lookup,
}
actual, err := e.Extract(req)
@ -74,21 +74,21 @@ func TestBearerExtractor(t *testing.T) {
Header: "Doesntexist",
Value: "reallyimportant",
Expected: "",
Err: chronograf.ErrAuthentication,
Err: ErrAuthentication,
},
{
Desc: "Auth header doesn't have Bearer",
Header: "Authorization",
Value: "Bad Value",
Expected: "",
Err: chronograf.ErrAuthentication,
Err: ErrAuthentication,
},
{
Desc: "Auth header doesn't have Bearer token",
Header: "Authorization",
Value: "Bearer",
Expected: "",
Err: chronograf.ErrAuthentication,
Err: ErrAuthentication,
},
{
Desc: "Authorization Bearer token success",
@ -102,7 +102,7 @@ func TestBearerExtractor(t *testing.T) {
req, _ := http.NewRequest("", "http://howdy.com", nil)
req.Header.Add(test.Header, test.Value)
var e chronograf.TokenExtractor = &server.BearerExtractor{}
var e TokenExtractor = &server.BearerExtractor{}
actual, err := e.Extract(req)
if err != test.Err {
t.Errorf("Bearer extract error; expected %v actual %v", test.Err, err)
@ -123,15 +123,15 @@ func (m *MockExtractor) Extract(*http.Request) (string, error) {
}
type MockAuthenticator struct {
Principal chronograf.Principal
Principal Principal
Err error
}
func (m *MockAuthenticator) Authenticate(context.Context, string) (chronograf.Principal, error) {
func (m *MockAuthenticator) Authenticate(context.Context, string) (Principal, error) {
return m.Principal, m.Err
}
func (m *MockAuthenticator) Token(context.Context, chronograf.Principal, time.Duration) (string, error) {
func (m *MockAuthenticator) Token(context.Context, Principal, time.Duration) (string, error) {
return "", m.Err
}
@ -139,7 +139,7 @@ func TestAuthorizedToken(t *testing.T) {
var tests = []struct {
Desc string
Code int
Principal chronograf.Principal
Principal Principal
ExtractorErr error
AuthErr error
Expected string
@ -164,10 +164,10 @@ func TestAuthorizedToken(t *testing.T) {
for _, test := range tests {
// next is a sentinel StatusOK and
// principal recorder.
var principal chronograf.Principal
var principal Principal
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
principal = r.Context().Value(chronograf.PrincipalKey).(chronograf.Principal)
principal = r.Context().Value(PrincipalKey).(Principal)
})
req, _ := http.NewRequest("GET", "", nil)
w := httptest.NewRecorder()

32
oauth2/doc.go Normal file
View File

@ -0,0 +1,32 @@
// ┌─────────┐ ┌───────────┐ ┌────────┐
// │ Browser │ │Chronograf │ │Provider│
// └─────────┘ └───────────┘ └────────┘
// │ │ │
// ├─────── GET /auth ─────────▶ │
// │ │ │
// │ │ │
// ◀ ─ ─ ─302 to Provider ─ ─ ┤ │
// │ │ │
// │ │ │
// ├──────────────── GET /auth w/ callback ─────────────────────▶
// │ │ │
// │ │ │
// ◀─ ─ ─ ─ ─ ─ ─ 302 to Chronograf Callback ─ ─ ─ ─ ─ ─ ─ ─ ┤
// │ │ │
// │ Code and State from │ │
// │ Provider │ │
// ├───────────────────────────▶ Request token w/ code & │
// │ │ state │
// │ ├────────────────────────────────▶
// │ │ │
// │ │ Response with │
// │ │ Token │
// │ Set cookie, Redirect │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
// │ to / │ │
// ◀───────────────────────────┤ │
// │ │ │
// │ │ │
// │ │ │
// │ │ │
package oauth2

View File

@ -1,4 +1,4 @@
package server
package oauth2
import (
"crypto/rand"
@ -6,7 +6,6 @@ import (
"errors"
"io"
"net/http"
"time"
"github.com/google/go-github/github"
"github.com/influxdata/chronograf"
@ -14,34 +13,13 @@ import (
ogh "golang.org/x/oauth2/github"
)
const (
// DefaultCookieName is the name of the stored cookie
DefaultCookieName = "session"
// DefaultCookieDuration is the length of time the cookie is valid
DefaultCookieDuration = time.Hour * 24 * 30
)
// Cookie represents the location and expiration time of new cookies.
type Cookie struct {
Name string
Duration time.Duration
}
// NewCookie creates a Cookie with DefaultCookieName and DefaultCookieDuration
func NewCookie() Cookie {
return Cookie{
Name: DefaultCookieName,
Duration: DefaultCookieDuration,
}
}
var _ OAuth2Provider = &Github{}
var _ Provider = &Github{}
// Github provides OAuth Login and Callback server. Callback will set
// an authentication cookie. This cookie's value is a JWT containing
// the user's primary Github email address.
type Github struct {
Auth chronograf.Authenticator
Auth Authenticator
ClientID string
ClientSecret string
Orgs []string // Optional github organization checking
@ -69,7 +47,7 @@ func (g *Github) Scopes() []string {
}
// NewGithub constructs a Github with default scopes.
func NewGithub(clientID, clientSecret string, orgs []string, auth chronograf.Authenticator, log chronograf.Logger) Github {
func NewGithub(clientID, clientSecret string, orgs []string, auth Authenticator, log chronograf.Logger) Github {
scopes := []string{"user:email"}
if len(orgs) > 0 {
scopes = append(scopes, "read:org")

View File

@ -1,7 +1,7 @@
package server
package oauth2
type Google struct {
OAuth2Provider
Provider
Domains []string // Optional google email domain checking
}

View File

@ -1,4 +1,4 @@
package jwt
package oauth2
import (
"context"
@ -6,11 +6,10 @@ import (
"time"
gojwt "github.com/dgrijalva/jwt-go"
"github.com/influxdata/chronograf"
)
// Test if JWT implements Authenticator
var _ chronograf.Authenticator = &JWT{}
var _ Authenticator = &JWT{}
// JWT represents a javascript web token that can be validated or marshaled into string.
type JWT struct {
@ -45,7 +44,7 @@ func (c *Claims) Valid() error {
}
// Authenticate checks if the jwtToken is signed correctly and validates with Claims.
func (j *JWT) Authenticate(ctx context.Context, jwtToken string) (chronograf.Principal, error) {
func (j *JWT) Authenticate(ctx context.Context, jwtToken string) (Principal, error) {
gojwt.TimeFunc = j.Now
// Check for expected signing method.
@ -72,11 +71,11 @@ func (j *JWT) Authenticate(ctx context.Context, jwtToken string) (chronograf.Pri
return "", fmt.Errorf("unable to convert claims to standard claims")
}
return chronograf.Principal(claims.Subject), nil
return Principal(claims.Subject), nil
}
// Token creates a signed JWT token from user that expires at Now + duration
func (j *JWT) Token(ctx context.Context, user chronograf.Principal, duration time.Duration) (string, error) {
func (j *JWT) Token(ctx context.Context, user Principal, duration time.Duration) (string, error) {
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
now := j.Now().UTC()

View File

@ -1,4 +1,4 @@
package jwt_test
package oauth2_test
import (
"context"
@ -15,7 +15,7 @@ func TestAuthenticate(t *testing.T) {
Desc string
Secret string
Token string
User chronograf.Principal
User Principal
Err error
}{
{
@ -83,7 +83,7 @@ func TestToken(t *testing.T) {
return time.Unix(-446774400, 0)
},
}
if token, err := j.Token(context.Background(), chronograf.Principal("/chronograf/v1/users/1"), duration); err != nil {
if token, err := j.Token(context.Background(), Principal("/chronograf/v1/users/1"), duration); err != nil {
t.Errorf("Error creating token for user: %v", err)
} else if token != expected {
t.Errorf("Error creating token; expected: %s actual: %s", "", token)

160
oauth2/mux.go Normal file
View File

@ -0,0 +1,160 @@
package oauth2
import (
"net/http"
"time"
"github.com/influxdata/chronograf"
"golang.org/x/oauth2"
)
const (
// DefaultCookieName is the name of the stored cookie
DefaultCookieName = "session"
// DefaultCookieDuration is the length of time the cookie is valid
DefaultCookieDuration = time.Hour * 24 * 30
)
// Cookie represents the location and expiration time of new cookies.
type cookie struct {
Name string
Duration time.Duration
}
// Check to ensure JWTMux is an oauth2.Mux
var _ Mux = &JWTMux{}
func NewJWTMux(p Provider, a Authenticator, l chronograf.Logger) *JWTMux {
return &JWTMux{
Provider: p,
Auth: a,
Logger: l,
SuccessURL: "/",
FailureURL: "/login",
Now: time.Now,
cookie: cookie{
Name: DefaultCookieName,
Duration: DefaultCookieDuration,
},
}
}
// JWTMux services an Oauth2 interaction with a provider and browser and stores
// the resultant token in the user's browser as a cookie encoded as a JWT. The
// benefit of this is that the JWT's authenticity can be verified independently
// by any Chronograf instance.
type JWTMux struct {
Provider Provider
Auth Authenticator
cookie cookie
Logger chronograf.Logger
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
}
// Uses JWT with a random string as the state validation method.
// JWTs are used because they can be validated without storing state.
func (j *JWTMux) Login() http.Handler {
conf := j.Provider.Config()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// We are creating a token with an encoded random string to prevent CSRF attacks
// This token will be validated during the OAuth callback.
// We'll give our users 10 minutes from this point to type in their github password.
// If the callback is not received within 10 minutes, then authorization will fail.
csrf := randomString(32) // 32 is not important... just long
state, err := j.Auth.Token(r.Context(), Principal(csrf), 10*time.Minute)
// This is likely an internal server error
if err != nil {
j.Logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL).
Error("Internal authentication error: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
url := conf.AuthCodeURL(state, oauth2.AccessTypeOnline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
})
}
// Callback is used by OAuth2 provider after authorization is granted. If
// granted, Callback will set a cookie with a month-long expiration. The
// value of the cookie is a JWT because the JWT can be validated without
// the need for saving state. The JWT contains the principal's identifier (e.g.
// email address).
func (j *JWTMux) Callback() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := j.Logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
state := r.FormValue("state")
// Check if the OAuth state token is valid to prevent CSRF
_, err := j.Auth.Authenticate(r.Context(), state)
if err != nil {
log.Error("Invalid OAuth state received: ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
// Exchange the code back with the provider to the the token
conf := j.Provider.Config()
code := r.FormValue("code")
token, err := conf.Exchange(r.Context(), code)
if err != nil {
log.Error("Unable to exchange code for token ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
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
}
// We create an auth token that will be used by all other endpoints to validate the principal has a claim
authToken, err := j.Auth.Token(r.Context(), Principal(id), j.cookie.Duration)
if err != nil {
log.Error("Unable to create cookie auth token ", err.Error())
http.Redirect(w, r, j.FailureURL, http.StatusTemporaryRedirect)
return
}
expireCookie := j.Now().UTC().Add(j.cookie.Duration)
cookie := http.Cookie{
Name: j.cookie.Name,
Value: authToken,
Expires: expireCookie,
HttpOnly: true,
Path: "/",
}
log.Info("User ", id, " is authenticated")
http.SetCookie(w, &cookie)
http.Redirect(w, r, j.SuccessURL, http.StatusTemporaryRedirect)
})
} // Login returns a handler that redirects to the providers OAuth login.
// Logout handler will expire our authentication cookie and redirect to the successURL
func (j *JWTMux) Logout() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
deleteCookie := http.Cookie{
Name: j.cookie.Name,
Value: "none",
Expires: j.Now().UTC().Add(-1 * time.Hour),
HttpOnly: true,
Path: "/",
}
http.SetCookie(w, &deleteCookie)
http.Redirect(w, r, j.SuccessURL, http.StatusTemporaryRedirect)
})
}

65
oauth2/oauth2.go Normal file
View File

@ -0,0 +1,65 @@
package oauth2
import (
"context"
"errors"
"net/http"
"time"
"golang.org/x/oauth2"
)
/* Constants */
const (
// PrincipalKey is used to pass principal
// via context.Context to request-scoped
// functions.
PrincipalKey Principal = "principal"
)
var (
/* Errors */
ErrAuthentication = errors.New("user not authenticated")
)
/* Types */
// Principal is any entity that can be authenticated
type Principal string
/* Interfaces */
// Provider are the common parameters for all providers (RFC 6749)
type Provider interface {
// ID is issued to the registered client by the authorization (RFC 6749 Section 2.2)
ID() string
// Secret associated is with the ID (Section 2.2)
Secret() string
// Scopes is used by the authorization server to "scope" responses (Section 3.3)
Scopes() []string
// Config is the OAuth2 configuration settings for this provider
Config() *oauth2.Config
// PrincipalID with fetch the identifier to be associated with the principal.
PrincipalID(provider *http.Client) (string, error)
}
// Mux is a collection of handlers responsible for servicing an Oauth2 interaction between a browser and a provider
type Mux interface {
Login() http.Handler
Logout() http.Handler
Callback() http.Handler
}
// Authenticator represents a service for authenticating users.
type Authenticator interface {
// Authenticate returns User associated with token if successful.
Authenticate(ctx context.Context, token string) (Principal, error)
// Token generates a valid token for Principal lasting a duration
Token(context.Context, Principal, time.Duration) (string, error)
}
// TokenExtractor extracts tokens from http requests
type TokenExtractor interface {
// Extract will return the token or an error.
Extract(r *http.Request) (string, error)
}

View File

@ -1,196 +0,0 @@
package server
import (
"context"
"net/http"
"strings"
"time"
"golang.org/x/oauth2"
"github.com/influxdata/chronograf"
)
// CookieExtractor extracts the token from the value of the Name cookie.
type CookieExtractor struct {
Name string
}
// Extract returns the value of cookie Name
func (c *CookieExtractor) Extract(r *http.Request) (string, error) {
cookie, err := r.Cookie(c.Name)
if err != nil {
return "", chronograf.ErrAuthentication
}
return cookie.Value, nil
}
// BearerExtractor extracts the token from Authorization: Bearer header.
type BearerExtractor struct{}
// Extract returns the string following Authorization: Bearer
func (b *BearerExtractor) Extract(r *http.Request) (string, error) {
s := r.Header.Get("Authorization")
if s == "" {
return "", chronograf.ErrAuthentication
}
// Check for Bearer token.
strs := strings.Split(s, " ")
if len(strs) != 2 || strs[0] != "Bearer" {
return "", chronograf.ErrAuthentication
}
return strs[1], nil
}
// AuthorizedToken extracts the token and validates; if valid the next handler
// will be run. The principal will be sent to the next handler via the request's
// Context. It is up to the next handler to determine if the principal has access.
// On failure, will return http.StatusUnauthorized.
func AuthorizedToken(auth chronograf.Authenticator, te chronograf.TokenExtractor, logger chronograf.Logger, next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
token, err := te.Extract(r)
if err != nil {
log.Error("Unable to extract token")
w.WriteHeader(http.StatusUnauthorized)
return
}
// We do not check the validity of the principal. Those
// server further down the chain should do so.
principal, err := auth.Authenticate(r.Context(), token)
if err != nil {
log.Error("Invalid token")
w.WriteHeader(http.StatusUnauthorized)
return
}
// Send the principal to the next handler
ctx := context.WithValue(r.Context(), chronograf.PrincipalKey, principal)
next.ServeHTTP(w, r.WithContext(ctx))
return
})
}
// Login returns a handler that redirects to the providers OAuth login.
// Uses JWT with a random string as the state validation method.
// JWTs are used because they can be validated without storing state.
func Login(provider OAuth2Provider, auth chronograf.Authenticator, logger chronograf.Logger) http.HandlerFunc {
conf := provider.Config()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// We are creating a token with an encoded random string to prevent CSRF attacks
// This token will be validated during the OAuth callback.
// We'll give our users 10 minutes from this point to type in their github password.
// If the callback is not received within 10 minutes, then authorization will fail.
csrf := randomString(32) // 32 is not important... just long
state, err := auth.Token(r.Context(), chronograf.Principal(csrf), 10*time.Minute)
// This is likely an internal server error
if err != nil {
logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL).
Error("Internal authentication error: ", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
url := conf.AuthCodeURL(state, oauth2.AccessTypeOnline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
})
}
// Logout handler will expire our authentication cookie and redirect to the successURL
func Logout(cookie Cookie, successURL string, now func() time.Time) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
deleteCookie := http.Cookie{
Name: cookie.Name,
Value: "none",
Expires: now().UTC().Add(-1 * time.Hour),
HttpOnly: true,
Path: "/",
}
http.SetCookie(w, &deleteCookie)
http.Redirect(w, r, successURL, http.StatusTemporaryRedirect)
})
}
// CallbackOpts are the options for the Callback handler
type CallbackOpts struct {
Provider OAuth2Provider
Auth chronograf.Authenticator
Cookie Cookie
Logger chronograf.Logger
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
}
// Callback is used by OAuth2 provider after authorization is granted. If
// granted, Callback will set a cookie with a month-long expiration. The
// value of the cookie is a JWT because the JWT can be validated without
// the need for saving state. The JWT contains the principal's identifier (e.g.
// email address).
func Callback(opts CallbackOpts) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := opts.Logger.
WithField("component", "auth").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL)
state := r.FormValue("state")
// Check if the OAuth state token is valid to prevent CSRF
_, err := opts.Auth.Authenticate(r.Context(), state)
if err != nil {
log.Error("Invalid OAuth state received: ", err.Error())
http.Redirect(w, r, opts.FailureURL, http.StatusTemporaryRedirect)
return
}
// Exchange the code back with the provider to the the token
conf := opts.Provider.Config()
code := r.FormValue("code")
token, err := conf.Exchange(r.Context(), code)
if err != nil {
log.Error("Unable to exchange code for token ", err.Error())
http.Redirect(w, r, opts.FailureURL, http.StatusTemporaryRedirect)
return
}
// Using the token get the principal identifier from the provider
oauthClient := conf.Client(r.Context(), token)
id, err := opts.Provider.PrincipalID(oauthClient)
if err != nil {
log.Error("Unable to get principal identifier ", err.Error())
http.Redirect(w, r, opts.FailureURL, http.StatusTemporaryRedirect)
return
}
// We create an auth token that will be used by all other endpoints to validate the principal has a claim
authToken, err := opts.Auth.Token(r.Context(), chronograf.Principal(id), opts.Cookie.Duration)
if err != nil {
log.Error("Unable to create cookie auth token ", err.Error())
http.Redirect(w, r, opts.FailureURL, http.StatusTemporaryRedirect)
return
}
expireCookie := opts.Now().UTC().Add(opts.Cookie.Duration)
cookie := http.Cookie{
Name: opts.Cookie.Name,
Value: authToken,
Expires: expireCookie,
HttpOnly: true,
Path: "/",
}
log.Info("User ", id, " is authenticated")
http.SetCookie(w, &cookie)
http.Redirect(w, r, opts.SuccessURL, http.StatusTemporaryRedirect)
})
}

View File

@ -6,12 +6,11 @@ import (
"net/http"
"strconv"
"strings"
"time"
"github.com/NYTimes/gziphandler"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf" // When julienschmidt/httprouter v2 w/ context is out, switch
"github.com/influxdata/chronograf/jwt"
"github.com/influxdata/chronograf/oauth2"
)
const (
@ -133,9 +132,9 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
// AuthAPI adds the OAuth routes if auth is enabled.
func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
auth := jwt.NewJWT(opts.TokenSecret)
auth := oauth2.NewJWT(opts.TokenSecret)
gh := NewGithub(
gh := oauth2.NewGithub(
opts.GithubClientID,
opts.GithubClientSecret,
opts.GithubOrgs,
@ -143,20 +142,12 @@ func AuthAPI(opts MuxOpts, router *httprouter.Router) http.Handler {
opts.Logger,
)
callback := CallbackOpts{
Provider: &gh,
Auth: &auth,
Cookie: NewCookie(),
Logger: opts.Logger,
SuccessURL: "/",
FailureURL: "/login",
Now: time.Now,
}
router.GET("/oauth/github", gh.Login())
router.GET("/oauth/logout", gh.Logout())
router.GET("/oauth/github/callback", gh.Callback())
ghMux := oauth2.NewJWTMux(&gh, &auth, opts.Logger)
router.Handler("GET", "/oauth/github/login", ghMux.Login())
router.Handler("GET", "/oauth/github/logout", ghMux.Logout())
router.Handler("GET", "/oauth/github/callback", ghMux.Callback())
tokenMiddleware := AuthorizedToken(&auth, &CookieExtractor{Name: "session"}, opts.Logger, router)
tokenMiddleware := oauth2.AuthorizedToken(&auth, &oauth2.CookieExtractor{Name: "session"}, opts.Logger, router)
// Wrap the API with token validation middleware.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/chronograf/v1/") {

View File

@ -1,21 +0,0 @@
package server
import (
"net/http"
"golang.org/x/oauth2"
)
// OAuth2Provider are the common parameters for all providers (RFC 6749)
type OAuth2Provider interface {
// ID is issued to the registered client by the authorization (RFC 6749 Section 2.2)
ID() string
// Secret associated is with the ID (Section 2.2)
Secret() string
// Scopes is used by the authorization server to "scope" responses (Section 3.3)
Scopes() []string
// Config is the OAuth2 configuration settings for this provider
Config() *oauth2.Config
// PrincipalID with fetch the identifier to be associated with the principal.
PrincipalID(provider *http.Client) (string, error)
}

View File

@ -8,6 +8,7 @@ import (
"golang.org/x/net/context"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/oauth2"
)
type userLinks struct {
@ -135,7 +136,7 @@ func ValidUserRequest(s *chronograf.User) error {
}
func getEmail(ctx context.Context) (string, error) {
principal := ctx.Value(chronograf.PrincipalKey).(chronograf.Principal)
principal := ctx.Value(oauth2.PrincipalKey).(oauth2.Principal)
if principal == "" {
return "", fmt.Errorf("Token not found")
}

View File

@ -1,12 +1,6 @@
package uuid
import (
"context"
"time"
"github.com/influxdata/chronograf"
uuid "github.com/satori/go.uuid"
)
import uuid "github.com/satori/go.uuid"
// V4 implements chronograf.ID
type V4 struct{}
@ -15,30 +9,3 @@ type V4 struct{}
func (i *V4) Generate() (string, error) {
return uuid.NewV4().String(), nil
}
// APIKey implements chronograf.Authenticator using V4
type APIKey struct {
Key string
}
// NewAPIKey creates an APIKey with a UUID v4 Key
func NewAPIKey() chronograf.Authenticator {
v4 := V4{}
key, _ := v4.Generate()
return &APIKey{
Key: key,
}
}
// Authenticate checks the key against the UUID v4 key
func (k *APIKey) Authenticate(ctx context.Context, key string) (chronograf.Principal, error) {
if key != k.Key {
return "", chronograf.ErrAuthentication
}
return "admin", nil
}
// Token returns the UUID v4 key
func (k *APIKey) Token(context.Context, chronograf.Principal, time.Duration) (string, error) {
return k.Key, nil
}

View File

@ -1,48 +0,0 @@
package uuid_test
import (
"context"
"testing"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/uuid"
)
func TestAuthenticate(t *testing.T) {
var tests = []struct {
Desc string
APIKey string
Key string
Err error
User chronograf.Principal
}{
{
Desc: "Test auth err when keys are different",
APIKey: "key",
Key: "badkey",
Err: chronograf.ErrAuthentication,
User: "",
},
{
Desc: "Test that admin user comes back",
APIKey: "key",
Key: "key",
Err: nil,
User: "admin",
},
}
for _, test := range tests {
k := uuid.APIKey{
Key: test.APIKey,
}
u, err := k.Authenticate(context.Background(), test.Key)
if err != test.Err {
t.Errorf("Auth error different; expected %v actual %v", test.Err, err)
}
if u != test.User {
t.Errorf("Auth user different; expected %v actual %v", test.User, u)
}
}
}