Skip to content

Commit c8d64a3

Browse files
committed
Refactor authentication and authorization into internal packages with improved modularity
Major architectural refactoring to improve code organization and maintainability: 1. **Package Restructuring**: - Moved authentication logic to `internal/authn` package (OIDC, SAML, federation, token validation) - Moved authorization logic to `internal/authz` package (ACLs, allowlists, fence, sites) - Moved session management to `internal/session` package - Moved host rewriting to `internal/mas
1 parent be324fa commit c8d64a3

29 files changed

Lines changed: 1029 additions & 803 deletions

CLAUDE.md

Lines changed: 89 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -4,123 +4,104 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
Beyond is a BeyondCorp-inspired authentication proxy that controls access to services beyond your perimeter network. It implements zero-trust security patterns with support for OIDC, OAuth2, and SAML authentication.
7+
Beyond is a zero-trust access proxy inspired by Google BeyondCorp. It controls access to services beyond your perimeter network using authentication (OIDC, OAuth2, SAML) and fine-grained access control policies.
88

9-
## Architecture
10-
11-
### Core Components
12-
13-
- **Authentication Layer**: Supports multiple authentication methods (OIDC, OAuth2, SAML)
14-
- `oidc.go` - OpenID Connect implementation
15-
- `saml.go` - SAML authentication handling
16-
- `token.go` - OAuth2 token validation
17-
- `federate.go` - Federation support for cross-domain access
18-
19-
- **Proxy Layer**: HTTP/WebSocket reverse proxy with smart backend discovery
20-
- `proxy.go` - Core reverse proxy implementation
21-
- `learn.go` - Automatic backend port discovery
22-
- `masq.go` - Host rewriting functionality
23-
24-
- **Access Control**: Configuration-driven access management
25-
- `acl.go` - Access control lists and allowlisting
26-
- Sites/fence/allowlist configuration via JSON URLs
27-
28-
- **Specialized Handlers**:
29-
- `docker.go` - Docker registry API compatibility
30-
- `web.go` - Web UI and error pages
31-
- `log.go` - ElasticSearch integration for analytics
32-
33-
### Request Flow
34-
35-
1. Incoming requests hit the main handler (`handler.go`)
36-
2. Authentication is verified via session cookies
37-
3. Unauthenticated requests redirect to `/launch` for auth flow
38-
4. Authenticated requests are proxied to backend services
39-
5. Backend ports are learned automatically or from configuration
40-
41-
## Development Commands
42-
43-
### Building
9+
## Build and Test Commands
4410

4511
```bash
46-
# Build the main httpd binary
47-
go build ./cmd/httpd
12+
# Build all packages
13+
go build -v ./...
4814

