Skip to content

Commit

Permalink
Merge pull request #85 from svetlyi/skip_verification_option
Browse files Browse the repository at this point in the history
Added the skip_verification option.
  • Loading branch information
ggicci authored Aug 10, 2024
2 parents 5f1c2a8 + 1f4273d commit e1e2a8c
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 20 deletions.
5 changes: 1 addition & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ GOTEST=$(GO) test
GOCOVER=$(GO) tool cover
XCADDY=xcaddy

example:
$(XCADDY) run -config=example/caddy.json

debug:
XCADDY_DEBUG=1 $(XCADDY) build --with github.com/ggicci/caddy-jwt=$(shell pwd)

Expand All @@ -24,4 +21,4 @@ test/cover:
test/report:
$(GOCOVER) -html=main.cover.out

.PHONY: example debug test test/cover test/report
.PHONY: debug test test/cover test/report
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,12 @@ Module **caddy-jwt** behaves like a **"JWT Validator"**. The authentication flow
│ 3. cookies │
└────────┬─────────┘
┌───────▼────────┐
│ is valid? │
│using `sign_key`├────NO───────┐
└───────┬────────┘ │
┌───────▼───────────┐
│ is valid? │
│ using `sign_key` │
│ or validation is │
│ disabled ├─NO───────┐
└───────┬───────────┘ │
│YES │
┌───────────▼───────────┐ │
│Populate {http.user.id}│ │
Expand Down
2 changes: 2 additions & 0 deletions caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
if !h.AllArgs(&ja.JWKURL) {
return nil, h.Errf("invalid jwk_url: %q", ja.JWKURL)
}
case "skip_verification":
ja.SkipVerification = true
case "from_query":
ja.FromQuery = h.RemainingArgs()

Expand Down
35 changes: 35 additions & 0 deletions caddyfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,38 @@ func TestParseMetaClaim(t *testing.T) {
}
}
}

