Make remember and oauth2 work better together.

- Change OAuth2 extra params to not use state, but session instead.
This commit is contained in:
Aaron
2015-03-24 19:39:20 -07:00
parent e83110ee31
commit 07cbd6016f
5 changed files with 65 additions and 61 deletions
+2
View File
@@ -13,6 +13,8 @@ const (
SessionLastAction = "last_action" SessionLastAction = "last_action"
// SessionOAuth2State is the xsrf protection key for oauth. // SessionOAuth2State is the xsrf protection key for oauth.
SessionOAuth2State = "oauth2_state" SessionOAuth2State = "oauth2_state"
// SessionOAuth2Params is the additional settings for oauth like redirection/remember.
SessionOAuth2Params = "oauth2_params"
// CookieRemember is used for cookies and form input names. // CookieRemember is used for cookies and form input names.
CookieRemember = "rm" CookieRemember = "rm"
+38 -22
View File
@@ -3,6 +3,7 @@ package oauth2
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@@ -87,14 +88,21 @@ func oauthInit(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) er
state := base64.URLEncoding.EncodeToString(random) state := base64.URLEncoding.EncodeToString(random)
ctx.SessionStorer.Put(authboss.SessionOAuth2State, state) ctx.SessionStorer.Put(authboss.SessionOAuth2State, state)
var passAlongs []string passAlongs := make(map[string]string)
for k, vals := range r.URL.Query() { for k, vals := range r.URL.Query() {
for _, val := range vals { for _, val := range vals {
passAlongs = append(passAlongs, fmt.Sprintf("%s=%s", k, val)) passAlongs[k] = val
} }
} }
if len(passAlongs) > 0 { if len(passAlongs) > 0 {
state += ";" + strings.Join(passAlongs, ";") str, err := json.Marshal(passAlongs)
if err != nil {
return err
}
ctx.SessionStorer.Put(authboss.SessionOAuth2Params, string(str))
} else {
ctx.SessionStorer.Del(authboss.SessionOAuth2Params)
} }
url := cfg.OAuth2Config.AuthCodeURL(state) url := cfg.OAuth2Config.AuthCodeURL(state)
@@ -114,6 +122,21 @@ var exchanger = (*oauth2.Config).Exchange
func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error { func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) error {
provider := strings.ToLower(filepath.Base(r.URL.Path)) provider := strings.ToLower(filepath.Base(r.URL.Path))
sessState, err := ctx.SessionStorer.GetErr(authboss.SessionOAuth2State)
ctx.SessionStorer.Del(authboss.SessionOAuth2State)
if err != nil {
return err
}
sessValues, ok := ctx.SessionStorer.Get(authboss.SessionOAuth2Params)
// Don't delete this value from session immediately, callbacks use this too
var values map[string]string
if ok {
if err := json.Unmarshal([]byte(sessValues), &values); err != nil {
return err
}
}
hasErr := r.FormValue("error") hasErr := r.FormValue("error")
if len(hasErr) > 0 { if len(hasErr) > 0 {
if err := authboss.Cfg.Callbacks.FireAfter(authboss.EventOAuthFail, ctx); err != nil { if err := authboss.Cfg.Callbacks.FireAfter(authboss.EventOAuthFail, ctx); err != nil {
@@ -127,12 +150,6 @@ func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request
} }
} }
sessState, err := ctx.SessionStorer.GetErr(authboss.SessionOAuth2State)
if err != nil {
return err
}
ctx.SessionStorer.Del(authboss.SessionOAuth2State)
cfg, ok := authboss.Cfg.OAuth2Providers[provider] cfg, ok := authboss.Cfg.OAuth2Providers[provider]
if !ok { if !ok {
return fmt.Errorf("OAuth2 provider %q not found", provider) return fmt.Errorf("OAuth2 provider %q not found", provider)
@@ -183,23 +200,22 @@ func oauthCallback(ctx *authboss.Context, w http.ResponseWriter, r *http.Request
return nil return nil
} }
ctx.SessionStorer.Del(authboss.SessionOAuth2Params)
redirect := authboss.Cfg.AuthLoginOKPath redirect := authboss.Cfg.AuthLoginOKPath
values := make(url.Values) query := make(url.Values)
if len(splState) > 0 { for k, v := range values {
for _, arg := range splState[1:] { switch k {
spl := strings.Split(arg, "=") case authboss.CookieRemember:
switch spl[0] { case authboss.FormValueRedirect:
case authboss.CookieRemember: redirect = v
case authboss.FormValueRedirect: default:
redirect = spl[1] query.Set(k, v)
default:
values.Set(spl[0], spl[1])
}
} }
} }
if len(values) > 0 { if len(query) > 0 {
redirect = fmt.Sprintf("%s?%s", redirect, values.Encode()) redirect = fmt.Sprintf("%s?%s", redirect, query.Encode())
} }
http.Redirect(w, r, redirect, http.StatusFound) http.Redirect(w, r, redirect, http.StatusFound)
+16 -23
View File
@@ -6,7 +6,6 @@ import (
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"path" "path"
"sort"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -78,7 +77,7 @@ func TestOAuth2Init(t *testing.T) {
cfg.OAuth2Providers = testProviders cfg.OAuth2Providers = testProviders
authboss.Cfg = cfg authboss.Cfg = cfg
r, _ := http.NewRequest("GET", "/oauth2/google?r=/my/redirect&rm=true", nil) r, _ := http.NewRequest("GET", "/oauth2/google?redir=/my/redirect%23lol&rm=true", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
ctx := authboss.NewContext() ctx := authboss.NewContext()
ctx.SessionStorer = session ctx.SessionStorer = session
@@ -107,24 +106,8 @@ func TestOAuth2Init(t *testing.T) {
t.Error("It should have had some state:", loc) t.Error("It should have had some state:", loc)
} }
splits := strings.Split(state, ";") if params := session.Values[authboss.SessionOAuth2Params]; params != `{"redir":"/my/redirect#lol","rm":"true"}` {
if len(splits[0]) != 44 { t.Error("The params were wrong:", params)
t.Error("The xsrf token was wrong size:", len(splits[0]), splits[0])
}
// Maps are fun
sort.Strings(splits[1:])
if v, err := url.QueryUnescape(splits[1]); err != nil {
t.Error(err)
} else if v != "r=/my/redirect" {
t.Error("Redirect parameter not saved:", splits[1])
}
if v, err := url.QueryUnescape(splits[2]); err != nil {
t.Error(err)
} else if v != "rm=true" {
t.Error("Remember parameter not saved:", splits[2])
} }
} }
@@ -171,12 +154,18 @@ func TestOAuthSuccess(t *testing.T) {
} }
authboss.Cfg = cfg authboss.Cfg = cfg
url := fmt.Sprintf("/oauth2/fake?code=code&state=%s", url.QueryEscape("state;redir=/myurl;rm=true;myparam=5")) values := make(url.Values)
values.Set("code", "code")
values.Set("state", "state")
url := fmt.Sprintf("/oauth2/fake?%s", values.Encode())
r, _ := http.NewRequest("GET", url, nil) r, _ := http.NewRequest("GET", url, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
ctx := authboss.NewContext() ctx := authboss.NewContext()
session := mocks.NewMockClientStorer() session := mocks.NewMockClientStorer()
session.Put(authboss.SessionOAuth2State, authboss.FormValueOAuth2State) session.Put(authboss.SessionOAuth2State, authboss.FormValueOAuth2State)
session.Put(authboss.SessionOAuth2Params, `{"redir":"/myurl?myparam=5","rm":"true"}`)
storer := mocks.NewMockStorer() storer := mocks.NewMockStorer()
ctx.SessionStorer = session ctx.SessionStorer = session
cfg.OAuth2Storer = storer cfg.OAuth2Storer = storer
@@ -232,9 +221,9 @@ func TestOAuthXSRFFailure(t *testing.T) {
values.Set(authboss.FormValueOAuth2State, "notstate") values.Set(authboss.FormValueOAuth2State, "notstate")
values.Set("code", "code") values.Set("code", "code")
r, _ := http.NewRequest("GET", "/oauth2/google?"+values.Encode(), nil)
ctx := authboss.NewContext() ctx := authboss.NewContext()
ctx.SessionStorer = session ctx.SessionStorer = session
r, _ := http.NewRequest("GET", "/oauth2/google?"+values.Encode(), nil)
err := oauthCallback(ctx, nil, r) err := oauthCallback(ctx, nil, r)
if err != errOAuthStateValidation { if err != errOAuthStateValidation {
@@ -253,9 +242,13 @@ func TestOAuthFailure(t *testing.T) {
values.Set("error_reason", "auth_failure") values.Set("error_reason", "auth_failure")
values.Set("error_description", "Failed to auth.") values.Set("error_description", "Failed to auth.")
ctx := authboss.NewContext()
session := mocks.NewMockClientStorer()
session.Put(authboss.SessionOAuth2State, authboss.FormValueOAuth2State)
ctx.SessionStorer = session
r, _ := http.NewRequest("GET", "/oauth2/google?"+values.Encode(), nil) r, _ := http.NewRequest("GET", "/oauth2/google?"+values.Encode(), nil)
err := oauthCallback(nil, nil, r) err := oauthCallback(ctx, nil, r)
if red, ok := err.(authboss.ErrAndRedirect); !ok { if red, ok := err.(authboss.ErrAndRedirect); !ok {
t.Error("Should be a redirect error") t.Error("Should be a redirect error")
} else if len(red.FlashError) == 0 { } else if len(red.FlashError) == 0 {
+7 -13
View File
@@ -6,9 +6,9 @@ import (
"crypto/md5" "crypto/md5"
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings"
"gopkg.in/authboss.v0" "gopkg.in/authboss.v0"
) )
@@ -97,24 +97,18 @@ func (r *Remember) afterAuth(ctx *authboss.Context) error {
// Has to pander to horrible state variable packing to figure out if we want // Has to pander to horrible state variable packing to figure out if we want
// to be remembered. // to be remembered.
func (r *Remember) afterOAuth(ctx *authboss.Context) error { func (r *Remember) afterOAuth(ctx *authboss.Context) error {
state, ok := ctx.FirstFormValue(authboss.FormValueOAuth2State) sessValues, ok := ctx.SessionStorer.Get(authboss.SessionOAuth2Params)
if !ok { if !ok {
return nil return nil
} }
splState := strings.Split(state, ";") var values map[string]string
if len(splState) < 0 { if err := json.Unmarshal([]byte(sessValues), &values); err != nil {
return nil return err
} }
should := false val, ok := values[authboss.CookieRemember]
for _, arg := range splState[1:] { should := ok && val == "true"
spl := strings.Split(arg, "=")
if spl[0] == authboss.CookieRemember {
should = spl[1] == "true"
break
}
}
if !should { if !should {
return nil return nil
+2 -3
View File
@@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"testing" "testing"
"gopkg.in/authboss.v0" "gopkg.in/authboss.v0"
@@ -73,9 +72,9 @@ func TestAfterOAuth(t *testing.T) {
authboss.Cfg.Storer = storer authboss.Cfg.Storer = storer
cookies := mocks.NewMockClientStorer() cookies := mocks.NewMockClientStorer()
session := mocks.NewMockClientStorer() session := mocks.NewMockClientStorer(authboss.SessionOAuth2Params, `{"rm":"true"}`)
uri := fmt.Sprintf("%s?state=%s", "localhost/oauthed", url.QueryEscape("xsrf;rm=true")) uri := fmt.Sprintf("%s?state=%s", "localhost/oauthed", "xsrf")
req, err := http.NewRequest("GET", uri, nil) req, err := http.NewRequest("GET", uri, nil)
if err != nil { if err != nil {
t.Error("Unexpected Error:", err) t.Error("Unexpected Error:", err)