Skip to content

Commit

Permalink
[feat] allow revocation of all users or activations (#114)
Browse files Browse the repository at this point in the history
* Added the ability to revoke users/activations by timestamp. If the value for the public key is "*", any JWT issued before the timestamp specified will be considered revoked.
  • Loading branch information
aricart authored Oct 30, 2020
1 parent c31b0a8 commit 7d55e83
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 16 deletions.
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,20 @@ test:
go test -v --race
staticcheck ./...

cd v2/
gofmt -s -w *.go
goimports -w *.go
go vet ./...
go test -v
go test -v --race
staticcheck ./...

fmt:
gofmt -w -s *.go
go mod tidy
cd v2/
gofmt -w -s *.go
go mod tidy

cover:
go test -v -covermode=count -coverprofile=coverage.out
Expand Down
3 changes: 2 additions & 1 deletion account_claims.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2019 The NATS Authors
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -213,6 +213,7 @@ func (a *AccountClaims) ClearRevocation(pubKey string) {
// IsRevokedAt checks if the public key is in the revoked list with a timestamp later than the one passed in.
// Generally this method is called with the subject and issue time of the jwt to be tested.
// DO NOT pass time.Now(), it will not produce a stable/expected response.
// The value is expected to be a public key or "*" (means all public keys)
func (a *AccountClaims) IsRevokedAt(pubKey string, timestamp time.Time) bool {
return a.Revocations.IsRevoked(pubKey, timestamp)
}
Expand Down
20 changes: 20 additions & 0 deletions account_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,23 @@ func TestUserRevocation(t *testing.T) {
t.Errorf("revocation be true we revoked in the future")
}
}

func TestUserRevocationAll(t *testing.T) {
akp := createAccountNKey(t)
apk := publicKey(akp, t)
account := NewAccountClaims(apk)

now := time.Now().Add(time.Second * -10)
account.RevokeAt(All, now)

before := now.Add(time.Second * -1)
if !account.IsRevokedAt("foo", before) {
t.Error("foo should have been revoked (before)")
}
if !account.IsRevokedAt("foo", now) {
t.Error("foo should have been revoked (now)")
}
if account.IsRevokedAt("foo", now.Add(time.Second)) {
t.Error("foo should have not been revoked")
}
}
36 changes: 35 additions & 1 deletion activation_claims_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 The NATS Authors
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -424,3 +424,37 @@ func TestCleanSubject(t *testing.T) {
}
}
}

func TestActivationClaimRevocation(t *testing.T) {
akp := createAccountNKey(t)
apk := publicKey(akp, t)
account := NewAccountClaims(apk)
e := &Export{Subject: "q.>", Type: Service, TokenReq: true}
account.Exports.Add(e)

a := publicKey(createAccountNKey(t), t)
aminAgo := time.Now().Add(-time.Minute)

if account.Exports[0].Revocations.IsRevoked(a, aminAgo) {
t.Fatal("should not be revoked")
}
e.RevokeAt(a, aminAgo)
if !account.Exports[0].Revocations.IsRevoked(a, aminAgo) {
t.Fatal("should be revoked")
}

a2 := publicKey(createAccountNKey(t), t)
if account.Exports[0].Revocations.IsRevoked(a2, aminAgo) {
t.Fatal("should not be revoked")
}
e.RevokeAt("*", aminAgo)
if !account.Exports[0].Revocations.IsRevoked(a2, time.Now().Add(-time.Hour)) {
t.Fatal("should be revoked")
}

vr := ValidationResults{}
account.Validate(&vr)
if !vr.IsEmpty() {
t.Fatal("account validation shouldn't have failed")
}
}
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
module github.com/nats-io/jwt

require (
github.com/nats-io/nkeys v0.2.0
)
require github.com/nats-io/nkeys v0.2.0

