...

Source file src/go.etcd.io/etcd/server/v3/auth/jwt_test.go

Documentation: go.etcd.io/etcd/server/v3/auth

     1  // Copyright 2017 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package auth
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"testing"
    21  	"time"
    22  
    23  	"github.com/golang-jwt/jwt/v4"
    24  	"github.com/stretchr/testify/require"
    25  	"go.uber.org/zap"
    26  )
    27  
    28  const (
    29  	jwtRSAPubKey  = "../../tests/fixtures/server.crt"
    30  	jwtRSAPrivKey = "../../tests/fixtures/server.key.insecure"
    31  
    32  	jwtECPubKey  = "../../tests/fixtures/server-ecdsa.crt"
    33  	jwtECPrivKey = "../../tests/fixtures/server-ecdsa.key.insecure"
    34  )
    35  
    36  func TestJWTInfo(t *testing.T) {
    37  	optsMap := map[string]map[string]string{
    38  		"RSA-priv": {
    39  			"priv-key":    jwtRSAPrivKey,
    40  			"sign-method": "RS256",
    41  			"ttl":         "1h",
    42  		},
    43  		"RSA": {
    44  			"pub-key":     jwtRSAPubKey,
    45  			"priv-key":    jwtRSAPrivKey,
    46  			"sign-method": "RS256",
    47  		},
    48  		"RSAPSS-priv": {
    49  			"priv-key":    jwtRSAPrivKey,
    50  			"sign-method": "PS256",
    51  		},
    52  		"RSAPSS": {
    53  			"pub-key":     jwtRSAPubKey,
    54  			"priv-key":    jwtRSAPrivKey,
    55  			"sign-method": "PS256",
    56  		},
    57  		"ECDSA-priv": {
    58  			"priv-key":    jwtECPrivKey,
    59  			"sign-method": "ES256",
    60  		},
    61  		"ECDSA": {
    62  			"pub-key":     jwtECPubKey,
    63  			"priv-key":    jwtECPrivKey,
    64  			"sign-method": "ES256",
    65  		},
    66  		"HMAC": {
    67  			"priv-key":    jwtECPrivKey, // any file, raw bytes used as shared secret
    68  			"sign-method": "HS256",
    69  		},
    70  	}
    71  
    72  	for k, opts := range optsMap {
    73  		t.Run(k, func(tt *testing.T) {
    74  			testJWTInfo(tt, opts)
    75  		})
    76  	}
    77  }
    78  
    79  func testJWTInfo(t *testing.T, opts map[string]string) {
    80  	lg := zap.NewNop()
    81  	jwt, err := newTokenProviderJWT(lg, opts)
    82  	if err != nil {
    83  		t.Fatal(err)
    84  	}
    85  
    86  	ctx := context.TODO()
    87  
    88  	token, aerr := jwt.assign(ctx, "abc", 123)
    89  	if aerr != nil {
    90  		t.Fatalf("%#v", aerr)
    91  	}
    92  	ai, ok := jwt.info(ctx, token, 123)
    93  	if !ok {
    94  		t.Fatalf("failed to authenticate with token %s", token)
    95  	}
    96  	if ai.Revision != 123 {
    97  		t.Fatalf("expected revision 123, got %d", ai.Revision)
    98  	}
    99  	ai, ok = jwt.info(ctx, "aaa", 120)
   100  	if ok || ai != nil {
   101  		t.Fatalf("expected aaa to fail to authenticate, got %+v", ai)
   102  	}
   103  
   104  	// test verify-only provider
   105  	if opts["pub-key"] != "" && opts["priv-key"] != "" {
   106  		t.Run("verify-only", func(t *testing.T) {
   107  			newOpts := make(map[string]string, len(opts))
   108  			for k, v := range opts {
   109  				newOpts[k] = v
   110  			}
   111  			delete(newOpts, "priv-key")
   112  			verify, err := newTokenProviderJWT(lg, newOpts)
   113  			if err != nil {
   114  				t.Fatal(err)
   115  			}
   116  
   117  			ai, ok := verify.info(ctx, token, 123)
   118  			if !ok {
   119  				t.Fatalf("failed to authenticate with token %s", token)
   120  			}
   121  			if ai.Revision != 123 {
   122  				t.Fatalf("expected revision 123, got %d", ai.Revision)
   123  			}
   124  			ai, ok = verify.info(ctx, "aaa", 120)
   125  			if ok || ai != nil {
   126  				t.Fatalf("expected aaa to fail to authenticate, got %+v", ai)
   127  			}
   128  
   129  			_, aerr := verify.assign(ctx, "abc", 123)
   130  			if aerr != ErrVerifyOnly {
   131  				t.Fatalf("unexpected error when attempting to sign with public key: %v", aerr)
   132  			}
   133  
   134  		})
   135  	}
   136  }
   137  
   138  func TestJWTBad(t *testing.T) {
   139  
   140  	var badCases = map[string]map[string]string{
   141  		"no options": {},
   142  		"invalid method": {
   143  			"sign-method": "invalid",
   144  		},
   145  		"rsa no key": {
   146  			"sign-method": "RS256",
   147  		},
   148  		"invalid ttl": {
   149  			"sign-method": "RS256",
   150  			"ttl":         "forever",
   151  		},
   152  		"rsa invalid public key": {
   153  			"sign-method": "RS256",
   154  			"pub-key":     jwtRSAPrivKey,
   155  			"priv-key":    jwtRSAPrivKey,
   156  		},
   157  		"rsa invalid private key": {
   158  			"sign-method": "RS256",
   159  			"pub-key":     jwtRSAPubKey,
   160  			"priv-key":    jwtRSAPubKey,
   161  		},
   162  		"hmac no key": {
   163  			"sign-method": "HS256",
   164  		},
   165  		"hmac pub key": {
   166  			"sign-method": "HS256",
   167  			"pub-key":     jwtRSAPubKey,
   168  		},
   169  		"missing public key file": {
   170  			"sign-method": "HS256",
   171  			"pub-key":     "missing-file",
   172  		},
   173  		"missing private key file": {
   174  			"sign-method": "HS256",
   175  			"priv-key":    "missing-file",
   176  		},
   177  		"ecdsa no key": {
   178  			"sign-method": "ES256",
   179  		},
   180  		"ecdsa invalid public key": {
   181  			"sign-method": "ES256",
   182  			"pub-key":     jwtECPrivKey,
   183  			"priv-key":    jwtECPrivKey,
   184  		},
   185  		"ecdsa invalid private key": {
   186  			"sign-method": "ES256",
   187  			"pub-key":     jwtECPubKey,
   188  			"priv-key":    jwtECPubKey,
   189  		},
   190  	}
   191  
   192  	lg := zap.NewNop()
   193  
   194  	for k, v := range badCases {
   195  		t.Run(k, func(t *testing.T) {
   196  			_, err := newTokenProviderJWT(lg, v)
   197  			if err == nil {
   198  				t.Errorf("expected error for options %v", v)
   199  			}
   200  		})
   201  	}
   202  }
   203  
   204  // testJWTOpts is useful for passing to NewTokenProvider which requires a string.
   205  func testJWTOpts() string {
   206  	return fmt.Sprintf("%s,pub-key=%s,priv-key=%s,sign-method=RS256", tokenTypeJWT, jwtRSAPubKey, jwtRSAPrivKey)
   207  }
   208  
   209  func TestJWTTokenWithMissingFields(t *testing.T) {
   210  	testCases := []struct {
   211  		name        string
   212  		username    string // An empty string means not present
   213  		revision    uint64 // 0 means not present
   214  		expectValid bool
   215  	}{
   216  		{
   217  			name:        "valid token",
   218  			username:    "hello",
   219  			revision:    100,
   220  			expectValid: true,
   221  		},
   222  		{
   223  			name:        "no username",
   224  			username:    "",
   225  			revision:    100,
   226  			expectValid: false,
   227  		},
   228  		{
   229  			name:        "no revision",
   230  			username:    "hello",
   231  			revision:    0,
   232  			expectValid: false,
   233  		},
   234  	}
   235  
   236  	for _, tc := range testCases {
   237  		tc := tc
   238  		optsMap := map[string]string{
   239  			"priv-key":    jwtRSAPrivKey,
   240  			"sign-method": "RS256",
   241  			"ttl":         "1h",
   242  		}
   243  
   244  		t.Run(tc.name, func(t *testing.T) {
   245  			// prepare claims
   246  			claims := jwt.MapClaims{
   247  				"exp": time.Now().Add(time.Hour).Unix(),
   248  			}
   249  			if tc.username != "" {
   250  				claims["username"] = tc.username
   251  			}
   252  			if tc.revision != 0 {
   253  				claims["revision"] = tc.revision
   254  			}
   255  
   256  			// generate a JWT token with the given claims
   257  			var opts jwtOptions
   258  			err := opts.ParseWithDefaults(optsMap)
   259  			require.NoError(t, err)
   260  			key, err := opts.Key()
   261  			require.NoError(t, err)
   262  
   263  			tk := jwt.NewWithClaims(opts.SignMethod, claims)
   264  			token, err := tk.SignedString(key)
   265  			require.NoError(t, err)
   266  
   267  			// verify the token
   268  			jwtProvider, err := newTokenProviderJWT(zap.NewNop(), optsMap)
   269  			require.NoError(t, err)
   270  			ai, ok := jwtProvider.info(context.TODO(), token, 123)
   271  
   272  			require.Equal(t, tc.expectValid, ok)
   273  			if ok {
   274  				require.Equal(t, tc.username, ai.Username)
   275  				require.Equal(t, tc.revision, ai.Revision)
   276  			}
   277  		})
   278  	}
   279  }
   280  

View as plain text