diff --git a/jwt_test.go b/jwt_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b01e899d30d583aa915e5601cbcdcdc397214b3d
--- /dev/null
+++ b/jwt_test.go
@@ -0,0 +1,89 @@
+package jwt
+
+import (
+	"testing"
+)
+
+func TestSplitToken(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    string
+		expected []string
+		isValid  bool
+	}{
+		{
+			name:     "valid token with three parts",
+			input:    "header.claims.signature",
+			expected: []string{"header", "claims", "signature"},
+			isValid:  true,
+		},
+		{
+			name:     "invalid token with two parts only",
+			input:    "header.claims",
+			expected: nil,
+			isValid:  false,
+		},
+		{
+			name:     "invalid token with one part only",
+			input:    "header",
+			expected: nil,
+			isValid:  false,
+		},
+		{
+			name:     "invalid token with extra delimiter",
+			input:    "header.claims.signature.extra",
+			expected: nil,
+			isValid:  false,
+		},
+		{
+			name:     "invalid empty token",
+			input:    "",
+			expected: nil,
+			isValid:  false,
+		},
+		{
+			name:     "valid token with empty parts",
+			input:    "..signature",
+			expected: []string{"", "", "signature"},
+			isValid:  true,
+		},
+		{
+			// We are just splitting the token into parts, so we don't care about the actual values.
+			// It is up to the caller to validate the parts.
+			name:     "valid token with all parts empty",
+			input:    "..",
+			expected: []string{"", "", ""},
+			isValid:  true,
+		},
+		{
+			name:     "invalid token with just delimiters and extra part",
+			input:    "...",
+			expected: nil,
+			isValid:  false,
+		},
+		{
+			name:     "invalid token with many delimiters",
+			input:    "header.claims.signature..................",
+			expected: nil,
+			isValid:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			parts, ok := splitToken(tt.input)
+			if ok != tt.isValid {
+				t.Errorf("expected %t, got %t", tt.isValid, ok)
+			}
+			if ok {
+				for i, part := range tt.expected {
+					if parts[i] != part {
+						t.Errorf("expected %s, got %s", part, parts[i])
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/parser.go b/parser.go
index 9dd36e5a5acd4c5606c061db0aa08583eed7baf6..0fc510a0aae43b268ba085d04d6ce73df75c9fc8 100644
--- a/parser.go
+++ b/parser.go
@@ -7,6 +7,8 @@ import (
 	"strings"
 )
 
+const tokenDelimiter = "."
+
 type Parser struct {
 	// If populated, only these methods will be considered valid.
 	//
@@ -122,9 +124,10 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
 // It's only ever useful in cases where you know the signature is valid (because it has
 // been checked previously in the stack) and you want to extract values from it.
 func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
-	parts = strings.Split(tokenString, ".")
-	if len(parts) != 3 {
-		return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
+	var ok bool
+	parts, ok = splitToken(tokenString)
+	if !ok {
+		return nil, nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
 	}
 
 	token = &Token{Raw: tokenString}
@@ -174,3 +177,30 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
 
 	return token, parts, nil
 }
+
+// splitToken splits a token string into three parts: header, claims, and signature. It will only
+// return true if the token contains exactly two delimiters and three parts. In all other cases, it
+// will return nil parts and false.
+func splitToken(token string) ([]string, bool) {
+	parts := make([]string, 3)
+	header, remain, ok := strings.Cut(token, tokenDelimiter)
+	if !ok {
+		return nil, false
+	}
+	parts[0] = header
+	claims, remain, ok := strings.Cut(remain, tokenDelimiter)
+	if !ok {
+		return nil, false
+	}
+	parts[1] = claims
+	// One more cut to ensure the signature is the last part of the token and there are no more
+	// delimiters. This avoids an issue where malicious input could contain additional delimiters
+	// causing unecessary overhead parsing tokens.
+	signature, _, unexpected := strings.Cut(remain, tokenDelimiter)
+	if unexpected {
+		return nil, false
+	}
+	parts[2] = signature
+
+	return parts, true
+}