func TestParsingCaddyfileWithSkipVerification(t *testing.T) {
helper := httpcaddyfile.Helper{
Dispenser: caddyfile.NewTestDispenser(`
jwtauth {
skip_verification
from_query access_token token _tok
from_header X-Api-Key
from_cookies user_session SESSID
issuer_whitelist https://api.example.com
audience_whitelist https://api.example.io https://learn.example.com
user_claims uid user_id login username
meta_claims "IsAdmin -> is_admin" "gender"
}
`),
}
expectedJA := &JWTAuth{
SkipVerification: true,
FromQuery: []string{"access_token", "token", "_tok"},
FromHeader: []string{"X-Api-Key"},
FromCookies: []string{"user_session", "SESSID"},
IssuerWhitelist: []string{"https://api.example.com"},
AudienceWhitelist: []string{"https://api.example.io", "https://learn.example.com"},
UserClaims: []string{"uid", "user_id", "login", "username"},
MetaClaims: map[string]string{"IsAdmin": "is_admin", "gender": "gender"},
}

h, err := parseCaddyfile(helper)
assert.Nil(t, err)
auth, ok := h.(caddyauth.Authentication)
assert.True(t, ok)
jsonConfig, ok := auth.ProvidersRaw["jwt"]
assert.True(t, ok)
assert.Equal(t, caddyconfig.JSON(expectedJA, nil), jsonConfig)
}
54 changes: 42 additions & 12 deletions jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type JWTAuth struct {
// If you'd like to use JWK, set this field and leave SignKey unset.
JWKURL string `json:"jwk_url"`

// SignAlgorithm is the the signing algorithm used. Available values are defined in
// SignAlgorithm is the signing algorithm used. Available values are defined in
// https://www.rfc-editor.org/rfc/rfc7518#section-3.1
// This is an optional field, which is used for determining the signing algorithm.
// We will try to determine the algorithm automatically from the following sources:
Expand All @@ -61,6 +61,19 @@ type JWTAuth struct {
// 3. The value set here.
SignAlgorithm string `json:"sign_alg"`

// SkipVerification disables the verification of the JWT token signature.
//
// Use this option with caution, as it bypasses JWT signature verification.
// This can be useful if the token's signature has already been verified before
// reaching this proxy server or will be verified later, preventing redundant
// verifications and handling of the same token multiple times.
//
// This is particularly relevant if you want to use this plugin for routing
// based on the JWT payload, while avoiding unnecessary signature checks.
//
// This flag also disables usage and check of both JWKURL and SignAlgorithm options.
SkipVerification bool `json:"skip_verification"`

// FromQuery defines a list of names to get tokens from the query parameters
// of an HTTP request.
//
Expand Down Expand Up @@ -182,6 +195,26 @@ func (ja *JWTAuth) refreshJWKCache() {

// Validate implements caddy.Validator interface.
func (ja *JWTAuth) Validate() error {
if !ja.SkipVerification {
if err := ja.validateSignatureKeys(); err != nil {
return err
}
}

if len(ja.UserClaims) == 0 {
ja.UserClaims = []string{
"sub",
}
}
for claim, placeholder := range ja.MetaClaims {
if claim == "" || placeholder == "" {
return fmt.Errorf("invalid meta claim: %s -> %s", claim, placeholder)
}
}
return nil
}

func (ja *JWTAuth) validateSignatureKeys() error {
if ja.usingJWK() {
ja.setupJWKLoader()
} else {
Expand All @@ -205,16 +238,6 @@ func (ja *JWTAuth) Validate() error {
}
}

if len(ja.UserClaims) == 0 {
ja.UserClaims = []string{
"sub",
}
}
for claim, placeholder := range ja.MetaClaims {
if claim == "" || placeholder == "" {
return fmt.Errorf("invalid meta claim: %s -> %s", claim, placeholder)
}
}
return nil
}

Expand Down Expand Up @@ -277,7 +300,14 @@ func (ja *JWTAuth) Authenticate(rw http.ResponseWriter, r *http.Request) (User,
continue
}

gotToken, err = jwt.ParseString(tokenString, jwt.WithKeyProvider(ja.keyProvider()))
jwtOptions := []jwt.ParseOption{
jwt.WithVerify(!ja.SkipVerification),
}
if !ja.SkipVerification {
jwtOptions = append(jwtOptions, jwt.WithKeyProvider(ja.keyProvider()))
}
gotToken, err = jwt.ParseString(tokenString, jwtOptions...)

checked[tokenString] = struct{}{}

logger := ja.logger.With(zap.String("token_string", desensitizedTokenString(tokenString)))
Expand Down
92 changes: 92 additions & 0 deletions jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,17 @@ func TestValidate_usingJWK(t *testing.T) {
assert.Nil(t, err)
}

// TestValidate_SkipVerification checks that validation does not fail when
// SkipVerification is enabled and no keys are provided. This ensures that
// enabling SkipVerification bypasses signature and keys validation without errors.
func TestValidate_SkipVerification(t *testing.T) {
// skipping verification
ja := &JWTAuth{
SkipVerification: true,
}
assert.NoError(t, ja.Validate())
}

func TestValidate_InvalidMetaClaims(t *testing.T) {
ja := &JWTAuth{
SignKey: TestSignKey,
Expand Down Expand Up @@ -247,6 +258,87 @@ func TestAuthenticate_FromCustomHeader(t *testing.T) {
assert.Equal(t, User{ID: "ggicci"}, gotUser)
}

func TestAuthenticate_FromQueryWithSkipVerification(t *testing.T) {
var (
claims = MapClaims{"sub": "ggicci"}
ja = &JWTAuth{
FromQuery: []string{"access_token", "token"},
SkipVerification: true,
logger: testLogger,
}
tokenString = issueTokenString(claims)

err error
rw *httptest.ResponseRecorder
r *http.Request
params url.Values
gotUser User
authenticated bool
)
assert.Nil(t, ja.Validate())

// trying "access_token" without signature key
rw = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "/", nil)
params = make(url.Values)
params.Add("access_token", tokenString)
r.URL.RawQuery = params.Encode()
gotUser, authenticated, err = ja.Authenticate(rw, r)
assert.Nil(t, err)
assert.True(t, authenticated)
assert.Equal(t, User{ID: "ggicci"}, gotUser)

// invalid "token" without signature key
rw = httptest.NewRecorder()
r, _ = http.NewRequest("GET", "/", nil)
params = make(url.Values)
params.Add("access_token", tokenString+"INVALID")
params.Add("token", tokenString)
r.URL.RawQuery = params.Encode()
gotUser, authenticated, err = ja.Authenticate(rw, r)
assert.Nil(t, err)
assert.True(t, authenticated)
assert.Equal(t, User{ID: "ggicci"}, gotUser)
}

func TestAuthenticate_PopulateUserMetadataWithSkipVerification(t *testing.T) {
ja := &JWTAuth{
SkipVerification: true,
MetaClaims: map[string]string{
"jti": "jti",
"IsAdmin": "is_admin",
"settings.role": "role",
"settings.payout.paypal.enabled": "is_paypal_enabled",
},
logger: testLogger,
}
assert.Nil(t, ja.Validate())

claimsWithMetadata := MapClaims{
"jti": "a976475a-186a-4c1f-b182-95b3f886e2b4",
"sub": "ggicci",
"IsAdmin": true,
"settings": map[string]interface{}{
"role": "admin",
"payout": map[string]interface{}{
"paypal": map[string]interface{}{
"enabled": true,
},
},
},
}
rw := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Add("Authorization", issueTokenString(claimsWithMetadata))
gotUser, authenticated, err := ja.Authenticate(rw, r)
assert.Nil(t, err)
assert.True(t, authenticated)
assert.Equal(t, "a976475a-186a-4c1f-b182-95b3f886e2b4", gotUser.Metadata["jti"])
assert.Equal(t, "true", gotUser.Metadata["is_admin"])
assert.Equal(t, "admin", gotUser.Metadata["role"])
assert.Equal(t, "true", gotUser.Metadata["is_paypal_enabled"])
}

func TestAuthenticate_FromQuery(t *testing.T) {
var (
claims = MapClaims{"sub": "ggicci"}
Expand Down

0 comments on commit e1e2a8c

Please sign in to comment.