Skip to content
This repository was archived by the owner on Nov 24, 2025. It is now read-only.

Commit 8fdf861

Browse files
authored
Merge pull request #7 from wahyd4/wildcard-domain
Support wildcard domain
2 parents 5954381 + c385729 commit 8fdf861

10 files changed

Lines changed: 426 additions & 195 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ webauthn:
7070
cors:
7171
allowed_origins:
7272
- "https://your-domain.com" # Your domain with protocol
73+
# Wildcard domains are supported for subdomains:
74+
- "*.your-domain.com" # Matches api.your-domain.com, app.your-domain.com, etc.
7375

7476
auth:
7577
session_secret: "your-secure-secret-key" # Generate a secure random string

cors_integration_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package main
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"passkey-auth/internal/cors"
9+
)
10+
11+
func TestWildcardCORSIntegration(t *testing.T) {
12+
// Create a simple test handler
13+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14+
w.WriteHeader(http.StatusOK)
15+
w.Write([]byte("OK"))
16+
})
17+
18+
// Create CORS middleware with wildcard support
19+
corsMiddleware := cors.WildcardCORS(cors.Config{
20+
AllowedOrigins: []string{"*.junv.cc", "https://static.example.com"},
21+
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
22+
AllowedHeaders: []string{"*"},
23+
AllowCredentials: true,
24+
})
25+
26+
// Wrap the test handler
27+
handler := corsMiddleware(testHandler)
28+
29+
tests := []struct {
30+
name string
31+
origin string
32+
expectAllowed bool
33+
expectedOrigin string
34+
}{
35+
{
36+
name: "wildcard subdomain match",
37+
origin: "https://api.junv.cc",
38+
expectAllowed: true,
39+
expectedOrigin: "https://api.junv.cc",
40+
},
41+
{
42+
name: "wildcard base domain match",
43+
origin: "https://junv.cc",
44+
expectAllowed: true,
45+
expectedOrigin: "https://junv.cc",
46+
},
47+
{
48+
name: "static domain match",
49+
origin: "https://static.example.com",
50+
expectAllowed: true,
51+
},
52+
{
53+
name: "no match",
54+
origin: "https://evil.com",
55+
expectAllowed: false,
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
// Create a preflight OPTIONS request
62+
req := httptest.NewRequest("OPTIONS", "/", nil)
63+
req.Header.Set("Origin", tt.origin)
64+
req.Header.Set("Access-Control-Request-Method", "POST")
65+
66+
// Record the response
67+
w := httptest.NewRecorder()
68+
handler.ServeHTTP(w, req)
69+
70+
// Check CORS headers
71+
allowOriginHeader := w.Header().Get("Access-Control-Allow-Origin")
72+
73+
if tt.expectAllowed {
74+
if allowOriginHeader == "" {
75+
t.Errorf("Expected Access-Control-Allow-Origin header, but got none")
76+
}
77+
78+
// For wildcard matches, should return the specific origin
79+
if tt.expectedOrigin != "" && allowOriginHeader != tt.expectedOrigin {
80+
t.Errorf("Expected Access-Control-Allow-Origin: %s, got: %s", tt.expectedOrigin, allowOriginHeader)
81+
}
82+
} else {
83+
if allowOriginHeader != "" {
84+
t.Errorf("Expected no Access-Control-Allow-Origin header, but got: %s", allowOriginHeader)
85+
}
86+
}
87+
})
88+
}
89+
}

internal/cors/middleware.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package cors
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
"github.com/rs/cors"
8+
)
9+
10+
// Config holds the CORS configuration with wildcard support
11+
type Config struct {
12+
AllowedOrigins []string
13+
AllowedMethods []string
14+
AllowedHeaders []string
15+
AllowCredentials bool
16+
}
17+
18+
// WildcardCORS creates a CORS handler with wildcard domain support
19+
func WildcardCORS(config Config) func(http.Handler) http.Handler {
20+
return func(next http.Handler) http.Handler {
21+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22+
origin := r.Header.Get("Origin")
23+
24+
// Determine allowed origins for this request
25+
var allowedOrigins []string
26+
27+
// Separate wildcard and static origins
28+
var wildcardPatterns []string
29+
var staticOrigins []string
30+
31+
for _, configuredOrigin := range config.AllowedOrigins {
32+
if strings.Contains(configuredOrigin, "*") {
33+
wildcardPatterns = append(wildcardPatterns, configuredOrigin)
34+
} else {
35+
staticOrigins = append(staticOrigins, configuredOrigin)
36+
}
37+
}
38+
39+
// Check if origin matches any wildcard pattern
40+
if len(wildcardPatterns) > 0 && origin != "" {
41+
wildcardMatcher := NewWildcardMatcher(wildcardPatterns)
42+
if wildcardMatcher.MatchOrigin(origin) {
43+
// For wildcard matches, allow the specific origin
44+
allowedOrigins = []string{origin}
45+
}
46+
}
47+
48+
// If no wildcard match, use static origins
49+
if len(allowedOrigins) == 0 {
50+
allowedOrigins = staticOrigins
51+
} else {
52+
// If we had a wildcard match, also include static origins
53+
allowedOrigins = append(allowedOrigins, staticOrigins...)
54+
}
55+
56+
// Create a new CORS instance for this request with the determined origins
57+
c := cors.New(cors.Options{
58+
AllowedOrigins: allowedOrigins,
59+
AllowedMethods: config.AllowedMethods,
60+
AllowedHeaders: config.AllowedHeaders,
61+
AllowCredentials: config.AllowCredentials,
62+
})
63+
64+
// Use the rs/cors handler
65+
c.Handler(next).ServeHTTP(w, r)
66+
})
67+
}
68+
}

