Skip to content

Commit 7654fc0

Browse files
camronkhaneshepelyuk
authored andcommitted
feat: add configurable header opts for JWKS request
1 parent b23661a commit 7654fc0

File tree

3 files changed

+80
-1
lines changed

3 files changed

+80
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ PayloadFields | The field-name in the JWT payload that are required (e.g. `exp`)
5252
Required | Is `Authorization` header with JWT token required for every request.
5353
Keys | Used to validate JWT signature. Multiple keys are supported. Allowed values include certificates, public keys, symmetric keys. In case the value is a valid URL, the plugin will fetch keys from the JWK endpoint.
5454
Alg | Used to verify which PKI algorithm is used in the JWT.
55+
JwksHeaders | Map used to add headers to a JWKS request (e.g. credentials for a 3rd party JWKS service).
5556
JwtHeaders | Map used to inject JWT payload fields as HTTP request headers.
5657
OpaHeaders | Map used to inject OPA result fields as HTTP request headers. Populated if request is allowed by OPA. Only 1st level keys from OPA document are supported.
5758
OpaResponseHeaders | Map used to inject OPA result fields as HTTP response headers. Populated if OPA response has `OpaAllowField` present, regardless of value. Only 1st level keys from OPA document are supported.

jwt.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Config struct {
3838
Alg string
3939
OpaHeaders map[string]string
4040
JwtHeaders map[string]string
41+
JwksHeaders map[string]string
4142
OpaResponseHeaders map[string]string
4243
OpaHttpStatusField string
4344
JwtCookieKey string
@@ -55,6 +56,7 @@ func CreateConfig() *Config {
5556

5657
// JwtPlugin contains the runtime config
5758
type JwtPlugin struct {
59+
httpClient *http.Client
5860
next http.Handler
5961
opaUrl string
6062
opaAllowField string
@@ -67,6 +69,7 @@ type JwtPlugin struct {
6769
alg string
6870
opaHeaders map[string]string
6971
jwtHeaders map[string]string
72+
jwksHeaders map[string]string
7073
opaResponseHeaders map[string]string
7174
opaHttpStatusField string
7275
jwtCookieKey string
@@ -163,6 +166,7 @@ type Response struct {
163166
// New creates a new plugin
164167
func New(_ context.Context, next http.Handler, config *Config, _ string) (http.Handler, error) {
165168
jwtPlugin := &JwtPlugin{
169+
httpClient: &http.Client{},
166170
next: next,
167171
opaUrl: config.OpaUrl,
168172
opaAllowField: config.OpaAllowField,
@@ -174,6 +178,7 @@ func New(_ context.Context, next http.Handler, config *Config, _ string) (http.H
174178
keys: make(map[string]interface{}),
175179
opaHeaders: config.OpaHeaders,
176180
jwtHeaders: config.JwtHeaders,
181+
jwksHeaders: config.JwksHeaders,
177182
opaResponseHeaders: config.OpaResponseHeaders,
178183
opaHttpStatusField: config.OpaHttpStatusField,
179184
jwtCookieKey: config.JwtCookieKey,
@@ -231,7 +236,15 @@ func (jwtPlugin *JwtPlugin) FetchKeys() {
231236
logInfo(fmt.Sprintf("FetchKeys - #%d jwkEndpoints to fetch", len(jwtPlugin.jwkEndpoints))).
232237
print()
233238
for _, u := range jwtPlugin.jwkEndpoints {
234-
response, err := http.Get(u.String())
239+
req, err := http.NewRequest("GET", u.String(), nil)
240+
if err != nil {
241+
logWarn("FetchKeys - Failed to create request").withUrl(u.String()).print()
242+
continue
243+
}
244+
for headerKey, headerValue := range jwtPlugin.jwksHeaders {
245+
req.Header.Add(headerKey, headerValue)
246+
}
247+
response, err := jwtPlugin.httpClient.Do(req)
235248
if err != nil {
236249
logWarn("FetchKeys - Failed to fetch keys").withUrl(u.String()).print()
237250
continue

jwt_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,3 +1122,68 @@ func TestTokenFromQueryConfiguredButNotInURL(t *testing.T) {
11221122
t.Fatal("next.ServeHTTP was called, but should not")
11231123
}
11241124
}
1125+
1126+
func TestJwksHeaders(t *testing.T) {
1127+
tests := []struct {
1128+
name string
1129+
jwksHeaders map[string]string
1130+
expected map[string]string
1131+
}{
1132+
{
1133+
name: "No Headers",
1134+
jwksHeaders: map[string]string{},
1135+
expected: map[string]string{},
1136+
},
1137+
{
1138+
name: "One Header",
1139+
jwksHeaders: map[string]string{"Content-Type": "application/json"},
1140+
expected: map[string]string{"Content-Type": "application/json"},
1141+
},
1142+
{
1143+
name: "Multiple Headers",
1144+
jwksHeaders: map[string]string{
1145+
"Authorization": "Bearer token",
1146+
"User-Agent": "Traefik",
1147+
},
1148+
expected: map[string]string{
1149+
"Authorization": "Bearer token",
1150+
"User-Agent": "Traefik",
1151+
},
1152+
},
1153+
}
1154+
1155+
for _, tt := range tests {
1156+
t.Run(tt.name, func(t *testing.T) {
1157+
cfg := Config{
1158+
JwksHeaders: tt.jwksHeaders,
1159+
}
1160+
ctx := context.Background()
1161+
1162+
jwtPlugin, err := New(ctx, nil, &cfg, "test-traefik-jwt-plugin")
1163+
if err != nil {
1164+
t.Fatal(err)
1165+
}
1166+
1167+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1168+
for key, val := range tt.expected {
1169+
if r.Header.Get(key) != val {
1170+
t.Errorf("Expected header %s to be %s, got %s", key, val, r.Header.Get(key))
1171+
}
1172+
}
1173+
}))
1174+
defer ts.Close()
1175+
1176+
jwtPlugin.(*JwtPlugin).jwkEndpoints = append(jwtPlugin.(*JwtPlugin).jwkEndpoints, mustParseUrl(ts.URL))
1177+
1178+
jwtPlugin.(*JwtPlugin).FetchKeys()
1179+
})
1180+
}
1181+
}
1182+
1183+
func mustParseUrl(urlStr string) *url.URL {
1184+
u, err := url.Parse(urlStr)
1185+
if err != nil {
1186+
panic(err)
1187+
}
1188+
return u
1189+
}

0 commit comments

Comments
 (0)