Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic auth middleware for basic protection of web ui #113

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Job counts are cached for very large job tables to make request timeouts less likely. [PR #108](https://github.com/riverqueue/riverui/pull/108).
- Added basic auth config parameter for protection. [PR #113](https://github.com/riverqueue/riverui/pull/113)

## [0.3.1] - 2024-08-02

Expand Down
10 changes: 6 additions & 4 deletions cmd/riverui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,12 @@ func initAndServe(ctx context.Context) int {
}

handlerOpts := &riverui.HandlerOpts{
Client: client,
DBPool: dbPool,
Logger: logger,
Prefix: pathPrefix,
Client: client,
DBPool: dbPool,
Logger: logger,
Prefix: pathPrefix,
BasicAuthUser: os.Getenv("BASIC_AUTH_USER"),
BasicAuthPassword: os.Getenv("BASIC_AUTH_PASSWORD"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You use "user" here, but "username" below.

Do you want to change all references to user/pass/BASIC_AUTH_USER/BASIC_AUTH_PASS? The symmetry in length appeals to me, and it's a little shorter.

Also, can you keep this list alphabetized?

}

server, err := riverui.NewServer(handlerOpts)
Expand Down
9 changes: 9 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ $ docker pull ghcr.io/riverqueue/riverui:latest
$ docker run -p 8080:8080 --env DATABASE_URL ghcr.io/riverqueue/riverui:latest
```

### Environment variables

- `DATABASE_URL=...` - define database url
- `PORT=8080` - define listening port
- `RIVER_DEBUG=true` - enable debugging logs
- `CORS_ORIGINS=url1,url2` - define allowed CORS origins
- `OTEL_ENABLED=true` - enable OTEL integration
- `BASIC_AUTH_USER=admin`, `BASIC_AUTH_PASSWORD=changeme` - enable basic auth username/password
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea on some basic documentation for this stuff.

Can you sort this list? Better if there's some sort of scheme in how things like this are organized.


## Development

See [developing River UI](./docs/development.md).
48 changes: 47 additions & 1 deletion handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package riverui
import (
"context"
"embed"
"encoding/base64"
"errors"
"fmt"
"io/fs"
Expand Down Expand Up @@ -37,7 +38,9 @@ type HandlerOpts struct {
// Logger is the logger to use logging errors within the handler.
Logger *slog.Logger
// Prefix is the path prefix to use for the API and UI HTTP requests.
Prefix string
Prefix string
BasicAuthUser string
BasicAuthPassword string
}

func (opts *HandlerOpts) validate() error {
Expand Down Expand Up @@ -146,6 +149,10 @@ func NewServer(opts *HandlerOpts) (*Server, error) {
middlewareStack.Use(&stripPrefixMiddleware{prefix})
}

if opts.BasicAuthUser != "" && opts.BasicAuthPassword != "" {
middlewareStack.Use(basicAuthMiddleware{opts.BasicAuthUser, opts.BasicAuthPassword})
}

server := &Server{
handler: middlewareStack.Mount(mux),
services: services,
Expand Down Expand Up @@ -233,6 +240,45 @@ func mountStaticFiles(logger *slog.Logger, mux *http.ServeMux) error {
})
}

// Requires basic auth username/password authentication on all endpoints
// https://en.wikipedia.org/wiki/Basic_access_authentication
type basicAuthMiddleware struct {
username string
password string
}

func (m basicAuthMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
basicAuthHeader := r.Header.Get("Authorization")
if basicAuthHeader == "" {
w.Header().Set("WWW-Authenticate", `Basic realm="riverui restricted"`)
w.WriteHeader(http.StatusUnauthorized)
return
}

if !strings.HasPrefix(basicAuthHeader, "Basic ") {
w.Header().Set("WWW-Authenticate", `Basic realm="riverui restricted"`)
w.WriteHeader(http.StatusUnauthorized)
return
}

data, err := base64.StdEncoding.DecodeString(basicAuthHeader[len("Basic "):])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know about Request.BasicAuth? Probably better to use that than implement it yourself.

if err != nil {
w.Header().Set("WWW-Authenticate", `Basic realm="riverui restricted"`)
w.WriteHeader(http.StatusUnauthorized)
return
}

parts := strings.Split(string(data), ":")
if len(parts) != 2 || parts[0] != m.username || parts[1] != m.password {
w.Header().Set("WWW-Authenticate", `Basic realm="riverui restricted"`)
w.WriteHeader(http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think you could take a stab at a basic couple of tests for this middleware? I know the project is hit or miss testing wise, but we're trying to shore that up.


// Go's http.StripPrefix can sometimes result in an empty path. For example,
// when removing a prefix like "/foo" from path "/foo", the result is "". This
// does not get handled by the ServeMux correctly (it results in a redirect to
Expand Down