internal/cors/wildcard.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package cors
2+
3+
import (
4+
"strings"
5+
)
6+
7+
// WildcardMatcher provides wildcard domain matching for CORS origins
8+
type WildcardMatcher struct {
9+
patterns []string
10+
}
11+
12+
// NewWildcardMatcher creates a new wildcard matcher with the given patterns
13+
func NewWildcardMatcher(patterns []string) *WildcardMatcher {
14+
return &WildcardMatcher{
15+
patterns: patterns,
16+
}
17+
}
18+
19+
// MatchOrigin checks if the given origin matches any of the wildcard patterns
20+
func (m *WildcardMatcher) MatchOrigin(origin string) bool {
21+
for _, pattern := range m.patterns {
22+
if m.matchPattern(origin, pattern) {
23+
return true
24+
}
25+
}
26+
return false
27+
}
28+
29+
// matchPattern checks if origin matches a specific pattern
30+
// Supports patterns like:
31+
// - "*.example.com" matches "api.example.com", "auth.example.com", etc.
32+
// - "*.*.example.com" matches "api.v1.example.com", etc.
33+
// - "example.com" matches exactly "example.com"
34+
func (m *WildcardMatcher) matchPattern(origin, pattern string) bool {
35+
// Remove protocol from origin if present
36+
origin = strings.TrimPrefix(origin, "https://")
37+
origin = strings.TrimPrefix(origin, "http://")
38+
39+
// Remove port if present
40+
if colonIndex := strings.LastIndex(origin, ":"); colonIndex != -1 && colonIndex > strings.LastIndex(origin, "]") {
41+
origin = origin[:colonIndex]
42+
}
43+
44+
// Exact match
45+
if origin == pattern {
46+
return true
47+
}
48+
49+
// Wildcard match
50+
if strings.Contains(pattern, "*") {
51+
return m.wildcardMatch(origin, pattern)
52+
}
53+
54+
return false
55+
}
56+
57+
// wildcardMatch performs wildcard matching
58+
func (m *WildcardMatcher) wildcardMatch(origin, pattern string) bool {
59+
// Handle simple case: *.domain.com
60+
if strings.HasPrefix(pattern, "*.") {
61+
suffix := pattern[2:] // Remove "*."
62+
63+
// Check if origin ends with the suffix and has at least one subdomain
64+
if strings.HasSuffix(origin, "."+suffix) {
65+
// Ensure there's a subdomain (not just the suffix itself)
66+
prefix := strings.TrimSuffix(origin, "."+suffix)
67+
// Make sure the prefix doesn't contain dots (single-level subdomain wildcard)
68+
// If you want multi-level subdomains, remove this check
69+
return !strings.Contains(prefix, ".")
70+
}
71+
72+
// Also check if origin exactly matches the suffix (without subdomain)
73+
return origin == suffix
74+
}
75+
76+
// For more complex patterns, we could implement more sophisticated matching
77+
// For now, handle the common *.domain.com case
78+
return false
79+
}
80+
81+
// GetAllowedOrigins returns the actual allowed origins for a request
82+
// This expands wildcard patterns based on the request origin
83+
func (m *WildcardMatcher) GetAllowedOrigins(requestOrigin string, staticOrigins []string) []string {
84+
allowedOrigins := make([]string, 0, len(staticOrigins))
85+
86+
for _, origin := range staticOrigins {
87+
if strings.Contains(origin, "*") {
88+
// This is a wildcard pattern
89+
if m.matchPattern(requestOrigin, origin) {
90+
// Add the actual request origin instead of the pattern
91+
allowedOrigins = append(allowedOrigins, requestOrigin)
92+
}
93+
} else {
94+
// This is a static origin, add as-is
95+
allowedOrigins = append(allowedOrigins, origin)
96+
}
97+
}
98+
99+
return allowedOrigins
100+
}

0 commit comments

Comments
 (0)