Skip to content

Commit

Permalink
Rate limit login requests
Browse files Browse the repository at this point in the history
  • Loading branch information
yiannistri committed Feb 25, 2022
1 parent 82db3cb commit cbe2890
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 6 deletions.
12 changes: 10 additions & 2 deletions cmd/gitops/ui/run/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ import (
"github.com/weaveworks/weave-gitops/pkg/server/tls"
)

const (
// Allowed login requests per second
loginRequestRateLimit = 20
)

// Options contains all the options for the `ui run` command.
type Options struct {
Port string
Expand Down Expand Up @@ -229,8 +234,11 @@ func runCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("could not create auth server: %w", err)
}

appConfig.Logger.Info("Registering callback route")
auth.RegisterAuthServer(mux, "/oauth2", srv)
appConfig.Logger.Info("Registering auth routes")

if err := auth.RegisterAuthServer(mux, "/oauth2", srv, loginRequestRateLimit); err != nil {
return fmt.Errorf("failed to register auth routes: %w", err)
}

authServer = srv
}
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ require (
github.com/russross/blackfriday v1.5.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sethvargo/go-limiter v0.7.2
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1128,6 +1128,8 @@ github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvW
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8=
github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand Down
21 changes: 19 additions & 2 deletions pkg/server/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"encoding/base64"
"net/http"
"net/url"

"github.com/sethvargo/go-limiter/httplimit"
"github.com/sethvargo/go-limiter/memorystore"
)

const (
Expand All @@ -28,12 +31,26 @@ const (
// RegisterAuthServer registers the /callback route under a specified prefix.
// This route is called by the OIDC Provider in order to pass back state after
// the authentication flow completes.
func RegisterAuthServer(mux *http.ServeMux, prefix string, srv *AuthServer) {
func RegisterAuthServer(mux *http.ServeMux, prefix string, srv *AuthServer, loginRequestRateLimit uint64) error {
store, err := memorystore.New(&memorystore.Config{
Tokens: loginRequestRateLimit,
})
if err != nil {
return err
}

middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc())
if err != nil {
return err
}

mux.Handle(prefix, srv.OAuth2Flow())
mux.Handle(prefix+"/callback", srv.Callback())
mux.Handle(prefix+"/sign_in", srv.SignIn())
mux.Handle(prefix+"/sign_in", middleware.Handle(srv.SignIn()))
mux.Handle(prefix+"/userinfo", srv.UserInfo())
mux.Handle(prefix+"/logout", srv.Logout())

return nil
}

type principalCtxKey struct{}
Expand Down
67 changes: 65 additions & 2 deletions pkg/server/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package auth_test

import (
"bytes"
"context"
"fmt"
"net/http"
Expand All @@ -14,7 +15,11 @@ import (
"github.com/go-logr/logr"
"github.com/oauth2-proxy/mockoidc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/weaveworks/weave-gitops/pkg/server/auth"
"golang.org/x/crypto/bcrypt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
)

Expand Down Expand Up @@ -53,7 +58,7 @@ func TestWithAPIAuthReturns401ForUnauthenticatedRequests(t *testing.T) {
t.Error("failed to create auth config")
}

auth.RegisterAuthServer(mux, "/oauth2", srv)
_ = auth.RegisterAuthServer(mux, "/oauth2", srv, 1)

s := httptest.NewServer(mux)
defer s.Close()
Expand Down Expand Up @@ -114,7 +119,7 @@ func TestOauth2FlowRedirectsToOIDCIssuerForUnauthenticatedRequests(t *testing.T)
t.Error("failed to create auth config")
}

auth.RegisterAuthServer(mux, "/oauth2", srv)
_ = auth.RegisterAuthServer(mux, "/oauth2", srv, 1)

s := httptest.NewServer(mux)
defer s.Close()
Expand Down Expand Up @@ -142,3 +147,61 @@ func TestIsPublicRoute(t *testing.T) {
assert.False(t, auth.IsPublicRoute(&url.URL{Path: "foo"}, []string{"/foo"}))
assert.False(t, auth.IsPublicRoute(&url.URL{Path: "/foob"}, []string{"/foo"}))
}

func TestRateLimit(t *testing.T) {
ctx := context.Background()
mux := http.NewServeMux()
tokenSignerVerifier, err := auth.NewHMACTokenSignerVerifier(5 * time.Minute)
require.NoError(t, err)

password := "my-secret-password"
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
require.NoError(t, err)

hashedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "admin-password-hash",
Namespace: "wego-system",
},
Data: map[string][]byte{
"password": hashed,
},
}
fakeKubernetesClient := ctrlclient.NewClientBuilder().WithObjects(hashedSecret).Build()

srv, err := auth.NewAuthServer(ctx, logr.Discard(), http.DefaultClient,
auth.AuthConfig{
auth.OIDCConfig{
TokenDuration: 20 * time.Minute,
},
}, fakeKubernetesClient, tokenSignerVerifier)
require.NoError(t, err)
err = auth.RegisterAuthServer(mux, "/oauth2", srv, 1)
require.NoError(t, err)

s := httptest.NewServer(mux)
defer s.Close()

res1, err := http.Post(s.URL+"/oauth2/sign_in", "application/json", bytes.NewReader([]byte(`{"password":"my-secret-password"}`)))
require.NoError(t, err)

if res1.StatusCode != http.StatusOK {
t.Errorf("expected 200 but got %d instead", res1.StatusCode)
}

res2, err := http.Post(s.URL+"/oauth2/sign_in", "application/json", bytes.NewReader([]byte(`{"password":"my-secret-password"}`)))
require.NoError(t, err)

if res2.StatusCode != http.StatusTooManyRequests {
t.Errorf("expected 429 but got %d instead", res2.StatusCode)
}

time.Sleep(time.Second)

res3, err := http.Post(s.URL+"/oauth2/sign_in", "application/json", bytes.NewReader([]byte(`{"password":"my-secret-password"}`)))
require.NoError(t, err)

if res3.StatusCode != http.StatusOK {
t.Errorf("expected 200 but got %d instead", res3.StatusCode)
}
}

0 comments on commit cbe2890

Please sign in to comment.