diff --git a/app/rest/auth/auth.go b/app/rest/auth/auth.go index 3d85d159..f49ffed8 100644 --- a/app/rest/auth/auth.go +++ b/app/rest/auth/auth.go @@ -77,7 +77,7 @@ func (p Provider) Routes() chi.Router { return router } -// LoginHandler - GET /login/{provider}?from=redirect-back-url +// LoginHandler - GET /login?from=redirect-back-url func (p Provider) LoginHandler(w http.ResponseWriter, r *http.Request) { // make state (random) and store in session @@ -106,6 +106,7 @@ func (p Provider) LoginHandler(w http.ResponseWriter, r *http.Request) { } // AuthHandler fills user info and redirects to "from" url. This is callback url redirected locally by browser +// GET /callback func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { session, err := p.Get(r, "remark") @@ -116,8 +117,13 @@ func (p Provider) AuthHandler(w http.ResponseWriter, r *http.Request) { // compare saved state to the one from redirect url retrievedState, ok := session.Values["state"] - if !ok || retrievedState != r.URL.Query().Get("state") { - http.Error(w, fmt.Sprintf("unexpected state %s", retrievedState.(string)), http.StatusUnauthorized) + if !ok { + http.Error(w, "missing state in store", http.StatusUnauthorized) + return + } + + if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { + http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized) return } diff --git a/app/rest/auth/auth_test.go b/app/rest/auth/auth_test.go new file mode 100644 index 00000000..ac666d04 --- /dev/null +++ b/app/rest/auth/auth_test.go @@ -0,0 +1,130 @@ +package auth + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strings" + "testing" + + "github.com/gorilla/sessions" + "github.com/stretchr/testify/assert" + "github.com/umputun/remark/app/store" + "golang.org/x/oauth2" +) + +func TestAuth(t *testing.T) { + + sessionStore := &mockStore{values: make(map[interface{}]interface{})} + + _, ts, ots := mockProvider(t, sessionStore) + defer func() { + ts.Close() + ots.Close() + }() + + resp, err := http.Get("http://localhost:8081/login") + assert.Nil(t, err) + assert.Equal(t, 200, resp.StatusCode) + body, err := ioutil.ReadAll(resp.Body) + t.Logf("resp %s", string(body)) + u := store.User{} + err = json.Unmarshal(body, &u) + assert.Nil(t, err) + assert.Equal(t, store.User{Name: "blah", ID: "myuser", Picture: "", Profile: "", Admin: false, Blocked: false, IP: ""}, u) + +} + +func mockProvider(t *testing.T, sessStore sessions.Store) (provder Provider, ts *http.Server, oauth *http.Server) { + + provider := Provider{ + Name: "mock", + Endpoint: oauth2.Endpoint{ + AuthURL: "http://localhost:8082/login/oauth/authorize", + TokenURL: "http://localhost:8082/login/oauth/access_token", + }, + RedirectURL: "http://localhost:8081/callback", + Scopes: []string{"user:email"}, + InfoURL: "http://localhost:8082/user", + MapUser: func(data userData) store.User { + userInfo := store.User{ + ID: data.value("id"), + Name: data.value("name"), + Picture: data.value("pictrue"), + Profile: data.value("profile"), + } + return userInfo + }, + } + + provder = initProvider(Params{SessionStore: sessStore, Cid: "cid", Csecret: "csecret"}, provider) + + ts = &http.Server{Addr: ":8081", Handler: provder.Routes()} + + oauth = &http.Server{ + Addr: ":8082", + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Printf("[MOCK OAUTH] request %s %s %+v", r.Method, r.URL, r.Header) + switch { + case strings.HasPrefix(r.URL.Path, "/login/oauth/authorize"): + state := r.URL.Query().Get("state") + w.Header().Add("Location", "http://localhost:8081/callback?code=g0ZGZmNjVmOWI&state="+state) + w.WriteHeader(302) + case strings.HasPrefix(r.URL.Path, "/login/oauth/access_token"): + res := `{ + "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", + "token_type":"bearer", + "expires_in":3600, + "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", + "scope":"create", + "state":"12345678" + }` + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + w.Write([]byte(res)) + case strings.HasPrefix(r.URL.Path, "/user"): + res := `{ + "id":"myuser", + "name":"blah" + }` + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(200) + w.Write([]byte(res)) + default: + t.Logf("unexpected oauth request %s %s", r.Method, r.URL) + } + }), + } + + go oauth.ListenAndServe() + go ts.ListenAndServe() + + return provider, ts, oauth +} + +type mockStore struct { + values map[interface{}]interface{} +} + +func (ms *mockStore) Get(r *http.Request, name string) (*sessions.Session, error) { + if ms.values == nil { + ms.values = make(map[interface{}]interface{}) + } + s := sessions.NewSession(ms, name) + s.Values = ms.values + return s, nil +} + +func (ms *mockStore) New(r *http.Request, name string) (*sessions.Session, error) { + ms.values = make(map[interface{}]interface{}) + return &sessions.Session{Values: ms.values}, nil +} + +func (ms *mockStore) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error { + if ms.values == nil { + ms.values = make(map[interface{}]interface{}) + } + ms.values = s.Values + return nil +}