Skip to content

Commit 6051904

Browse files
committed
Support unknown verification of HMAC JWTs; include some claims in ExtraData
1 parent 93e6fa8 commit 6051904

File tree

1 file changed

+74
-21
lines changed

1 file changed

+74
-21
lines changed

pkg/detectors/jwt/jwt.go

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package jwt
22

33
import (
4+
"cmp"
45
"context"
56
"encoding/json"
67
"errors"
@@ -64,28 +65,59 @@ func limitReader(reader io.Reader) io.Reader {
6465

6566
// FromData will find and optionally verify JWT secrets in a given set of bytes.
6667
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
67-
dataStr := string(data)
68+
client := cmp.Or(s.client, defaultClient)
69+
seenMatches := make(map[string]bool)
6870

69-
uniqueMatches := make(map[string]struct{})
70-
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
71-
uniqueMatches[match[1]] = struct{}{}
72-
}
71+
for _, matchGroups := range keyPat.FindAllStringSubmatch(string(data), -1) {
72+
match := matchGroups[1]
73+
74+
if seenMatches[match] {
75+
continue
76+
}
77+
seenMatches[match] = true
78+
79+
parsedToken, _, err := jwt.NewParser(jwt.WithPaddingAllowed()).ParseUnverified(match, jwt.MapClaims{})
80+
if err != nil {
81+
// we can skip a token that doesn't parse without any validation or verification
82+
continue
83+
}
84+
85+
issString, _ := parsedToken.Claims.GetIssuer()
86+
87+
iatString := ""
88+
iat, err := parsedToken.Claims.GetIssuedAt()
89+
if err == nil && iat != nil {
90+
iatString = iat.String()
91+
}
92+
93+
expString := ""
94+
exp, err := parsedToken.Claims.GetExpirationTime()
95+
if err == nil && exp != nil {
96+
expString = exp.String()
97+
}
98+
99+
extraData := map[string]string{
100+
"iss": issString,
101+
"iat": iatString,
102+
"exp": expString,
103+
}
73104

74-
for match := range uniqueMatches {
75105
s1 := detectors.Result{
76106
DetectorType: detectorspb.DetectorType_JWT,
77107
Raw: []byte(match),
108+
ExtraData: extraData,
78109
}
79110

80111
if verify {
81-
client := s.client
82-
if client == nil {
83-
client = defaultClient
112+
var isVerified bool
113+
var verificationErr error
114+
switch parsedToken.Method.Alg() {
115+
case "HS256", "HS384", "HS512":
116+
isVerified, verificationErr = verifyHMAC(parsedToken)
117+
default:
118+
isVerified, verificationErr = verifyPublicKey(ctx, client, match)
84119
}
85-
86-
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
87120
s1.Verified = isVerified
88-
s1.ExtraData = extraData
89121
s1.SetVerificationError(verificationErr, match)
90122
}
91123

@@ -138,11 +170,31 @@ func performHttpRequest(ctx context.Context, client *http.Client, method string,
138170
return resp, nil
139171
}
140172

141-
// Attempt to verify a JWT.
173+
// Attempt to verify a JWT that uses an HMAC algorithm.
174+
//
175+
// This implementation only attempts to verify JWTs whose issuers use the OIDC Discovery protocol to make public keys available via request.
176+
func verifyHMAC(parsedToken *jwt.Token) (bool, error) {
177+
v := jwt.NewValidator(
178+
jwt.WithValidMethods([]string{
179+
jwt.SigningMethodHS256.Alg(),
180+
jwt.SigningMethodHS384.Alg(),
181+
jwt.SigningMethodHS512.Alg(),
182+
}),
183+
jwt.WithIssuedAt(),
184+
jwt.WithPaddingAllowed(),
185+
jwt.WithLeeway(time.Minute),
186+
)
187+
if err := v.Validate(parsedToken.Claims); err != nil {
188+
// though we have not checked the signature, the token is definitely invalid
189+
return false, nil
190+
}
191+
return false, fmt.Errorf("no key available to verify an HMAC-based signature")
192+
}
193+
194+
// Attempt to verify a JWT that uses a public-key signing algorithm.
142195
//
143-
// This implementation only attempts to verify JWTs that use an asymmetric encryption algorithm,
144-
// and only those whose issuers use the OIDC Discovery protocol to make public keys available via request.
145-
func verifyMatch(ctx context.Context, client *http.Client, tokenString string) (bool, map[string]string, error) {
196+
// This implementation only attempts to verify JWTs whose issuers use the OIDC Discovery protocol to make public keys available via request.
197+
func verifyPublicKey(ctx context.Context, client *http.Client, tokenString string) (bool, error) {
146198

147199
// A key retrieval function that uses the OIDC Discovery protocol,
148200
// being careful to avoid possible DoS from a potentially malicious JWKS server.
@@ -216,7 +268,8 @@ func verifyMatch(ctx context.Context, client *http.Client, tokenString string) (
216268

217269
// Parse matching key to the "raw" key type needed for signature verification
218270
var rawMatchingKey any
219-
err = jwk.Export(matchingKey, &rawMatchingKey); if err != nil {
271+
err = jwk.Export(matchingKey, &rawMatchingKey)
272+
if err != nil {
220273
return nil, fmt.Errorf("failed to export matching key: %w", err)
221274
}
222275

@@ -244,13 +297,13 @@ func verifyMatch(ctx context.Context, client *http.Client, tokenString string) (
244297
)
245298
switch {
246299
case token.Valid:
247-
return true, nil, nil
300+
return true, nil
248301
case errors.Is(err, jwt.ErrTokenUnverifiable):
249-
return false, nil, err
302+
return false, err
250303
case errors.Is(err, jwt.ErrHashUnavailable):
251-
return false, nil, err
304+
return false, err
252305
default:
253-
return false, nil, nil
306+
return false, nil
254307
}
255308
}
256309

0 commit comments

Comments
 (0)