Skip to content

Commit d5eae8e

Browse files
authored
Merge pull request #92 from FIWARE/ticket-33/work
Added configurable CORS origins to VCVerifier
2 parents 4067432 + f13ad71 commit d5eae8e

8 files changed

Lines changed: 271 additions & 2 deletions

File tree

IMPLEMENTATION_PLAN.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Implementation Plan: VCVerifier should allow to configure CORS headers
2+
3+
## Overview
4+
5+
The VCVerifier currently has a hardcoded CORS configuration in `main.go` (lines 51–57) that allows all origins (`*`), only `POST`/`GET` methods, and a fixed set of headers. The ticket requests making CORS origins configurable as part of the service configuration (`ConfiguredService` in `config/configClient.go`), including support for the wildcard origin. This plan adds an `AllowedOrigins` field to `ConfiguredService`, updates the CORS middleware to read from configuration, and preserves backward compatibility (wildcard by default when nothing is configured).
6+
7+
## Steps
8+
9+
### Step 1: Add CORS configuration field to ConfiguredService and update config parsing
10+
11+
**Goal:** Extend the `ConfiguredService` struct to accept an `allowedOrigins` list, so each service can declare which origins are permitted.
12+
13+
**Files to modify:**
14+
- `config/configClient.go` — Add `AllowedOrigins []string` field (with `json:"allowedOrigins,omitempty" mapstructure:"allowedOrigins,omitempty"`) to the `ConfiguredService` struct (line 40).
15+
16+
**Acceptance criteria:**
17+
- `ConfiguredService` has a new `AllowedOrigins []string` field with appropriate `json` and `mapstructure` tags.
18+
- The field is optional (`omitempty`); when absent, it defaults to an empty/nil slice (meaning "no restriction specified by this service").
19+
- The field is documented with a GoDoc comment explaining its purpose and that `["*"]` means allow all origins.
20+
21+
### Step 2: Wire CORS middleware to use configured origins from all services
22+
23+
**Goal:** Replace the hardcoded CORS config in `main.go` with logic that aggregates `AllowedOrigins` from all configured services and passes them to the `gin-contrib/cors` middleware. When no origins are configured anywhere (or no services exist), fall back to the current wildcard behavior for backward compatibility.
24+
25+
**Files to modify:**
26+
- `main.go` — Update the CORS middleware setup (lines 51–57) to:
27+
1. Accept the `Configuration` struct.
28+
2. Collect all `AllowedOrigins` values from `configuration.ConfigRepo.Services`.
29+
3. Deduplicate the collected origins.
30+
4. If the aggregated list is empty (no services configured any origins), default to `["*"]` for backward compatibility.
31+
5. If `"*"` is present in the aggregated list, use `["*"]` (wildcard takes precedence).
32+
6. Pass the resolved origins to `cors.Config.AllowOrigins`.
33+
34+
**Design notes:**
35+
- Extract the CORS origin resolution into a dedicated, exported helper function (e.g., `func ResolveAllowedOrigins(services []config.ConfiguredService) []string`) in `main.go` or a small utility so it can be unit-tested independently.
36+
- Keep `AllowMethods`, `AllowHeaders`, and `AllowCredentials` at their current hardcoded values — the ticket only asks for origin configuration.
37+
- Note: `gin-contrib/cors` does not allow `AllowCredentials: true` with `AllowOrigins: ["*"]`. The current code has this combination, which means credentials are effectively not sent cross-origin. Maintain this existing behavior for now — do not change `AllowCredentials`. If the wildcard is resolved, keep the same config as today.
38+
39+
**Acceptance criteria:**
40+
- When no `allowedOrigins` are set on any service, CORS behaves identically to today (wildcard).
41+
- When services specify origins, only those origins are allowed.
42+
- When any service specifies `"*"`, the wildcard is used.
43+
- The helper function is exported and documented.
44+
45+
### Step 3: Update test fixtures and add unit tests
46+
47+
**Goal:** Add test coverage for the new configuration field parsing and the CORS origin resolution logic.
48+
49+
**Files to modify/create:**
50+
- `config/data/config_test.yaml` — Add `allowedOrigins` to the existing test service entry.
51+
- `config/provider_test.go` — Update the `Test_ReadConfig` table-driven test's expected `ConfiguredService` to include the new `AllowedOrigins` field.
52+
- `main_test.go` (new file, or add to existing test file if one exists) — Add parameterized tests for the `ResolveAllowedOrigins` helper function covering:
53+
- No services → returns `["*"]`
54+
- Services with no `allowedOrigins` set → returns `["*"]`
55+
- Single service with specific origins → returns those origins
56+
- Multiple services with different origins → returns deduplicated union
57+
- Any service includes `"*"` → returns `["*"]`
58+
- Duplicate origins across services → deduplicated
59+
60+
**Files to modify:**
61+
- `config/data/config_test.yaml` — Add `allowedOrigins: ["https://example.com"]` under the test service.
62+
- `config/provider_test.go` — Update expected struct to include `AllowedOrigins: []string{"https://example.com"}`.
63+
- `main_test.go` — New file with parameterized table-driven tests for `ResolveAllowedOrigins`.
64+
65+
**Acceptance criteria:**
66+
- `go test ./config/... -v` passes with the updated fixture and expected values.
67+
- `go test ./... -v` passes, including the new `ResolveAllowedOrigins` tests.
68+
- Tests cover all edge cases listed above using table-driven test pattern.
69+
70+
### Step 4: Update example configuration and documentation
71+
72+
**Goal:** Update `server.yaml` to document the new `allowedOrigins` option so operators know it exists.
73+
74+
**Files to modify:**
75+
- `server.yaml` — Add a commented-out `allowedOrigins` example under the `configRepo.services` section showing usage (e.g., `# allowedOrigins: ["https://my-app.example.com"]`). If `configRepo.services` is not present in `server.yaml`, add a commented-out example block.
76+
77+
**Acceptance criteria:**
78+
- `server.yaml` contains a clear, commented example showing how to configure `allowedOrigins` for a service, including a note that `["*"]` is the default when omitted.
79+
- The application still starts correctly with the updated `server.yaml` (no parse errors from comments).

