diff --git a/jws/jws.go b/jws/jws.go index 6f03a49d3..27ab06139 100644 --- a/jws/jws.go +++ b/jws/jws.go @@ -116,12 +116,12 @@ func (h *Header) encode() (string, error) { // Decode decodes a claim set from a JWS payload. func Decode(payload string) (*ClaimSet, error) { // decode returned id token to get expiry - s := strings.Split(payload, ".") - if len(s) < 2 { + _, claims, _, ok := parseToken(payload) + if !ok { // TODO(jbd): Provide more context about the error. return nil, errors.New("jws: invalid token received") } - decoded, err := base64.RawURLEncoding.DecodeString(s[1]) + decoded, err := base64.RawURLEncoding.DecodeString(claims) if err != nil { return nil, err } @@ -165,18 +165,34 @@ func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) { // Verify tests whether the provided JWT token's signature was produced by the private key // associated with the supplied public key. func Verify(token string, key *rsa.PublicKey) error { - if strings.Count(token, ".") != 2 { + header, claims, sig, ok := parseToken(token) + if !ok { return errors.New("jws: invalid token received, token must have 3 parts") } - - parts := strings.SplitN(token, ".", 3) - signedContent := parts[0] + "." + parts[1] - signatureString, err := base64.RawURLEncoding.DecodeString(parts[2]) + signatureString, err := base64.RawURLEncoding.DecodeString(sig) if err != nil { return err } h := sha256.New() - h.Write([]byte(signedContent)) + h.Write([]byte(header + tokenDelim + claims)) return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString) } + +func parseToken(s string) (header, claims, sig string, ok bool) { + header, s, ok = strings.Cut(s, tokenDelim) + if !ok { // no period found + return "", "", "", false + } + claims, s, ok = strings.Cut(s, tokenDelim) + if !ok { // only one period found + return "", "", "", false + } + sig, _, ok = strings.Cut(s, tokenDelim) + if ok { // three periods found + return "", "", "", false + } + return header, claims, sig, true +} + +const tokenDelim = "." diff --git a/jws/jws_test.go b/jws/jws_test.go index 39a136a29..1776f56b8 100644 --- a/jws/jws_test.go +++ b/jws/jws_test.go @@ -7,6 +7,8 @@ package jws import ( "crypto/rand" "crypto/rsa" + "net/http" + "strings" "testing" ) @@ -39,8 +41,57 @@ func TestSignAndVerify(t *testing.T) { } func TestVerifyFailsOnMalformedClaim(t *testing.T) { - err := Verify("abc.def", nil) - if err == nil { - t.Error("got no errors; want improperly formed JWT not to be verified") + cases := []struct { + desc string + token string + }{ + { + desc: "no periods", + token: "aa", + }, { + desc: "only one period", + token: "a.a", + }, { + desc: "more than two periods", + token: "a.a.a.a", + }, + } + for _, tc := range cases { + f := func(t *testing.T) { + err := Verify(tc.token, nil) + if err == nil { + t.Error("got no errors; want improperly formed JWT not to be verified") + } + } + t.Run(tc.desc, f) + } +} + +func BenchmarkVerify(b *testing.B) { + cases := []struct { + desc string + token string + }{ + { + desc: "full of periods", + token: strings.Repeat(".", http.DefaultMaxHeaderBytes), + }, { + desc: "two trailing periods", + token: strings.Repeat("a", http.DefaultMaxHeaderBytes-2) + "..", + }, + } + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + b.Fatal(err) + } + for _, bc := range cases { + f := func(b *testing.B) { + b.ReportAllocs() + b.ResetTimer() + for range b.N { + Verify(bc.token, &privateKey.PublicKey) + } + } + b.Run(bc.desc, f) } } diff --git a/token.go b/token.go index 109997d77..8c31136c4 100644 --- a/token.go +++ b/token.go @@ -169,7 +169,7 @@ func tokenFromInternal(t *internal.Token) *Token { // retrieveToken takes a *Config and uses that to retrieve an *internal.Token. // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along -// with an error.. +// with an error. func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) { tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get()) if err != nil {