Skip to content

Commit dfb5a39

Browse files
committed
feat: add JSON API for FDO Owner Server with OpenAPI specification
Implements comprehensive JSON responses for all FIDO Device Onboard Owner Server endpoints while maintaining backward compatibility with existing PEM-based clients through Accept header content negotiation. ## Key Features - All Owner API endpoints return JSON by default - OpenAPI 3.1 specification ready for code generation - Comprehensive test coverage validating JSON responses - Backward compatibility via Accept: application/x-pem-file - Split server architecture preserved - Consolidated error handling patterns ## New Files - api/openapi/owner-server.yaml: OpenAPI 3.1 specification (139 lines) - api/handlers/responses.go: JSON response utilities (108 lines) - api/handlersTest/json_api_test.go: JSON validation tests (84 lines) - api/handlersTest/helpers_test.go: Test helper functions (26 lines) - api/openapi/README.md: Documentation (25 lines) ## Modified Files - api/handlers/vouchers.go: JSON responses + error consolidation - api/handlers/ownerinfo.go: JSON error handling - api/handlersTest/vouchers_test.go: Updated for JSON expectations - Makefile: Added OpenAPI validation/generation targets - .gitignore: OpenAPI generator artifacts ## Testing All endpoints tested with comprehensive JSON validation: - TestJSONResponsesRequired: validates JSON Content-Type headers - TestInsertVoucherHandler: voucher operations with JSON responses - TestBackwardCompatibilityPEM: ensures PEM clients still work Signed-off-by: djach7 <djachimo@redhat.com>
1 parent 98e64c8 commit dfb5a39

10 files changed

Lines changed: 717 additions & 73 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ go-fdo-server
66
go-fdo-server-*.tar.*
77
rpmbuild
88
test/workdir
9+
10+
# Generated OpenAPI code
11+
generated/

Makefile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,53 @@ ARCH := $(shell uname -m)
1212
# Default target
1313
all: build test
1414

15+
#
16+
# OpenAPI Code Generation
17+
#
18+
OPENAPI_SPEC := api/openapi/owner-server.yaml
19+
GENERATED_DIR := generated
20+
21+
.PHONY: validate-openapi
22+
validate-openapi:
23+
@echo "Validating OpenAPI specification..."
24+
@command -v openapi-generator >/dev/null 2>&1 || { \
25+
echo "Error: openapi-generator CLI not found. Please install it first:"; \
26+
echo " npm install @openapitools/openapi-generator-cli -g"; \
27+
echo " or"; \
28+
echo " brew install openapi-generator"; \
29+
exit 1; \
30+
}
31+
openapi-generator validate -i $(OPENAPI_SPEC)
32+
33+
.PHONY: generate-api
34+
generate-api: validate-openapi
35+
@echo "Generating Go server and client code from OpenAPI specification..."
36+
@mkdir -p $(GENERATED_DIR)/server $(GENERATED_DIR)/client
37+
openapi-generator generate \
38+
-i $(OPENAPI_SPEC) \
39+
-g go-server \
40+
-o $(GENERATED_DIR)/server \
41+
--additional-properties=packageName=openapi,outputAsLibrary=true,sourceFolder=src/main/go
42+
openapi-generator generate \
43+
-i $(OPENAPI_SPEC) \
44+
-g go \
45+
-o $(GENERATED_DIR)/client \
46+
--additional-properties=packageName=client
47+
48+
.PHONY: clean-generated
49+
clean-generated:
50+
@echo "Cleaning generated OpenAPI code..."
51+
rm -rf $(GENERATED_DIR)
52+
53+
.PHONY: openapi-docs
54+
openapi-docs:
55+
@echo "Starting OpenAPI documentation server..."
56+
@command -v swagger-ui-serve >/dev/null 2>&1 || { \
57+
echo "Installing swagger-ui-serve..."; \
58+
npm install -g swagger-ui-serve; \
59+
}
60+
swagger-ui-serve $(OPENAPI_SPEC)
61+
1562
# Build the Go project
1663
.PHONY: build
1764
build: tidy fmt vet

api/handlers/ownerinfo.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ func OwnerInfoHandler(w http.ResponseWriter, r *http.Request) {
2828
}
2929
}
3030