go 1.14
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/jwt/v2 v2.0.0-20200827232814-292806fa48ba/go.mod h1:vs+ZEjP+XKy8szkBmQwCB7RjYdIlMaPsFPs4VdS4bTQ=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.2.0 h1:WXKF7diOaPU9cJdLD7nuzwasQy9vT1tBqzXZZf3AMJM=
github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
Expand Down
12 changes: 12 additions & 0 deletions revocation_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"time"
)

const All = "*"

// RevocationList is used to store a mapping of public keys to unix timestamps
type RevocationList map[string]int64

Expand All @@ -42,6 +44,16 @@ func (r RevocationList) ClearRevocation(pubKey string) {
// the one passed in. Generally this method is called with an issue time but other time's can
// be used for testing.
func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool {
if r.allRevoked(timestamp) {
return true
}
ts, ok := r[pubKey]
return ok && ts >= timestamp.Unix()
}

// allRevoked returns true if All is set and the timestamp is later or same as the
// one passed. This is called by IsRevoked.
func (r RevocationList) allRevoked(timestamp time.Time) bool {
ts, ok := r[All]
return ok && ts >= timestamp.Unix()
}
31 changes: 31 additions & 0 deletions user_claims_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,34 @@ func TestSourceNetworkValidation(t *testing.T) {
t.Error("limits should be invalid")
}
}

func TestUserClaimRevocation(t *testing.T) {
akp := createAccountNKey(t)
apk := publicKey(akp, t)
account := NewAccountClaims(apk)

u := publicKey(createUserNKey(t), t)
aminAgo := time.Now().Add(-time.Minute)
if account.Revocations.IsRevoked(u, aminAgo) {
t.Fatal("shouldn't be revoked")
}
account.RevokeAt(u, aminAgo)
if !account.Revocations.IsRevoked(u, aminAgo) {
t.Fatal("should be revoked")
}

u2 := publicKey(createUserNKey(t), t)
if account.Revocations.IsRevoked(u2, aminAgo) {
t.Fatal("should not be revoked")
}
account.RevokeAt("*", aminAgo)
if !account.Revocations.IsRevoked(u2, time.Now().Add(-time.Hour)) {
t.Fatal("should be revoked")
}

vr := ValidationResults{}
account.Validate(&vr)
if !vr.IsEmpty() {
t.Fatal("account validation shouldn't have failed")
}
}
4 changes: 2 additions & 2 deletions v2/account_claims.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018-2019 The NATS Authors
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -238,11 +238,11 @@ func (a *AccountClaims) Revoke(pubKey string) {
// RevokeAt enters a revocation by public key and timestamp into this account
// This will revoke all jwt issued for pubKey, prior to timestamp
// If there is already a revocation for this public key that is newer, it is kept.
// The value is expected to be a public key or "*" (means all public keys)
func (a *AccountClaims) RevokeAt(pubKey string, timestamp time.Time) {
if a.Revocations == nil {
a.Revocations = RevocationList{}
}

a.Revocations.Revoke(pubKey, timestamp)
}

Expand Down
30 changes: 29 additions & 1 deletion v2/account_claims_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 The NATS Authors
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -531,3 +531,31 @@ func TestAccountDefaultPermissions(t *testing.T) {

AssertEquals(account.String(), account2.String(), t)
}

func TestUserRevocationAll(t *testing.T) {
akp := createAccountNKey(t)
ukp := createUserNKey(t)
upk := publicKey(ukp, t)
user := NewUserClaims(upk)
token, err := user.Encode(akp)
if err != nil {
t.Fatal(err)
}

ud, err := DecodeUserClaims(token)
if err != nil {
t.Fatal(err)
}

apk := publicKey(akp, t)
account := NewAccountClaims(apk)
account.RevokeAt(All, time.Now().Add(time.Second))
if !account.IsClaimRevoked(ud) {
t.Fatal("user should have been revoked")
}

account.RevokeAt(All, time.Now().Add(time.Second*-10))
if !account.IsClaimRevoked(ud) {
t.Fatal("user should have not been revoked")
}
}
36 changes: 35 additions & 1 deletion v2/activation_claims_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 The NATS Authors
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -354,3 +354,37 @@ func TestCleanSubject(t *testing.T) {
}
}
}

