@@ -10,7 +10,6 @@ import (
1010 "crypto/rand"
1111 "crypto/rsa"
1212 "crypto/sha256"
13- "crypto/subtle"
1413 "crypto/x509"
1514 "encoding/base64"
1615 "encoding/hex"
@@ -28,6 +27,7 @@ import (
2827 kmiplib "github.com/cyphera-labs/kmip-go"
2928 "github.com/cyphera-labs/open-kmip-server/internal/audit"
3029 "github.com/cyphera-labs/open-kmip-server/internal/dashboard"
30+ "github.com/cyphera-labs/open-kmip-server/internal/dashauth"
3131 "github.com/cyphera-labs/open-kmip-server/internal/kmip"
3232 "github.com/cyphera-labs/open-kmip-server/internal/storage"
3333 "golang.org/x/crypto/chacha20poly1305"
@@ -78,19 +78,27 @@ func (rl *ipRateLimiter) allow(ip string) bool {
7878
7979// API is the REST API server.
8080type API struct {
81- store storage.Storage
82- audit * audit.Logger
83- apiKey string
84- corsOrigin string
85- certFile string
86- keyFile string
87- start time.Time
88- tracker * kmip.ConnectionTracker
81+ store storage.Storage
82+ audit * audit.Logger
83+ apiKey string
84+ corsOrigin string
85+ certFile string
86+ keyFile string
87+ start time.Time
88+ tracker * kmip.ConnectionTracker
8989 rateLimiter * ipRateLimiter
90+ dashAuth * dashauth.DashAuth
9091}
9192
9293// NewAPI creates a REST API server.
93- func NewAPI (store storage.Storage , apiKey , corsOrigin , certFile , keyFile string , auditLog * audit.Logger , tracker * kmip.ConnectionTracker ) * API {
94+ // devMode=true skips dashboard authentication (--dev flag).
95+ func NewAPI (store storage.Storage , apiKey , corsOrigin , certFile , keyFile string , auditLog * audit.Logger , tracker * kmip.ConnectionTracker , devMode bool ) * API {
96+ var da * dashauth.DashAuth
97+ if devMode {
98+ da = dashauth .NewDevMode ()
99+ } else {
100+ da = dashauth .New (apiKey , true ) // KMIP REST always TLS
101+ }
94102 return & API {
95103 store : store ,
96104 audit : auditLog ,
@@ -101,6 +109,7 @@ func NewAPI(store storage.Storage, apiKey, corsOrigin, certFile, keyFile string,
101109 start : time .Now (),
102110 tracker : tracker ,
103111 rateLimiter : newIPRateLimiter (),
112+ dashAuth : da ,
104113 }
105114}
106115
@@ -132,28 +141,32 @@ func (a *API) logAudit(r *http.Request, operation, objectUID, objectName, status
132141func (a * API ) Serve (addr string ) error {
133142 mux := http .NewServeMux ()
134143
135- mux .HandleFunc ("GET /v1/keys" , a .auth (a .handleListKeys ))
136- mux .HandleFunc ("POST /v1/keys" , a .auth (a .handleCreateKey ))
137- mux .HandleFunc ("GET /v1/keys/{uid}" , a .auth (a .handleGetKey ))
144+ mux .HandleFunc ("GET /v1/keys" , a .dashAuth . RequireAPIAuth (a .handleListKeys ))
145+ mux .HandleFunc ("POST /v1/keys" , a .dashAuth . RequireAPIAuth (a .handleCreateKey ))
146+ mux .HandleFunc ("GET /v1/keys/{uid}" , a .dashAuth . RequireAPIAuth (a .handleGetKey ))
138147 // C4 fix: material export endpoint removed
139- mux .HandleFunc ("POST /v1/keys/{uid}/activate" , a .auth (a .handleActivateKey ))
140- mux .HandleFunc ("POST /v1/keys/{uid}/revoke" , a .auth (a .handleRevokeKey ))
141- mux .HandleFunc ("DELETE /v1/keys/{uid}" , a .auth (a .handleDestroyKey ))
142- mux .HandleFunc ("POST /v1/keys/{uid}/encrypt" , a .auth (a .handleEncryptKey ))
143- mux .HandleFunc ("POST /v1/keys/{uid}/decrypt" , a .auth (a .handleDecryptKey ))
144- mux .HandleFunc ("POST /v1/keys/{uid}/sign" , a .auth (a .handleSignKey ))
145- mux .HandleFunc ("POST /v1/keys/{uid}/verify" , a .auth (a .handleVerifyKey ))
146- mux .HandleFunc ("POST /v1/keys/{uid}/mac" , a .auth (a .handleMACKey ))
147- mux .HandleFunc ("POST /v1/keys/{uid}/rekey" , a .auth (a .handleRekeyKey ))
148- mux .HandleFunc ("POST /v1/keys/{uid}/wrap" , a .auth (a .handleWrapKey ))
149- mux .HandleFunc ("POST /v1/keys/{uid}/unwrap" , a .auth (a .handleUnwrapKey ))
150- mux .HandleFunc ("POST /v1/certificates" , a .auth (a .handleUploadCertificate ))
151- mux .HandleFunc ("GET /v1/connections" , a .auth (a .handleConnections ))
152- mux .HandleFunc ("GET /v1/status" , a .auth (a .handleStatus ))
153- mux .HandleFunc ("GET /v1/audit" , a .auth (a .handleAuditLog ))
154- mux .HandleFunc ("GET /v1/inventory" , a .auth (a .handleInventory ))
148+ mux .HandleFunc ("POST /v1/keys/{uid}/activate" , a .dashAuth . RequireAPIAuth (a .handleActivateKey ))
149+ mux .HandleFunc ("POST /v1/keys/{uid}/revoke" , a .dashAuth . RequireAPIAuth (a .handleRevokeKey ))
150+ mux .HandleFunc ("DELETE /v1/keys/{uid}" , a .dashAuth . RequireAPIAuth (a .handleDestroyKey ))
151+ mux .HandleFunc ("POST /v1/keys/{uid}/encrypt" , a .dashAuth . RequireAPIAuth (a .handleEncryptKey ))
152+ mux .HandleFunc ("POST /v1/keys/{uid}/decrypt" , a .dashAuth . RequireAPIAuth (a .handleDecryptKey ))
153+ mux .HandleFunc ("POST /v1/keys/{uid}/sign" , a .dashAuth . RequireAPIAuth (a .handleSignKey ))
154+ mux .HandleFunc ("POST /v1/keys/{uid}/verify" , a .dashAuth . RequireAPIAuth (a .handleVerifyKey ))
155+ mux .HandleFunc ("POST /v1/keys/{uid}/mac" , a .dashAuth . RequireAPIAuth (a .handleMACKey ))
156+ mux .HandleFunc ("POST /v1/keys/{uid}/rekey" , a .dashAuth . RequireAPIAuth (a .handleRekeyKey ))
157+ mux .HandleFunc ("POST /v1/keys/{uid}/wrap" , a .dashAuth . RequireAPIAuth (a .handleWrapKey ))
158+ mux .HandleFunc ("POST /v1/keys/{uid}/unwrap" , a .dashAuth . RequireAPIAuth (a .handleUnwrapKey ))
159+ mux .HandleFunc ("POST /v1/certificates" , a .dashAuth . RequireAPIAuth (a .handleUploadCertificate ))
160+ mux .HandleFunc ("GET /v1/connections" , a .dashAuth . RequireAPIAuth (a .handleConnections ))
161+ mux .HandleFunc ("GET /v1/status" , a .dashAuth . RequireAPIAuth (a .handleStatus ))
162+ mux .HandleFunc ("GET /v1/audit" , a .dashAuth . RequireAPIAuth (a .handleAuditLog ))
163+ mux .HandleFunc ("GET /v1/inventory" , a .dashAuth . RequireAPIAuth (a .handleInventory ))
155164 mux .HandleFunc ("GET /metrics" , a .handleMetrics ) // public for Prometheus scraping
156- // Dashboard static files are public — API calls require auth via ?key= param
165+
166+ // Auth endpoints (public — handles login/logout/status)
167+ a .dashAuth .RegisterRoutes (mux )
168+
169+ // Dashboard static files (public — JS handles showing login screen)
157170 mux .Handle ("/ui/" , http .StripPrefix ("/ui" , dashboard .Handler ()))
158171
159172 handler := a .rateLimitMiddleware (a .limitBodyMiddleware (a .corsMiddleware (mux )))
@@ -166,31 +179,7 @@ func (a *API) Serve(addr string) error {
166179 return http .ListenAndServeTLS (addr , a .certFile , a .keyFile , handler )
167180}
168181
169- // C3 fix: auth requires API key when configured — no bypass
170- func (a * API ) auth (next http.HandlerFunc ) http.HandlerFunc {
171- return func (w http.ResponseWriter , r * http.Request ) {
172- if a .apiKey == "" {
173- // H3 note: in production, apiKey is required (enforced in main)
174- next (w , r )
175- return
176- }
177- authHeader := r .Header .Get ("Authorization" )
178- if authHeader == "" {
179- // H3 fix: log failed auth
180- a .logAudit (r , "AUTHN_FAILURE" , "" , "" , "failure" , "missing Authorization header" )
181- a .writeError (w , http .StatusUnauthorized , "missing Authorization header" )
182- return
183- }
184- token := strings .TrimPrefix (authHeader , "Bearer " )
185- if token == authHeader || subtle .ConstantTimeCompare ([]byte (token ), []byte (a .apiKey )) != 1 {
186- // H3 fix: log failed auth
187- a .logAudit (r , "AUTHN_FAILURE" , "" , "" , "failure" , "invalid API key" )
188- a .writeError (w , http .StatusUnauthorized , "invalid API key" )
189- return
190- }
191- next (w , r )
192- }
193- }
182+ // auth() replaced by dashauth.RequireAPIAuth — session cookie + Bearer token
194183
195184func (a * API ) rateLimitMiddleware (next http.Handler ) http.Handler {
196185 return http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
0 commit comments