diff --git a/pkg/config/config.go b/pkg/config/config.go index 97a5dc43d..2b6ae9b90 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -89,6 +89,11 @@ type HTTPConfig struct { // CORS is the cross-origin configuration for the HTTP server. CORS CORSConfig `envPrefix:"CORS_" yaml:"cors"` + + // ProxyAuthHeader is the name of an HTTP header set by a trusted reverse + // proxy (e.g. X-User) that contains the authenticated username. When set, + // the server trusts this header for user identification. + ProxyAuthHeader string `env:"PROXY_AUTH_HEADER" yaml:"proxy_auth_header"` } // StatsConfig is the configuration for the stats server. diff --git a/pkg/web/auth.go b/pkg/web/auth.go index 86e15f1ef..35ccb01e8 100644 --- a/pkg/web/auth.go +++ b/pkg/web/auth.go @@ -16,7 +16,21 @@ import ( // authenticate authenticates the user from the request. func authenticate(r *http.Request) (proto.User, error) { - // Prefer the Authorization header + ctx := r.Context() + cfg := config.FromContext(ctx) + + // Check trusted proxy auth header first. + if header := cfg.HTTP.ProxyAuthHeader; header != "" { + if username := r.Header.Get(header); username != "" { + be := backend.FromContext(ctx) + user, err := be.User(ctx, username) + if err == nil { + return user, nil + } + } + } + + // Fall back to the Authorization header. user, err := parseAuthHdr(r) if err != nil || user == nil { if errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrInvalidPassword) { diff --git a/testscript/testdata/http-proxy-auth.txtar b/testscript/testdata/http-proxy-auth.txtar new file mode 100644 index 000000000..934d1f331 --- /dev/null +++ b/testscript/testdata/http-proxy-auth.txtar @@ -0,0 +1,60 @@ +# vi: set ft=conf + +# Test proxy_auth_header authentication + +[windows] skip 'curl makes github actions hang' + +# Enable proxy auth header +env SOFT_SERVE_HTTP_PROXY_AUTH_HEADER=X-User + +# start soft serve +exec soft serve & +# wait for SSH server to start +ensureserverrunning SSH_PORT + +# create a non-admin user +soft user create user1 --key "$USER1_AUTHORIZED_KEY" + +# create a private repo and push content +soft repo create private-repo -p +mkdir ./repo +git -c init.defaultBranch=main -C repo init +mkfile ./repo/README.md '# test' +git -C repo add -A +git -C repo commit -m 'init' +git -C repo remote add origin http://localhost:$HTTP_PORT/private-repo.git + +# create access token for admin to push +soft token create --expires-in '1h' 'push-token' +stdout 'ss_*' +cp stdout pushtokenfile +envfile PUSH_TOKEN=pushtokenfile +git -C repo remote set-url origin http://$PUSH_TOKEN@localhost:$HTTP_PORT/private-repo.git +git -C repo push origin main + +# add user1 as collaborator +soft repo collab add private-repo user1 + +# Test 1: proxy header authenticates as the named user +curl -H 'X-User: user1' http://localhost:$HTTP_PORT/private-repo.git/info/refs?service=git-upload-pack +stdout '.*refs/heads/main.*' + +# Test 2: wrong/unknown user in proxy header is denied +curl -H 'X-User: nobody' http://localhost:$HTTP_PORT/private-repo.git/info/refs?service=git-upload-pack +stdout '401 Unauthorized' + +# Test 3: no proxy header falls back to requiring auth +curl http://localhost:$HTTP_PORT/private-repo.git/info/refs?service=git-upload-pack +stdout '401 Unauthorized' + +# Test 4: create a token for user1 and verify Authorization header still works +usoft token create --expires-in '1h' 'test-token' +stdout 'ss_*' +cp stdout tokenfile +envfile TOKEN=tokenfile +curl http://$TOKEN@localhost:$HTTP_PORT/private-repo.git/info/refs?service=git-upload-pack +stdout '.*refs/heads/main.*' + +# stop the server +[windows] stopserver +[windows] ! stderr .