func TestActivationClaimRevocation(t *testing.T) {
akp := createAccountNKey(t)
apk := publicKey(akp, t)
account := NewAccountClaims(apk)
e := &Export{Subject: "q.>", Type: Service, TokenReq: true}
account.Exports.Add(e)

a := publicKey(createAccountNKey(t), t)
aminAgo := time.Now().Add(-time.Minute)

if account.Exports[0].Revocations.IsRevoked(a, aminAgo) {
t.Fatal("should not be revoked")
}
e.RevokeAt(a, aminAgo)
if !account.Exports[0].Revocations.IsRevoked(a, aminAgo) {
t.Fatal("should be revoked")
}

a2 := publicKey(createAccountNKey(t), t)
if account.Exports[0].Revocations.IsRevoked(a2, aminAgo) {
t.Fatal("should not be revoked")
}
e.RevokeAt("*", aminAgo)
if !account.Exports[0].Revocations.IsRevoked(a2, time.Now().Add(-time.Hour)) {
t.Fatal("should be revoked")
}

vr := ValidationResults{}
account.Validate(&vr)
if !vr.IsEmpty() {
t.Fatal("account validation shouldn't have failed")
}
}
4 changes: 2 additions & 2 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
module github.com/nats-io/jwt/v2

require (
github.com/nats-io/jwt v0.3.2
github.com/nats-io/jwt v1.1.0
github.com/nats-io/nkeys v0.2.0
)

replace github.com/nats-io/jwt v0.3.2 => ../
replace github.com/nats-io/jwt v1.1.0 => ../

go 1.14
12 changes: 12 additions & 0 deletions v2/revocation_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"time"
)

const All = "*"

// RevocationList is used to store a mapping of public keys to unix timestamps
type RevocationList map[string]int64

Expand All @@ -42,6 +44,16 @@ func (r RevocationList) ClearRevocation(pubKey string) {
// the one passed in. Generally this method is called with an issue time but other time's can
// be used for testing.
func (r RevocationList) IsRevoked(pubKey string, timestamp time.Time) bool {
if r.allRevoked(timestamp) {
return true
}
ts, ok := r[pubKey]
return ok && ts >= timestamp.Unix()
}

// allRevoked returns true if All is set and the timestamp is later or same as the
// one passed. This is called by IsRevoked.
func (r RevocationList) allRevoked(timestamp time.Time) bool {
ts, ok := r[All]
return ok && ts >= timestamp.Unix()
}
33 changes: 32 additions & 1 deletion v2/user_claims_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2018 The NATS Authors
* Copyright 2018-2020 The NATS Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand Down Expand Up @@ -367,3 +367,34 @@ func TestUserAllowedConnectionTypes(t *testing.T) {
AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeStandard), t)
AssertTrue(uc2.AllowedConnectionTypes.Contains(ConnectionTypeWebsocket), t)
}

func TestUserClaimRevocation(t *testing.T) {
akp := createAccountNKey(t)
apk := publicKey(akp, t)
account := NewAccountClaims(apk)

u := publicKey(createUserNKey(t), t)
aminAgo := time.Now().Add(-time.Minute)
if account.Revocations.IsRevoked(u, aminAgo) {
t.Fatal("shouldn't be revoked")
}
account.RevokeAt(u, aminAgo)
if !account.Revocations.IsRevoked(u, aminAgo) {
t.Fatal("should be revoked")
}

u2 := publicKey(createUserNKey(t), t)
if account.Revocations.IsRevoked(u2, aminAgo) {
t.Fatal("should not be revoked")
}
account.RevokeAt("*", aminAgo)
if !account.Revocations.IsRevoked(u2, time.Now().Add(-time.Hour)) {
t.Fatal("should be revoked")
}

vr := ValidationResults{}
account.Validate(&vr)
if !vr.IsEmpty() {
t.Fatal("account validation shouldn't have failed")
}
}

0 comments on commit 7d55e83

Please sign in to comment.