mirror of
https://github.com/volatiletech/authboss.git
synced 2026-06-19 23:00:27 +02:00
Make remember and oauth2 work better together.
- Change OAuth2 extra params to not use state, but session instead.
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user