Skip to content

Commit 94d0c46

Browse files
hlab-pawateshepelyuk
authored andcommitted
feat: validate aud payload
1 parent 7b34924 commit 94d0c46

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

jwt.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ type Config struct {
4949
JwtCookieKey string // Deprecated: use JwtSources instead
5050
JwtQueryKey string // Deprecated: use JwtSources instead
5151
JwtSources []map[string]string
52+
Aud string
5253
}
5354

5455
// CreateConfig creates a new OPA Config
@@ -79,6 +80,7 @@ type JwtPlugin struct {
7980
opaResponseHeaders map[string]string
8081
opaHttpStatusField string
8182
jwtSources []map[string]string
83+
aud string
8284

8385
name string
8486
keysLock sync.RWMutex
@@ -192,6 +194,7 @@ func New(ctx context.Context, next http.Handler, config *Config, pluginName stri
192194
opaResponseHeaders: config.OpaResponseHeaders,
193195
opaHttpStatusField: config.OpaHttpStatusField,
194196
jwtSources: config.JwtSources,
197+
aud: config.Aud,
195198
name: pluginName,
196199
}
197200
// use default order if jwtSourceOrder is set
@@ -473,6 +476,43 @@ func (jwtPlugin *JwtPlugin) CheckToken(request *http.Request, rw http.ResponseWr
473476
return 0, fmt.Errorf("token not valid yet")
474477
}
475478
}
479+
if fieldName == "aud" && jwtPlugin.aud != "" {
480+
audValue := jwtToken.Payload["aud"]
481+
switch v := audValue.(type) {
482+
case string:
483+
if v != jwtPlugin.aud {
484+
logError(fmt.Sprintf("Token audience mismatch, expected %s got %s", jwtPlugin.aud, v)).
485+
withSub(sub).
486+
withUrl(request.URL.String()).
487+
withNetwork(jwtPlugin.remoteAddr(request)).
488+
print()
489+
return 0, fmt.Errorf("token audience mismatch")
490+
}
491+
case []interface{}:
492+
found := false
493+
for _, a := range v {
494+
if aStr, ok := a.(string); ok && aStr == jwtPlugin.aud {
495+
found = true
496+
break
497+
}
498+
}
499+
if !found {
500+
logError(fmt.Sprintf("Token audience not found in list, expected %s", jwtPlugin.aud)).
501+
withSub(sub).
502+
withUrl(request.URL.String()).
503+
withNetwork(jwtPlugin.remoteAddr(request)).
504+
print()
505+
return 0, fmt.Errorf("token audience not found in list")
506+
}
507+
default:
508+
logError("Token audience has invalid type").
509+
withSub(sub).
510+
withUrl(request.URL.String()).
511+
withNetwork(jwtPlugin.remoteAddr(request)).
512+
print()
513+
return 0, fmt.Errorf("token audience has invalid type")
514+
}
515+
}
476516
}
477517
for k, v := range jwtPlugin.jwtHeaders {
478518
_, ok := jwtToken.Payload[v]

jwt_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,3 +1311,107 @@ func mustParseUrl(urlStr string) *url.URL {
13111311
}
13121312
return u
13131313
}
1314+
1315+
func TestServeHTTPAudience(t *testing.T) {
1316+
tests := []struct {
1317+
Name string
1318+
Fields []string
1319+
Aud string
1320+
Claims string
1321+
err string
1322+
nextCalled bool
1323+
}{
1324+
{
1325+
Name: "valid string audience",
1326+
Fields: []string{"aud"},
1327+
Aud: "my-api",
1328+
Claims: `{"aud": "my-api"}`,
1329+
err: "",
1330+
nextCalled: true,
1331+
},
1332+
{
1333+
Name: "valid array audience",
1334+
Fields: []string{"aud"},
1335+
Aud: "my-api",
1336+
Claims: `{"aud": ["other-api", "my-api"]}`,
1337+
err: "",
1338+
nextCalled: true,
1339+
},
1340+
{
1341+
Name: "invalid string audience",
1342+
Fields: []string{"aud"},
1343+
Aud: "my-api",
1344+
Claims: `{"aud": "other-api"}`,
1345+
err: "token audience mismatch",
1346+
nextCalled: false,
1347+
},
1348+
{
1349+
Name: "invalid array audience",
1350+
Fields: []string{"aud"},
1351+
Aud: "my-api",
1352+
Claims: `{"aud": ["other-api", "another-api"]}`,
1353+
err: "token audience not found in list",
1354+
nextCalled: false,
1355+
},
1356+
{
1357+
Name: "missing audience when required",
1358+
Fields: []string{"aud"},
1359+
Aud: "my-api",
1360+
Claims: `{}`,
1361+
err: "payload missing required field aud",
1362+
nextCalled: false,
1363+
},
1364+
{
1365+
Name: "audience not configured",
1366+
Fields: []string{"aud"},
1367+
Aud: "",
1368+
Claims: `{"aud": "my-api"}`,
1369+
err: "",
1370+
nextCalled: true,
1371+
},
1372+
{
1373+
Name: "invalid audience type",
1374+
Fields: []string{"aud"},
1375+
Aud: "my-api",
1376+
Claims: `{"aud": 123}`,
1377+
err: "token audience has invalid type",
1378+
nextCalled: false,
1379+
},
1380+
}
1381+
1382+
for _, tt := range tests {
1383+
t.Run(tt.Name, func(t *testing.T) {
1384+
ctx := context.Background()
1385+
nextCalled := false
1386+
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true })
1387+
1388+
jwt, err := New(ctx, next, &Config{
1389+
PayloadFields: tt.Fields,
1390+
Aud: tt.Aud,
1391+
}, "test-traefik-jwt-plugin")
1392+
if err != nil {
1393+
t.Fatal(err)
1394+
}
1395+
1396+
recorder := httptest.NewRecorder()
1397+
1398+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
1399+
if err != nil {
1400+
t.Fatal(err)
1401+
}
1402+
1403+
req.Header["Authorization"] = []string{"Bearer eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9." + base64.RawURLEncoding.EncodeToString([]byte(tt.Claims)) + ".JlX3gXGyClTBFciHhknWrjo7SKqyJ5iBO0n-3S2_I7cIgfaZAeRDJ3SQEbaPxVC7X8aqGCOM-pQOjZPKUJN8DMFrlHTOdqMs0TwQ2PRBmVAxXTSOZOoEhD4ZNCHohYoyfoDhJDP4Qye_FCqu6POJzg0Jcun4d3KW04QTiGxv2PkYqmB7nHxYuJdnqE3704hIS56pc_8q6AW0WIT0W-nIvwzaSbtBU9RgaC7ZpBD2LiNE265UBIFraMDF8IAFw9itZSUCTKg1Q-q27NwwBZNGYStMdIBDor2Bsq5ge51EkWajzZ7ALisVp-bskzUsqUf77ejqX_CBAqkNdH1Zebn93A"}
1404+
1405+
jwt.ServeHTTP(recorder, req)
1406+
1407+
if tt.nextCalled != nextCalled {
1408+
t.Fatalf("Expected next.ServeHTTP called: %v, got: %v", tt.nextCalled, nextCalled)
1409+
}
1410+
if tt.err != "" {
1411+
if strings.TrimSpace(recorder.Body.String()) != tt.err {
1412+
t.Fatalf("Expected error: %s, got: %s", tt.err, recorder.Body.String())
1413+
}
1414+
}
1415+
})
1416+
}
1417+
}

0 commit comments

Comments
 (0)