31-
func getOwnerInfo(w http.ResponseWriter, _ *http.Request) {
31+
func getOwnerInfo(w http.ResponseWriter, r *http.Request) {
3232
slog.Debug("Fetching ownerInfo")
3333
ownerInfoJSON, err := db.FetchOwnerInfoJSON()
3434
if err != nil {
3535
if errors.Is(err, gorm.ErrRecordNotFound) {
3636
slog.Error("No ownerInfo found")
37-
http.Error(w, "No ownerInfo found", http.StatusNotFound)
37+
WriteErrorResponse(w, r, http.StatusNotFound, "No ownerInfo found", "Owner redirect information has not been configured", "No ownerInfo found")
3838
} else {
3939
slog.Error("Error fetching ownerInfo", "error", err)
40-
http.Error(w, "Error fetching ownerInfo", http.StatusInternalServerError)
40+
WriteErrorResponse(w, r, http.StatusInternalServerError, "Error fetching ownerInfo", err.Error(), "Error fetching ownerInfo")
4141
}
4242
return
4343
}

api/handlers/responses.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-FileCopyrightText: (C) 2024 Intel Corporation
2+
// SPDX-License-Identifier: Apache 2.0
3+
4+
package handlers
5+
6+
import (
7+
"encoding/base64"
8+
"encoding/hex"
9+
"encoding/json"
10+
"net/http"
11+
12+
"github.com/fido-device-onboard/go-fdo-server/internal/utils"
13+
)
14+
15+
// VoucherResponse represents a voucher in JSON format
16+
type VoucherResponse struct {
17+
Voucher string `json:"voucher"`
18+
Encoding string `json:"encoding"`
19+
GUID string `json:"guid"`
20+
}
21+
22+
// VoucherInsertResponse represents the result of voucher insertion
23+
type VoucherInsertResponse struct {
24+
Processed int `json:"processed"`
25+
Inserted int `json:"inserted"`
26+
Errors []string `json:"errors,omitempty"`
27+
}
28+
29+
// ErrorResponse represents a standard error response
30+
type ErrorResponse struct {
31+
Error string `json:"error"`
32+
Details string `json:"details,omitempty"`
33+
}
34+
35+
// WriteJSONVoucher writes a voucher response in JSON format
36+
func WriteJSONVoucher(w http.ResponseWriter, voucherData []byte, encoding string, guid []byte) error {
37+
response := VoucherResponse{
38+
Voucher: base64.StdEncoding.EncodeToString(voucherData),
39+
Encoding: encoding,
40+
GUID: hex.EncodeToString(guid),
41+
}
42+
43+
w.Header().Set("Content-Type", "application/json")
44+
return json.NewEncoder(w).Encode(response)
45+
}
46+
47+
// WriteJSONError writes a standard error response in JSON format
48+
func WriteJSONError(w http.ResponseWriter, statusCode int, errorMsg, details string) {
49+
response := ErrorResponse{
50+
Error: errorMsg,
51+
Details: details,
52+
}
53+
54+
w.Header().Set("Content-Type", "application/json")
55+
w.WriteHeader(statusCode)
56+
json.NewEncoder(w).Encode(response)
57+
}
58+
59+
// WriteJSONVoucherInsertResponse writes a voucher insert response in JSON format
60+
func WriteJSONVoucherInsertResponse(w http.ResponseWriter, response VoucherInsertResponse) error {
61+
w.Header().Set("Content-Type", "application/json")
62+
if len(response.Errors) > 0 && response.Inserted == 0 {
63+
w.WriteHeader(http.StatusBadRequest)
64+
} else {
65+
w.WriteHeader(http.StatusOK)
66+
}
67+
return json.NewEncoder(w).Encode(response)
68+
}
69+
70+
// ShouldReturnJSON checks if the client prefers JSON response
71+
// This allows for backward compatibility with existing PEM clients
72+
func ShouldReturnJSON(r *http.Request) bool {
73+
accept := r.Header.Get("Accept")
74+
75+
// If client specifically requests PEM, honor it for backward compatibility
76+
if accept == "application/x-pem-file" {
77+
return false
78+
}
79+
80+
// Default to JSON for everything else (including no preference)
81+
return true
82+
}
83+
84+
// WriteErrorResponse writes an error response in JSON or text format based on Accept header
85+
func WriteErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int, jsonMsg, jsonDetails, textMsg string) {
86+
if ShouldReturnJSON(r) {
87+
WriteJSONError(w, statusCode, jsonMsg, jsonDetails)
88+
} else {
89+
http.Error(w, textMsg, statusCode)
90+
}
91+
}
92+
93+
// ValidateAndDecodeGUID validates a GUID string and returns the decoded bytes
94+
// Returns the decoded GUID bytes or an error response written to the writer
95+
func ValidateAndDecodeGUID(w http.ResponseWriter, r *http.Request, guidHex string) ([]byte, bool) {
96+
if !utils.IsValidGUID(guidHex) {
97+
WriteErrorResponse(w, r, http.StatusBadRequest, "Invalid GUID", "GUID must be 32 hexadecimal characters", "Invalid GUID")
98+
return nil, false
99+
}
100+
101+
guid, err := hex.DecodeString(guidHex)
102+
if err != nil {
103+
WriteErrorResponse(w, r, http.StatusBadRequest, "Invalid GUID format", err.Error(), "Invalid GUID format")
104+
return nil, false
105+
}
106+
107+
return guid, true
108+
}

0 commit comments

Comments
 (0)