Skip to content

Commit b353df9

Browse files
Implement Token-Based Authentication System (#75)
Implement authentication system enabling secure connections to remote Construct daemons while preserving local Unix socket simplicity. Core authentication flows: - Unix socket connections: implicit admin via OS permissions - TCP connections: Bearer token validation with database lookup - Setup codes: secure bootstrap mechanism for token distribution Token security model: - 256-bit cryptographic randomness (crypto/rand) - SHA-256 hashing (plaintext never stored) - Configurable expiration (90 day default, 365 day max) - One-time display at creation Setup code bootstrap: - Short-lived codes (20 minute default expiry) - Single-use consumption with automatic deletion - Case-insensitive for usability - Thread-safe in-memory storage API additions: - AuthService with 5 RPCs (CreateToken, CreateSetupCode, ListTokens, RevokeToken, ExchangeSetupCode) - ConnectRPC interceptor for authentication enforcement Co-authored-by: construct-agent <noreply@construct.sh>
1 parent 946d922 commit b353df9

33 files changed

Lines changed: 4735 additions & 33 deletions

api/go/client/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Client struct {
2121
agent v1connect.AgentServiceClient
2222
task v1connect.TaskServiceClient
2323
message v1connect.MessageServiceClient
24+
auth v1connect.AuthServiceClient
2425
}
2526

2627
type ClientOptions struct {
@@ -104,6 +105,7 @@ func NewClient(endpointContext EndpointContext, options ...ClientOption) (*Clien
104105
agent: v1connect.NewAgentServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
105106
task: v1connect.NewTaskServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
106107
message: v1connect.NewMessageServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
108+
auth: v1connect.NewAuthServiceClient(opts.HTTPClient, baseURL, opts.ConnectOptions...),
107109
}, nil
108110
}
109111

@@ -127,12 +129,17 @@ func (c *Client) Message() v1connect.MessageServiceClient {
127129
return c.message
128130
}
129131

132+
func (c *Client) Auth() v1connect.AuthServiceClient {
133+
return c.auth
134+
}
135+
130136
type MockClient struct {
131137
ModelProvider *mocks.MockModelProviderServiceClient
132138
Model *mocks.MockModelServiceClient
133139
Agent *mocks.MockAgentServiceClient
134140
Task *mocks.MockTaskServiceClient
135141
Message *mocks.MockMessageServiceClient
142+
Auth *mocks.MockAuthServiceClient
136143
}
137144

138145
func NewMockClient(ctrl *gomock.Controller) *MockClient {
@@ -142,6 +149,7 @@ func NewMockClient(ctrl *gomock.Controller) *MockClient {
142149
Agent: mocks.NewMockAgentServiceClient(ctrl),
143150
Task: mocks.NewMockTaskServiceClient(ctrl),
144151
Message: mocks.NewMockMessageServiceClient(ctrl),
152+
Auth: mocks.NewMockAuthServiceClient(ctrl),
145153
}
146154
}
147155

@@ -152,6 +160,7 @@ func (c *MockClient) Client() *Client {
152160
agent: c.Agent,
153161
task: c.Task,
154162
message: c.Message,
163+
auth: c.Auth,
155164
}
156165
}
157166

api/go/client/mocks/auth.connect_mock.go

Lines changed: 217 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/api/api.go

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/furisto/construct/api/go/v1/v1connect"
1515
"github.com/furisto/construct/backend/analytics"
16+
"github.com/furisto/construct/backend/api/auth"
1617
"github.com/furisto/construct/backend/event"
1718
"github.com/furisto/construct/backend/memory"
1819
"github.com/furisto/construct/backend/secret"
@@ -31,14 +32,17 @@ type Server struct {
3132
}
3233

