Skip to content

Commit 74abae6

Browse files
authored
Added Invalid Token Error Message that will be returned for bad tokens (#25953)
Edited changelog Added dummy policy to CE file to make tests pass Added changelog
1 parent 6482672 commit 74abae6

File tree

5 files changed

+230
-3
lines changed

5 files changed

+230
-3
lines changed

changelog/25953.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
```release-note:change
2+
core: return an additional "invalid token" error message in 403 response when the provided request token is expired,
3+
exceeded the number of uses, or is a bogus value
4+
```

http/auth_token_test.go

+134
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,40 @@ package http
66
import (
77
"strings"
88
"testing"
9+
"time"
910

11+
"github.com/hashicorp/go-hclog"
1012
"github.com/hashicorp/vault/api"
13+
"github.com/hashicorp/vault/sdk/helper/logging"
14+
"github.com/hashicorp/vault/sdk/logical"
1115
"github.com/hashicorp/vault/vault"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
const (
20+
rootLeasePolicies = `
21+
path "sys/internal/ui/*" {
22+
capabilities = ["create", "read", "update", "delete", "list"]
23+
}
24+
25+
path "auth/token/*" {
26+
capabilities = ["create", "update", "read", "list"]
27+
}
28+
29+
path "kv/foo*" {
30+
capabilities = ["create", "read", "update", "delete", "list"]
31+
}
32+
`
33+
34+
dummy = `
35+
path "/ns1/sys/leases/*" {
36+
capabilities = ["sudo", "create", "read", "update", "delete", "list"]
37+
}
38+
39+
path "/ns1/auth/token/*" {
40+
capabilities = ["sudo", "create", "read", "update", "delete", "list"]
41+
}
42+
`
1243
)
1344

1445
func TestAuthTokenCreate(t *testing.T) {
@@ -207,3 +238,106 @@ func TestAuthTokenRenew(t *testing.T) {
207238
t.Error("expected lease to be renewable")
208239
}
209240
}
241+
242+
// TestToken_InvalidTokenError checks that an InvalidToken error is only returned
243+
// for tokens that have (1) exceeded the token TTL and (2) exceeded the number of uses
244+
func TestToken_InvalidTokenError(t *testing.T) {
245+
coreConfig := &vault.CoreConfig{
246+
DisableMlock: true,
247+
DisableCache: true,
248+
Logger: logging.NewVaultLogger(hclog.Trace),
249+
}
250+
251+
// Init new test cluster
252+
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
253+
HandlerFunc: Handler,
254+
})
255+
256+
cluster.Start()
257+
defer cluster.Cleanup()
258+
259+
cores := cluster.Cores
260+
vault.TestWaitActive(t, cores[0].Core)
261+
262+
client := cores[0].Client
263+
264+
// Add policy
265+
if err := client.Sys().PutPolicy("root-lease-policy", rootLeasePolicies); err != nil {
266+
t.Fatal(err)
267+
}
268+
// Add a dummy policy
269+
if err := client.Sys().PutPolicy("dummy", dummy); err != nil {
270+
t.Fatal(err)
271+
}
272+
273+
rootToken := client.Token()
274+
275+
// Enable kv secrets and mount initial secrets
276+
err := client.Sys().Mount("kv", &api.MountInput{Type: "kv"})
277+
require.NoError(t, err)
278+
279+
writeSecretsToMount(t, client, "kv/foo", map[string]interface{}{
280+
"user": "admin",
281+
"password": "password",
282+
})
283+
284+
// Create a token that has a TTL of 5s
285+
tokenCreateRequest := &api.TokenCreateRequest{
286+
Policies: []string{"root-lease-policy"},
287+
TTL: "5s",
288+
}
289+
secret, err := client.Auth().Token().CreateOrphan(tokenCreateRequest)
290+
token := secret.Auth.ClientToken
291+
client.SetToken(token)
292+
293+
// Verify that token works to read from kv mount
294+
_, err = client.Logical().Read("kv/foo")
295+
require.NoError(t, err)
296+
297+
time.Sleep(time.Second * 5)
298+
299+
// Verify that token is expired and shows an "invalid token" error
300+
_, err = client.Logical().Read("kv/foo")
301+
require.ErrorContains(t, err, logical.ErrInvalidToken.Error())
302+
require.ErrorContains(t, err, logical.ErrPermissionDenied.Error())
303+
304+
// Create a second approle token with a token use limit
305+
client.SetToken(rootToken)
306+
tokenCreateRequest = &api.TokenCreateRequest{
307+
Policies: []string{"root-lease-policy"},
308+
NumUses: 5,
309+
}
310+
311+
secret, err = client.Auth().Token().CreateOrphan(tokenCreateRequest)
312+
token = secret.Auth.ClientToken
313+
client.SetToken(token)
314+
315+
for i := 0; i < 5; i++ {
316+
_, err = client.Logical().Read("kv/foo")
317+
require.NoError(t, err)
318+
}
319+
// Verify that the number of uses is exceeded so the "invalid token" error is displayed
320+
_, err = client.Logical().Read("kv/foo")
321+
require.ErrorContains(t, err, logical.ErrInvalidToken.Error())
322+
require.ErrorContains(t, err, logical.ErrPermissionDenied.Error())
323+
324+
// Create a third approle token that will have incorrect policy access to the subsequent request
325+
client.SetToken(rootToken)
326+
tokenCreateRequest = &api.TokenCreateRequest{
327+
Policies: []string{"dummy"},
328+
}
329+
330+
secret, err = client.Auth().Token().CreateOrphan(tokenCreateRequest)
331+
token = secret.Auth.ClientToken
332+
client.SetToken(token)
333+
334+
// Incorrect policy access should only return an ErrPermissionDenied error
335+
_, err = client.Logical().Read("kv/foo")
336+
require.ErrorContains(t, err, logical.ErrPermissionDenied.Error())
337+
require.NotContains(t, err.Error(), logical.ErrInvalidToken)
338+
}
339+
340+
func writeSecretsToMount(t *testing.T, client *api.Client, mountPath string, data map[string]interface{}) {
341+
_, err := client.Logical().Write(mountPath, data)
342+
require.NoError(t, err)
343+
}