49-
# Install to $GOPATH/bin
50-
go install ./cmd/httpd
51-
52-
# Build with Docker
53-
docker build -t beyond .
54-
```
15+
# Build the main binary
16+
go build -v ./cmd/httpd
5517

56-
### Testing
57-
58-
```bash
5918
# Run all tests
60-
go test ./...
61-
62-
# Run tests with coverage
63-
go test -cover ./...
64-
65-
# Run specific test
66-
go test -run TestHandlerPing
67-
68-
# Run tests with verbose output
6919
go test -v ./...
70-
```
7120

72-
### Running
21+
# Run tests for a specific package
22+
go test -v ./[package_name]_test.go
7323

74-
```bash
75-
# Run with minimal configuration (see example/ for configs)
76-
go run cmd/httpd/main.go \
77-
-beyond-host beyond.example.com \
78-
-cookie-domain .example.com \
79-
-cookie-key1 "$(openssl rand -hex 16)" \
80-
-cookie-key2 "$(openssl rand -hex 16)" \
81-
-oidc-issuer https://your-idp.com/oidc \
82-
-oidc-client-id your-client-id \
83-
-oidc-client-secret your-client-secret
84-
85-
# Run with Docker
86-
docker run --rm -p 80:80 presbrey/beyond httpd [flags]
24+
# Run a single test
25+
go test -v -run TestFunctionName
8726
```
8827

89-
### Code Quality
28+
## Architecture Overview
9029

91-
```bash
92-
# Format code
93-
go fmt ./...
94-
95-
# Run linter (install: go install golang.org/x/lint/golint@latest)
96-
golint ./...
97-
98-
# Vet code
99-
go vet ./...
100-
```
101-
102-
## Key Implementation Notes
103-
104-
- Session management uses Gorilla sessions with secure cookies
105-
- WebSocket support includes optional compression
106-
- Automatic backend discovery tries HTTPS first, then HTTP ports
107-
- ElasticSearch logging is optional but recommended for analytics
108-
- Docker registry support handles authentication for private registries
109-
- Federation allows trust relationships between Beyond instances
110-
111-
## Testing Approach
112-
113-
- Tests use `testflight` for HTTP testing
114-
- Mock services created with `httptest`
115-
- Test utilities in `test_utils.go` provide shared setup
116-
- Integration tests cover full authentication flows
117-
- Unit tests focus on individual components
118-
119-
## Configuration
120-
121-
The service is configured via command-line flags. Key configurations:
122-
- Authentication (OIDC/SAML) credentials and endpoints
123-
- Cookie settings for session management
124-
- Backend discovery and port preferences
125-
- Optional ElasticSearch for logging
126-
- Access control via JSON configuration URLs
30+
### Request Flow
31+
1. **Entry Point**: `cmd/httpd/main.go` creates HTTP server with `beyond.NewMux()` handler
32+
2. **Setup**: `setup.go:Setup()` initializes all modules (OIDC, SAML, ACLs, logging, etc.)
33+
3. **Routing**: `web.go:NewMux()` mounts handlers for health, auth, federation, Docker registry, and default proxy
34+
4. **Main Handler**: `handler.go:handler()` processes each request through this flow:
35+
- Check session cookie authentication
36+
- Check OAuth2 token authentication
37+
- Apply allowlist (bypass auth for allowlisted paths/hosts)
38+
- Check host-only restriction (if `-hosts-only` enabled)
39+
- Force login if unauthenticated
40+
- Apply fence (user-specific access restrictions)
41+
- Forward to backend via `nexthop()`
42+
43+
### Backend Connection Flow (`proxy.go`)
44+
1. **Host Rewriting**: `hostRewriteDetailed()` translates incoming host to backend target
45+
- Supports protocol switching (http/https)
46+
- Supports port remapping
47+
- Preserves subdomains during translation
48+
2. **Nexthop Learning**: If backend not cached and `-learn-nexthops=true`:
49+
- `learn.go:learn()` probes common HTTPS ports (443, 4443, 6443, 8443, 9443, 9090)
50+
- Then tries HTTP ports (80, 8080, etc.)
51+
- Caches successful backends in `hostProxy` sync.Map
52+
3. **Proxying**: Creates reverse proxy or WebSocket proxy depending on Upgrade header
53+
54+
### Authentication Flow
55+
- **OIDC** (`oidc.go`): `/launch` redirects to IdP, `/oidc` handles callback and sets session
56+
- **SAML** (`saml.go`): `/launch` initiates SAML flow, `/saml/*` handles assertions
57+
- **Token Auth** (`token.go`): Validates bearer tokens or query params against configured endpoints
58+
59+
### Access Control Layers
60+
1. **Allowlist** (`acl.go`): Bypass auth for specific host, host:method, or path patterns
61+
2. **Host Filtering** (`masq.go`): When `-hosts-only` is set, only allow configured hosts
62+
3. **Fence** (`acl.go`): User-specific restrictions mapping users to allowed site zones
63+
4. **Sites** (`acl.go`): Named zones containing lists of allowed backend URLs
64+
65+
### Special Features
66+
- **Docker Registry** (`docker.go`): Intercepts `/v2/` endpoints for registry authentication
67+
- **Federation** (`federate.go`): Allows Beyond instances to trust each other's auth
68+
- **Host Masquerading** (`masq.go`): Rewrites backend hostnames with protocol/port support
69+
- **Elasticsearch Logging** (`log.go`): Bulk commits HTTP logs to configured ES clusters
70+
71+
## Configuration Files
72+
73+
External JSON configuration URLs are supported for:
74+
- **Allowlist** (`-allowlist-url`): `{"host": [...], "host:method": [...], "path": [...]}`
75+
- **Fence** (`-fence-url`): `{"user@example.com": ["zone1", "zone2"]}`
76+
- **Sites** (`-sites-url`): `{"zone1": ["https://app1.example.com", "https://app2.example.com"]}`
77+
- **Hosts** (`-hosts-url`): `{"old.example.com": "https://new.example.com:8443"}`
78+
79+
## Key Implementation Details
80+
81+
### Session Management
82+
- Cookie-based sessions using gorilla/securecookie
83+
- Cookie key auto-generated if not provided (logs warning)
84+
- Sessions store user email, OAuth state, and next URL
85+
86+
### TLS and Backend Trust
87+
- `-insecure-skip-verify` allows untrusted backend certificates
88+
- Applies to both HTTP transport and WebSocket connections
89+
- TLS config shared via `tlsConfig` global variable
90+
91+
### WebSocket Handling
92+
- Detects `Upgrade: websocket` header in `proxy.go:nexthop()`
93+
- Converts http/https schemes to ws/wss in `http2ws()`
94+
- Uses koding/websocketproxy with custom director for headers
95+
96+
### Error Handling
97+
- Custom error pages with configurable color and support email
98+
- `-401-code` defaults to 418 (teapot) to trigger browser auth properly
99+
- Health check endpoint at `/healthz/ping` (configurable)
100+
101+
## Testing Patterns
102+
103+
Tests use standard Go testing with:
104+
- `httptest` for HTTP server testing
105+
- Table-driven tests for multiple scenarios
106+
- Mock authentication for handler tests
107+
- Temporary test servers for integration tests

a0.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ package beyond
22

33
import (
44
"log"
5-
"time"
65

7-
"github.com/cogolabs/wait"
6+
"github.com/presbrey/pkg/wait"
87
)
98

109
func init() {
1110
// prepend file:lineno
1211
log.SetFlags(log.Flags() | log.Lshortfile)
1312

1413
// wait for networking
15-
wait.ForNetwork(5, time.Second)
14+
wait.ForNetwork()
1615
}

acl_test.go

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,56 +5,54 @@ import (
55
"os"
66
"testing"
77

8+
"github.com/presbrey/beyond/internal/authz"
89
"github.com/stretchr/testify/assert"
910
)
1011

1112
func init() {
1213
t := &http.Transport{}
1314
t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
14-
httpACL.Transport = t
15+
client := &http.Client{Transport: t}
16+
authz.SetHTTPClient(client)
1517
}
1618

1719
const (
1820
aclErrorBase = "http://localhost:9999"
1921
)
2022

2123
func TestACL(t *testing.T) {
22-
*fenceURL = ""
23-
*sitesURL = ""
24-
*allowlistURL = ""
24+
*authz.FenceURL = ""
25+
*authz.SitesURL = ""
26+
*authz.AllowlistURL = ""
2527

26-
assert.NoError(t, refreshFence())
27-
assert.NoError(t, refreshSites())
28-
assert.NoError(t, refreshAllowlist())
28+
assert.NoError(t, authz.RefreshFence())
29+
assert.NoError(t, authz.RefreshSites())
30+
assert.NoError(t, authz.RefreshAllowlist())
2931

30-
*fenceURL = aclErrorBase
31-
*sitesURL = aclErrorBase
32-
*allowlistURL = aclErrorBase
32+
*authz.FenceURL = aclErrorBase
33+
*authz.SitesURL = aclErrorBase
34+
*authz.AllowlistURL = aclErrorBase
3335

34-
assert.Contains(t, refreshFence().Error(), "connection refused")
35-
assert.Contains(t, refreshSites().Error(), "connection refused")
36-
assert.Contains(t, refreshAllowlist().Error(), "connection refused")
36+
assert.Contains(t, authz.RefreshFence().Error(), "connection refused")
37+
assert.Contains(t, authz.RefreshSites().Error(), "connection refused")
38+
assert.Contains(t, authz.RefreshAllowlist().Error(), "connection refused")
3739

3840
cwd, _ := os.Getwd()
39-
*fenceURL = "file://" + cwd + "/example/error.json"
40-
*sitesURL = "file://" + cwd + "/example/error.json"
41-
*allowlistURL = "file://" + cwd + "/example/error.json"
42-
assert.EqualError(t, refreshFence(), "unexpected EOF")
43-
assert.EqualError(t, refreshSites(), "unexpected EOF")
44-
assert.EqualError(t, refreshAllowlist(), "unexpected EOF")
45-
46-
*fenceURL = "file://" + cwd + "/example/fence.json"
47-
*sitesURL = "file://" + cwd + "/example/sites.json"
48-
*allowlistURL = "file://" + cwd + "/example/allowlist.json"
41+
*authz.FenceURL = "file://" + cwd + "/example/error.json"
42+
*authz.SitesURL = "file://" + cwd + "/example/error.json"
43+
*authz.AllowlistURL = "file://" + cwd + "/example/error.json"
44+
assert.EqualError(t, authz.RefreshFence(), "unexpected EOF")
45+
assert.EqualError(t, authz.RefreshSites(), "unexpected EOF")
46+
assert.EqualError(t, authz.RefreshAllowlist(), "unexpected EOF")
47+
48+
*authz.FenceURL = "file://" + cwd + "/example/fence.json"
49+
*authz.SitesURL = "file://" + cwd + "/example/sites.json"
50+
*authz.AllowlistURL = "file://" + cwd + "/example/allowlist.json"
4951
assert.NoError(t, Setup())
5052

51-
assert.NotEmpty(t, fence.m)
52-
assert.NotEmpty(t, sites.m["git"])
53-
assert.NotEmpty(t, allowlist.m["host"])
54-
assert.NotEmpty(t, allowlist.m["path"])
55-
53+
// Test Deny function
5654
reqDeny, _ := http.NewRequest("GET", "https://deny", nil)
57-
assert.True(t, deny(reqDeny, "consultant@gmail.com"))
55+
assert.True(t, authz.Deny(reqDeny, "consultant@gmail.com"))
5856
reqAllow, _ := http.NewRequest("GET", "https://github.com/test", nil)
59-
assert.False(t, deny(reqAllow, "consultant@gmail.com"))
57+
assert.False(t, authz.Deny(reqAllow, "consultant@gmail.com"))
6058
}

example/allowlist.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"host": {
33
"httpbin.org": true,
4+
"1.1.1.1": true,
5+
"checkip.amazonaws.com": true,
46
"echo.websocket.org": true,
57
"nonexistent.example.test": true
68
},

0 commit comments

Comments
 (0)