3334
func NewServer(runtime AgentRuntime, listener net.Listener, eventBus *event.Bus, analyticsClient analytics.Client) *Server {
35+
tokenProvider := auth.NewTokenProvider()
36+
3437
apiHandler := NewHandler(
3538
HandlerOptions{
36-
DB: runtime.Memory(),
37-
Encryption: runtime.Encryption(),
38-
AgentRuntime: runtime,
39-
MessageHub: runtime.EventHub(),
40-
EventBus: eventBus,
41-
Analytics: analyticsClient,
39+
DB: runtime.Memory(),
40+
Encryption: runtime.Encryption(),
41+
AgentRuntime: runtime,
42+
MessageHub: runtime.EventHub(),
43+
EventBus: eventBus,
44+
Analytics: analyticsClient,
45+
TokenProvider: tokenProvider,
4246
},
4347
)
4448

@@ -60,6 +64,13 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
6064
BaseContext: func(l net.Listener) context.Context {
6165
return ctx
6266
},
67+
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
68+
transport := auth.TransportTCP
69+
if _, ok := c.(*net.UnixConn); ok {
70+
transport = auth.TransportUnix
71+
}
72+
return auth.WithTransport(ctx, transport)
73+
},
6374
}
6475

6576
return s.server.Serve(s.listener)
@@ -70,9 +81,10 @@ func (s *Server) Shutdown(ctx context.Context) error {
7081
}
7182

7283
type HandlerOptions struct {
73-
DB *memory.Client
74-
Encryption *secret.Encryption
75-
AgentRuntime AgentRuntime
84+
DB *memory.Client
85+
Encryption *secret.Encryption
86+
AgentRuntime AgentRuntime
87+
TokenProvider *auth.TokenProvider
7688

7789
EventBus *event.Bus
7890
MessageHub *event.MessageHub
@@ -90,20 +102,25 @@ func NewHandler(opts HandlerOptions) *Handler {
90102
mux: http.NewServeMux(),
91103
}
92104

105+
connectOpts := append([]connect.HandlerOption{connect.WithInterceptors(auth.NewAuthInterceptor(opts.DB, opts.TokenProvider))}, opts.RequestOptions...)
106+
107+
authHandler := NewAuthHandler(opts.DB, opts.TokenProvider)
108+
handler.mux.Handle(v1connect.NewAuthServiceHandler(authHandler, connectOpts...))
109+
93110
modelProviderHandler := NewModelProviderHandler(opts.DB, opts.Encryption)
94-
handler.mux.Handle(v1connect.NewModelProviderServiceHandler(modelProviderHandler, opts.RequestOptions...))
111+
handler.mux.Handle(v1connect.NewModelProviderServiceHandler(modelProviderHandler, connectOpts...))
95112

96113
modelHandler := NewModelHandler(opts.DB)
97-
handler.mux.Handle(v1connect.NewModelServiceHandler(modelHandler, opts.RequestOptions...))
114+
handler.mux.Handle(v1connect.NewModelServiceHandler(modelHandler, connectOpts...))
98115

99116
agentHandler := NewAgentHandler(opts.DB, opts.Analytics)
100-
handler.mux.Handle(v1connect.NewAgentServiceHandler(agentHandler, opts.RequestOptions...))
117+
handler.mux.Handle(v1connect.NewAgentServiceHandler(agentHandler, connectOpts...))
101118

102119
taskHandler := NewTaskHandler(opts.DB, opts.MessageHub, opts.EventBus, opts.AgentRuntime, opts.Analytics)
103-
handler.mux.Handle(v1connect.NewTaskServiceHandler(taskHandler, opts.RequestOptions...))
120+
handler.mux.Handle(v1connect.NewTaskServiceHandler(taskHandler, connectOpts...))
104121

105122
messageHandler := NewMessageHandler(opts.DB, opts.AgentRuntime, opts.MessageHub, opts.EventBus)
106-
handler.mux.Handle(v1connect.NewMessageServiceHandler(messageHandler, opts.RequestOptions...))
123+
handler.mux.Handle(v1connect.NewMessageServiceHandler(messageHandler, connectOpts...))
107124

108125
return handler
109126
}

0 commit comments

Comments
 (0)