mirror of
https://github.com/volatiletech/authboss.git
synced 2026-06-19 23:00:27 +02:00
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user