Clean up OAuth and finish tests.

- Fix #25
This commit is contained in:
Aaron
2015-03-13 02:15:58 -07:00
parent 538adcf188
commit 082caf88b3
6 changed files with 258 additions and 56 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ const (
// SessionLastAction is the session key to retrieve the last action of a user.
SessionLastAction = "last_action"
// SessionOAuth2State is the xsrf protection key for oauth.
SessionOAuth2State = "oauth2.state"
SessionOAuth2State = "oauth2_state"
// CookieRemember is used for cookies and form input names.
CookieRemember = "rm"
+12
View File
@@ -23,6 +23,9 @@ type MockUser struct {
Locked bool
AttemptNumber int
AttemptTime time.Time
OauthToken string
OauthRefresh string
OauthExpiry time.Time
}
// MockStorer should be valid for any module storer defined in authboss.
@@ -163,6 +166,15 @@ func (m *MockStorer) ConfirmUser(confirmToken string) (result interface{}, err e
return nil, authboss.ErrUserNotFound
}
func (m *MockStorer) OAuth2NewOrUpdate(key string, attr authboss.Attributes) error {
if len(m.CreateErr) > 0 {
return errors.New(m.CreateErr)
}
m.Users[key] = attr
return nil
}
// MockFailStorer is used for testing module initialize functions that recover more than the base storer
type MockFailStorer struct{}
+25 -20
View File
@@ -10,12 +10,14 @@ import (
"path/filepath"
"strings"
"github.com/davecgh/go-spew/spew"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"gopkg.in/authboss.v0"
)
var (
errOAuthStateValidation = errors.New("Could not validate oauth2 state param")
)
// OAuth2Storer is required to do OAuth2 storing.
type OAuth2Storer interface {
authboss.Storer
@@ -47,12 +49,14 @@ func (o *OAuth2) Routes() authboss.RouteTable {
init := fmt.Sprintf("/oauth2/%s", prov)
callback := fmt.Sprintf("/oauth2/callback/%s", prov)
if len(authboss.Cfg.MountPath) > 0 {
init = path.Join(authboss.Cfg.MountPath, init)
callback = path.Join(authboss.Cfg.MountPath, callback)
}
routes[init] = oauthInit
routes[callback] = oauthCallback
if len(authboss.Cfg.MountPath) > 0 {
callback = path.Join(authboss.Cfg.MountPath, callback)
}
cfg.OAuth2Config.RedirectURL = authboss.Cfg.RootURL + callback
}
@@ -96,6 +100,9 @@ func oauthInit(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) er
return nil
}
// for testing
var exchanger = (*oauth2.Config).Exchange
func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
provider := strings.ToLower(filepath.Base(r.URL.Path))
@@ -121,41 +128,39 @@ func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request
// Ensure request is genuine
state := r.FormValue("state")
if state != sessState {
return errors.New("Could not validate oauth2 state param")
return errOAuthStateValidation
}
// Get the code
code := r.FormValue("code")
oauthCtx := context.WithValue(nil, oauth2.HTTPClient, nil)
token, err := cfg.OAuth2Config.Exchange(oauthCtx, code)
token, err := exchanger(cfg.OAuth2Config, oauth2.NoContext, code)
if err != nil {
return fmt.Errorf("Could not validate oauth2 code: %v", err)
}
// User is authenticated
ctx.User[authboss.StoreOAuth2Expiry] = token.Expiry
ctx.User[authboss.StoreOAuth2Token] = token.AccessToken
if len(token.RefreshToken) != 0 {
ctx.User[authboss.StoreOAuth2Refresh] = token.RefreshToken
}
spew.Dump(token)
credentials, err := cfg.Callback(*cfg.OAuth2Config, token)
if err != nil {
return err
}
// User is authenticated
key := fmt.Sprintf("%s:%s", provider, credentials.UID)
ctx.User[authboss.StoreUsername] = key
user := make(authboss.Attributes)
user[authboss.StoreUsername] = key
user[authboss.StoreOAuth2Expiry] = token.Expiry
user[authboss.StoreOAuth2Token] = token.AccessToken
if len(token.RefreshToken) != 0 {
user[authboss.StoreOAuth2Refresh] = token.RefreshToken
}
if len(credentials.Email) > 0 {
ctx.User[authboss.StoreEmail] = credentials.Email
user[authboss.StoreEmail] = credentials.Email
}
// Log user in
ctx.SessionStorer.Put(authboss.SessionKey, key)
storer := authboss.Cfg.Storer.(OAuth2Storer)
if err = storer.OAuth2NewOrUpdate(key, ctx.User); err != nil {
if err = storer.OAuth2NewOrUpdate(key, user); err != nil {
return err
}
+171 -34
View File
@@ -4,17 +4,17 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"strings"
"testing"
"time"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
)
var testAddress = "localhost:23232"
var testProviders = map[string]authboss.OAuthProvider{
"google": authboss.OAuthProvider{
OAuth2Config: &oauth2.Config{
@@ -28,41 +28,45 @@ var testProviders = map[string]authboss.OAuthProvider{
},
}
func TestMain(m *testing.M) {
/*listener, err := net.Listen(testAddress)
if err != nil {
panic(err)
func TestInitialize(t *testing.T) {
authboss.Cfg = authboss.NewConfig()
authboss.Cfg.Storer = mocks.NewMockStorer()
o := OAuth2{}
if err := o.Initialize(); err != nil {
t.Error(err)
}
defer listener.Close()
}
mux := http.NewServeMux()
mux.HandleFunc("/oauth_init_success", func(w http.ResponseWriter, r *http.Request) {
vals := url.Values{
"code": "test",
}
io.WriteString(w, vals.Encode())
})
mux.HandleFunc("/oauth_init_fail", func(w http.ResponseWriter, r *http.Request) {
vals := url.Values{
"error": "error",
"error_reason": "access_denied",
"error_description": "The user denied your request.",
}
io.WriteString(w, vals.Encode())
})
mux.HandleFunc("/oauth_token", func(w http.ResponseWriter, r *http.Request) {
vals := url.Values{
"access_token": "ya29.MgEXfCc5ipyWWEXxcyR0fV7oqlbHQ1xQTDARQlciDYoWlQB72VTgsTeD-8diiB_2cxaXEGMvEpvhZQ",
"token_type": "Bearer",
"expires_in": 3600,
}
io.WriteString(w, vals.Encode())
})
go http.Serve(listener, mux)*/
func TestRoutes(t *testing.T) {
root := "https://localhost:8080"
mount := "/auth"
code := m.Run()
authboss.Cfg = authboss.NewConfig()
authboss.Cfg.RootURL = root
authboss.Cfg.MountPath = mount
authboss.Cfg.OAuth2Providers = testProviders
os.Exit(code)
googleCfg := authboss.Cfg.OAuth2Providers["google"].OAuth2Config
if 0 != len(googleCfg.RedirectURL) {
t.Error("RedirectURL should not be set")
}
o := OAuth2{}
routes := o.Routes()
authURL := path.Join(mount, "oauth2", "google")
tokenURL := path.Join(mount, "oauth2", "callback", "google")
redir := root + path.Join(mount, "oauth2", "callback", "google")
if _, ok := routes[authURL]; !ok {
t.Error("Expected an auth url route:", authURL)
}
if _, ok := routes[tokenURL]; !ok {
t.Error("Expected a token url route:", tokenURL)
}
if googleCfg.RedirectURL != redir {
t.Error("The redirect URL should have been set:", googleCfg.RedirectURL)
}
}
func TestOAuth2Init(t *testing.T) {
@@ -97,3 +101,136 @@ func TestOAuth2Init(t *testing.T) {
t.Error("Missing extra parameters:", loc)
}
}
func TestOAuthSuccess(t *testing.T) {
cfg := authboss.NewConfig()
expiry := time.Now().UTC().Add(3600 * time.Second)
fakeToken := &oauth2.Token{
AccessToken: "token",
TokenType: "Bearer",
RefreshToken: "refresh",
Expiry: expiry,
}
fakeCallback := func(_ oauth2.Config, _ *oauth2.Token) (authboss.OAuth2Credentials, error) {
return authboss.OAuth2Credentials{
UID: "uid",
Email: "email",
}, nil
}
saveExchange := exchanger
defer func() {
exchanger = saveExchange
}()
exchanger = func(_ *oauth2.Config, _ context.Context, _ string) (*oauth2.Token, error) {
return fakeToken, nil
}
cfg.OAuth2Providers = map[string]authboss.OAuthProvider{
"fake": authboss.OAuthProvider{
OAuth2Config: &oauth2.Config{
ClientID: `jazz`,
ClientSecret: `hands`,
Scopes: []string{`profile`, `email`},
Endpoint: oauth2.Endpoint{"fakeauth", "faketoken"},
},
Callback: fakeCallback,
AdditionalParams: url.Values{"include_requested_scopes": []string{"true"}},
},
}
authboss.Cfg = cfg
r, _ := http.NewRequest("GET", "/oauth2/fake?code=code&state=state", nil)
w := httptest.NewRecorder()
ctx := authboss.NewContext()
session := mocks.NewMockClientStorer()
session.Put(authboss.SessionOAuth2State, "state")
storer := mocks.NewMockStorer()
ctx.SessionStorer = session
cfg.Storer = storer
cfg.AuthLoginOKPath = "/fakeloginok"
if err := oauthCallback(ctx, w, r); err != nil {
t.Error(err)
}
key := "fake:uid"
user, ok := storer.Users[key]
if !ok {
t.Error("Couldn't find user.")
}
if val, _ := user.String(authboss.StoreUsername); val != key {
t.Error("Username was wrong:", val)
}
if val, _ := user.String(authboss.StoreEmail); val != "email" {
t.Error("Email was wrong:", val)
}
if val, _ := user.String(authboss.StoreOAuth2Token); val != "token" {
t.Error("Token was wrong:", val)
}
if val, _ := user.String(authboss.StoreOAuth2Refresh); val != "refresh" {
t.Error("Refresh was wrong:", val)
}
if val, _ := user.DateTime(authboss.StoreOAuth2Expiry); !val.Equal(expiry) {
t.Error("Expiry was wrong:", val)
}
if val, _ := session.Get(authboss.SessionKey); val != key {
t.Error("User was not logged in:", val)
}
if w.Code != http.StatusFound {
t.Error("It should redirect")
} else if loc := w.Header().Get("Location"); loc != authboss.Cfg.AuthLoginOKPath {
t.Error("Redirect is wrong:", loc)
}
}
func TestOAuthXSRFFailure(t *testing.T) {
cfg := authboss.NewConfig()
session := mocks.NewMockClientStorer()
session.Put(authboss.SessionOAuth2State, "state")
cfg.OAuth2Providers = testProviders
authboss.Cfg = cfg
values := url.Values{}
values.Set("state", "notstate")
values.Set("code", "code")
r, _ := http.NewRequest("GET", "/oauth2/google?"+values.Encode(), nil)
ctx := authboss.NewContext()
ctx.SessionStorer = session
err := oauthCallback(ctx, nil, r)
if err != errOAuthStateValidation {
t.Error("Should have gotten an error about state validation:", err)
}
}
func TestOAuthFailure(t *testing.T) {
cfg := authboss.NewConfig()
cfg.OAuth2Providers = testProviders
authboss.Cfg = cfg
values := url.Values{}
values.Set("error", "something")
values.Set("error_reason", "auth_failure")
values.Set("error_description", "Failed to auth.")
r, _ := http.NewRequest("GET", "/oauth2/google?"+values.Encode(), nil)
err := oauthCallback(nil, nil, r)
if red, ok := err.(authboss.ErrAndRedirect); !ok {
t.Error("Should be a redirect error")
} else if len(red.FlashError) == 0 {
t.Error("Should have a flash error.")
} else if red.Err.Error() != "auth_failure" {
t.Error("It should record the failure.")
}
}
+5 -1
View File
@@ -2,6 +2,7 @@ package oauth2
import (
"encoding/json"
"net/http"
"golang.org/x/oauth2"
"gopkg.in/authboss.v0"
@@ -21,10 +22,13 @@ type googleMeResponse struct {
Email string `json:"email"`
}
// testing
var clientGet = (*http.Client).Get
// Google is a callback appropriate for use with Google's OAuth2 configuration.
func Google(cfg oauth2.Config, token *oauth2.Token) (cred authboss.OAuth2Credentials, err error) {
client := cfg.Client(oauth2.NoContext, token)
resp, err := client.Get(googleInfoEndpoint)
resp, err := clientGet(client, googleInfoEndpoint)
if err != nil {
return cred, err
}
+44
View File
@@ -0,0 +1,44 @@
package oauth2
import (
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"golang.org/x/oauth2"
)
func TestGoogle(t *testing.T) {
saveClientGet := clientGet
defer func() {
clientGet = saveClientGet
}()
clientGet = func(_ *http.Client, url string) (*http.Response, error) {
return &http.Response{
Body: ioutil.NopCloser(strings.NewReader(`{"id":"id", "email":"email"}`)),
}, nil
}
cfg := *testProviders["google"].OAuth2Config
tok := &oauth2.Token{
AccessToken: "token",
TokenType: "Bearer",
RefreshToken: "refresh",
Expiry: time.Now().Add(60 * time.Minute),
}
cred, err := Google(cfg, tok)
if err != nil {
t.Error(err)
}
if cred.UID != "id" {
t.Error("UID wrong:", cred.UID)
}
if cred.Email != "email" {
t.Error("Email wrong:", cred.Email)
}
}