VCVerifier

26.4 MB
Binary file not shown.

config/configClient.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ type ConfiguredService struct {
4444
Id string `json:"id" mapstructure:"id"`
4545
AuthorizationType string `json:"authorizationType,omitempty" mapstructure:"authorizationType,omitempty"`
4646
AuthorizationPath string `json:"authorizationPath,omitempty" mapstructure:"authorizationPath,omitempty"`
47+
// AllowedOrigins specifies the list of origins permitted for CORS requests
48+
// to this service. When empty or nil, no service-specific restriction is
49+
// applied and the verifier falls back to the global default (wildcard).
50+
// Set to ["*"] to explicitly allow all origins for this service.
51+
AllowedOrigins []string `json:"allowedOrigins,omitempty" mapstructure:"allowedOrigins,omitempty"`
4752
}
4853

4954
type ScopeEntry struct {

config/data/config_test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ configRepo:
3636
services:
3737
- id: testService
3838
defaultOidcScope: someScope
39+
allowedOrigins: ["https://example.com"]
3940
oidcScopes:
4041
someScope:
4142
credentials:

config/provider_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func Test_ReadConfig(t *testing.T) {
7272
{
7373
Id: "testService",
7474
DefaultOidcScope: "someScope",
75+
AllowedOrigins: []string{"https://example.com"},
7576
ServiceScopes: map[string]ScopeEntry{
7677
"someScope": {
7778
Credentials: []Credential{

main.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ func main() {
4848
// health check
4949
router.GET("/health", HealthReq)
5050

51+
allowedOrigins := ResolveAllowedOrigins(configuration.ConfigRepo.Services)
52+
logger.Infof("CORS allowed origins: %v", allowedOrigins)
53+
5154
router.Use(cors.New(cors.Config{
52-
// we need to allow all, since we do not know the potential origin of a wallet
53-
AllowOrigins: []string{"*"},
55+
AllowOrigins: allowedOrigins,
5456
AllowMethods: []string{"POST", "GET"},
5557
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
5658
AllowCredentials: true,
@@ -138,3 +140,39 @@ func init() {
138140
}
139141
logging.Log().Infof("Will read config from %s", configFile)
140142
}
143+
144+
// wildcardOrigin is the CORS origin value that permits requests from any origin.
145+
const wildcardOrigin = "*"
146+
147+
// ResolveAllowedOrigins aggregates the AllowedOrigins from all configured
148+
// services into a deduplicated list of CORS origins. The rules are:
149+
//
150+
// - If no services are provided, or none of them specify any AllowedOrigins,
151+
// the function returns ["*"] (wildcard) for backward compatibility.
152+
// - If any service includes "*" in its AllowedOrigins, the function returns
153+
// ["*"] because the wildcard takes precedence over specific origins.
154+
// - Otherwise the function returns the deduplicated union of all origins.
155+
func ResolveAllowedOrigins(services []configModel.ConfiguredService) []string {
156+
seen := make(map[string]struct{})
157+
var origins []string
158+
159+
for _, svc := range services {
160+
for _, origin := range svc.AllowedOrigins {
161+
if origin == wildcardOrigin {
162+
// Wildcard takes precedence — no need to collect further.
163+
return []string{wildcardOrigin}
164+
}
165+
if _, exists := seen[origin]; !exists {
166+
seen[origin] = struct{}{}
167+
origins = append(origins, origin)
168+
}
169+
}
170+
}
171+
172+
// No origins configured at all — default to wildcard for backward compatibility.
173+
if len(origins) == 0 {
174+
return []string{wildcardOrigin}
175+
}
176+
177+
return origins
178+
}

main_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"sort"
6+
"testing"
7+
8+
"github.com/fiware/VCVerifier/config"
9+
)
10+
11+
func TestResolveAllowedOrigins(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
services []config.ConfiguredService
15+
want []string
16+
}{
17+
{
18+
name: "no services returns wildcard",
19+
services: nil,
20+
want: []string{"*"},
21+
},
22+
{
23+
name: "empty services slice returns wildcard",
24+
services: []config.ConfiguredService{},
25+
want: []string{"*"},
26+
},
27+
{
28+
name: "services with no allowedOrigins returns wildcard",
29+
services: []config.ConfiguredService{
30+
{Id: "svc1"},
31+
{Id: "svc2"},
32+
},
33+
want: []string{"*"},
34+
},
35+
{
36+
name: "services with empty allowedOrigins returns wildcard",
37+
services: []config.ConfiguredService{
38+
{Id: "svc1", AllowedOrigins: []string{}},
39+
},
40+
want: []string{"*"},
41+
},
42+
{
43+
name: "single service with specific origins",
44+
services: []config.ConfiguredService{
45+
{Id: "svc1", AllowedOrigins: []string{"https://example.com", "https://app.example.com"}},
46+
},
47+
want: []string{"https://example.com", "https://app.example.com"},
48+
},
49+
{
50+
name: "multiple services with different origins returns deduplicated union",
51+
services: []config.ConfiguredService{
52+
{Id: "svc1", AllowedOrigins: []string{"https://alpha.com"}},
53+
{Id: "svc2", AllowedOrigins: []string{"https://beta.com"}},
54+
},
55+
want: []string{"https://alpha.com", "https://beta.com"},
56+
},
57+
{
58+
name: "duplicate origins across services are deduplicated",
59+
services: []config.ConfiguredService{
60+
{Id: "svc1", AllowedOrigins: []string{"https://shared.com", "https://alpha.com"}},
61+
{Id: "svc2", AllowedOrigins: []string{"https://shared.com", "https://beta.com"}},
62+
},
63+
want: []string{"https://shared.com", "https://alpha.com", "https://beta.com"},
64+
},
65+
{
66+
name: "any service with wildcard returns wildcard only",
67+
services: []config.ConfiguredService{
68+
{Id: "svc1", AllowedOrigins: []string{"https://example.com"}},
69+
{Id: "svc2", AllowedOrigins: []string{"*"}},
70+
},
71+
want: []string{"*"},
72+
},
73+
{
74+
name: "first service with wildcard short-circuits",
75+
services: []config.ConfiguredService{
76+
{Id: "svc1", AllowedOrigins: []string{"*"}},
77+
{Id: "svc2", AllowedOrigins: []string{"https://example.com"}},
78+
},
79+
want: []string{"*"},
80+
},
81+
{
82+
name: "wildcard mixed within origins of a single service",
83+
services: []config.ConfiguredService{
84+
{Id: "svc1", AllowedOrigins: []string{"https://example.com", "*", "https://other.com"}},
85+
},
86+
want: []string{"*"},
87+
},
88+
{
89+
name: "mix of configured and unconfigured services",
90+
services: []config.ConfiguredService{
91+
{Id: "svc1"},
92+
{Id: "svc2", AllowedOrigins: []string{"https://example.com"}},
93+
{Id: "svc3", AllowedOrigins: []string{}},
94+
},
95+
want: []string{"https://example.com"},
96+
},
97+
}
98+
99+
for _, tt := range tests {
100+
t.Run(tt.name, func(t *testing.T) {
101+
got := ResolveAllowedOrigins(tt.services)
102+
103+
// Sort both slices for order-independent comparison when not testing
104+
// wildcard (wildcard is always a single element so order is irrelevant).
105+
if len(got) > 1 || len(tt.want) > 1 {
106+
sortedGot := make([]string, len(got))
107+
copy(sortedGot, got)
108+
sort.Strings(sortedGot)
109+
110+
sortedWant := make([]string, len(tt.want))
111+
copy(sortedWant, tt.want)
112+
sort.Strings(sortedWant)
113+
114+
if !reflect.DeepEqual(sortedGot, sortedWant) {
115+
t.Errorf("ResolveAllowedOrigins() = %v, want %v", got, tt.want)
116+
}
117+
} else if !reflect.DeepEqual(got, tt.want) {
118+
t.Errorf("ResolveAllowedOrigins() = %v, want %v", got, tt.want)
119+
}
120+
})
121+
}
122+
}

server.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,26 @@ verifier:
2222
registryAddress: https://registry.gaia-x.fiware.dev/development/api/complianceIssuers
2323
ssiKit:
2424
auditorURL: http://my-auditor
25+
26+
# configRepo defines the service configurations with their scopes and trust endpoints.
27+
# Each service under configRepo.services can optionally specify allowedOrigins to control
28+
# which origins are permitted for CORS requests. When omitted or empty, the default
29+
# behavior is to allow all origins (["*"]) for backward compatibility.
30+
#
31+
# configRepo:
32+
# services:
33+
# - id: my-service
34+
# defaultOidcScope: defaultScope
35+
# # allowedOrigins restricts CORS requests to the listed origins for this service.
36+
# # Origins from all services are merged into a single deduplicated list.
37+
# # If any service includes "*", all origins are allowed (wildcard).
38+
# # If omitted or empty, defaults to ["*"] (allow all origins).
39+
# allowedOrigins:
40+
# - "https://my-app.example.com"
41+
# - "https://admin.example.com"
42+
# oidcScopes:
43+
# defaultScope:
44+
# credentials:
45+
# - type: VerifiableCredential
46+
# trustedIssuersLists:
47+
# - https://tir.example.com

0 commit comments

Comments
 (0)