sdk/logical/error.go

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ var (
2323
// ErrPermissionDenied is returned if the client is not authorized
2424
ErrPermissionDenied = errors.New("permission denied")
2525

26+
// ErrInvalidToken is returned if the token is revoked, expired, or non-existent
27+
ErrInvalidToken = errors.New("invalid token")
28+
2629
// ErrInvalidCredentials is returned when the provided credentials are incorrect
2730
// This is used internally for user lockout purposes. This is not seen externally.
2831
// The status code returned does not change because of this error

vault/core_test.go

+84-1
Original file line numberDiff line numberDiff line change
@@ -1175,7 +1175,7 @@ func TestCore_HandleRequest_InvalidToken(t *testing.T) {
11751175
if err == nil || !errwrap.Contains(err, logical.ErrPermissionDenied.Error()) {
11761176
t.Fatalf("err: %v", err)
11771177
}
1178-
if resp.Data["error"] != "permission denied" {
1178+
if !strings.Contains(resp.Data["error"].(string), "permission denied") {
11791179
t.Fatalf("bad: %#v", resp)
11801180
}
11811181
}
@@ -1251,6 +1251,26 @@ func TestCore_HandleRequest_RootPath_WithSudo(t *testing.T) {
12511251
}
12521252
}
12531253

1254+
// TestCore_HandleRequest_TokenErrInvalidToken checks that a request made
1255+
// with a non-existent token will return the "permission denied" and "invalid token" error
1256+
func TestCore_HandleRequest_TokenErrInvalidToken(t *testing.T) {
1257+
c, _, _ := TestCoreUnsealed(t)
1258+
1259+
req := &logical.Request{
1260+
Operation: logical.UpdateOperation,
1261+
Path: "secret/test",
1262+
Data: map[string]interface{}{
1263+
"foo": "bar",
1264+
"lease": "1h",
1265+
},
1266+
ClientToken: "bogus",
1267+
}
1268+
resp, err := c.HandleRequest(namespace.RootContext(nil), req)
1269+
if err == nil || !errwrap.Contains(err, logical.ErrInvalidToken.Error()) || !errwrap.Contains(err, logical.ErrPermissionDenied.Error()) {
1270+
t.Fatalf("err: %v, resp: %v", err, resp)
1271+
}
1272+
}
1273+
12541274
// Check that standard permissions work
12551275
func TestCore_HandleRequest_PermissionDenied(t *testing.T) {
12561276
c, _, root := TestCoreUnsealed(t)
@@ -1271,6 +1291,69 @@ func TestCore_HandleRequest_PermissionDenied(t *testing.T) {
12711291
}
12721292
}
12731293

1294+
// TestCore_RevokedToken_InvalidTokenError checks that a request
1295+
// returns an "invalid token" and a "permission denied" error when a token
1296+
// that has been revoked is used in a request
1297+
func TestCore_RevokedToken_InvalidTokenError(t *testing.T) {
1298+
c, _, root := TestCoreUnsealed(t)
1299+
1300+
// Set the 'test' policy object to permit access to sys/policy
1301+
req := &logical.Request{
1302+
Operation: logical.UpdateOperation,
1303+
Path: "sys/policy/test", // root protected!
1304+
Data: map[string]interface{}{
1305+
"rules": `path "sys/policy" { policy = "sudo" }`,
1306+
},
1307+
ClientToken: root,
1308+
}
1309+
resp, err := c.HandleRequest(namespace.RootContext(nil), req)
1310+
if err != nil {
1311+
t.Fatalf("err: %v", err)
1312+
}
1313+
if resp != nil && (resp.IsError() || len(resp.Data) > 0) {
1314+
t.Fatalf("bad: %#v", resp)
1315+
}
1316+
1317+
// Child token (non-root) but with 'test' policy should have access
1318+
testMakeServiceTokenViaCore(t, c, root, "child", "", []string{"test"})
1319+
req = &logical.Request{
1320+
Operation: logical.ReadOperation,
1321+
Path: "sys/policy", // root protected!
1322+
ClientToken: "child",
1323+
}
1324+
resp, err = c.HandleRequest(namespace.RootContext(nil), req)
1325+
if err != nil {
1326+
t.Fatalf("err: %v", err)
1327+
}
1328+
if resp == nil {
1329+
t.Fatalf("bad: %#v", resp)
1330+
}
1331+
1332+
// Revoke the token
1333+
req = &logical.Request{
1334+
ClientToken: root,
1335+
Operation: logical.UpdateOperation,
1336+
Path: "auth/token/revoke",
1337+
Data: map[string]interface{}{
1338+
"token": "child",
1339+
},
1340+
}
1341+
resp, err = c.HandleRequest(namespace.RootContext(nil), req)
1342+
if err != nil {
1343+
t.Fatalf("err: %v", err)
1344+
}
1345+
1346+
req = &logical.Request{
1347+
Operation: logical.ReadOperation,
1348+
Path: "sys/policy", // root protected!
1349+
ClientToken: "child",
1350+
}
1351+
_, err = c.HandleRequest(namespace.RootContext(nil), req)
1352+
if err == nil || !errwrap.Contains(err, logical.ErrPermissionDenied.Error()) || !errwrap.Contains(err, logical.ErrInvalidToken.Error()) {
1353+
t.Fatalf("err: %v, resp: %v", err, resp)
1354+
}
1355+
}
1356+
12741357
// Check that standard permissions work
12751358
func TestCore_HandleRequest_PermissionAllowed(t *testing.T) {
12761359
c, _, root := TestCoreUnsealed(t)

vault/request_handling.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req
248248

249249
// Ensure the token is valid
250250
if te == nil {
251-
return nil, nil, nil, nil, logical.ErrPermissionDenied
251+
return nil, nil, nil, nil, multierror.Append(logical.ErrPermissionDenied, logical.ErrInvalidToken)
252252
}
253253

254254
// CIDR checks bind all tokens except non-expiring root tokens
@@ -627,6 +627,9 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request
627627

628628
err = c.PopulateTokenEntry(ctx, req)
629629
if err != nil {
630+
if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) {
631+
return nil, multierror.Append(err, logical.ErrInvalidToken)
632+
}
630633
return nil, err
631634
}
632635

@@ -1025,7 +1028,7 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp
10251028
}
10261029
if te == nil {
10271030
// Token has been revoked by this point
1028-
retErr = multierror.Append(retErr, logical.ErrPermissionDenied)
1031+
retErr = multierror.Append(retErr, logical.ErrPermissionDenied, logical.ErrInvalidToken)
10291032
return nil, nil, retErr
10301033
}
10311034
if te.NumUses == tokenRevocationPending {

0 commit comments

Comments
 (0)