Skip to content

Commit

Permalink
Merge pull request #26 from napptive/feature/PG-1070_multi_secret_int…
Browse files Browse the repository at this point in the history
…erceptor

PG-1070 Add multi secret zone aware interceptor
  • Loading branch information
dhiguero authored Mar 6, 2023
2 parents e894505 + ca17fc4 commit 50724d6
Show file tree
Hide file tree
Showing 14 changed files with 709 additions and 1,067 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v3
with:
go-version: ^1.16
go-version: 1.19
id: go

- name: Check out code into the Go module directory
Expand All @@ -35,7 +35,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v3
with:
go-version: ^1.16
go-version: 1.19
id: go

- name: Check out code into the Go module directory
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v3
with:
go-version: ^1.16
go-version: 1.19
id: go

- name: Checkout code
Expand Down
36 changes: 27 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
module github.com/napptive/njwt

go 1.16
go 1.19

require (
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/napptive/grpc-common-go v0.8.0 // indirect
github.com/golang/mock v1.6.0
github.com/napptive/go-utils v0.0.0-20230302113223-949e47c65976
github.com/napptive/grpc-jwt-go v0.1.0
github.com/napptive/grpc-ping-go v0.1.0
github.com/napptive/nerrors v1.1.0
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.27.1
github.com/onsi/gomega v1.27.2
github.com/rs/xid v1.4.0
github.com/rs/zerolog v1.29.0
google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44 // indirect
google.golang.org/grpc v1.53.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
syreclabs.com/go/faker v1.2.3
)

require (
github.com/envoyproxy/protoc-gen-validate v0.9.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/napptive/grpc-common-go v0.8.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230301171018-9ab4bdc49ad5 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
1,058 changes: 13 additions & 1,045 deletions go.sum

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions pkg/interceptors/interceptor_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@
package interceptors

import (
"testing"

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"testing"
)

func TestInterceptorr(t *testing.T) {
func TestInterceptors(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "Interceptors Suite")
}
}
9 changes: 4 additions & 5 deletions pkg/interceptors/jwt_interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func JwtInterceptor(config config.JWTConfig) grpc.UnaryServerInterceptor {
}

// add the claim information to the context metadata
newCtx, err := addClaimToContext(claim, ctx)
newCtx, err := AddClaimToContext(claim, ctx)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -92,8 +92,8 @@ func authorizeJWTToken(ctx context.Context, config config.JWTConfig) (*njwt.Clai
return claim, nil
}

// addClaimToContext returns new Context joining the claim information
func addClaimToContext(claim *njwt.Claim, ctx context.Context) (context.Context, error) {
// AddClaimToContext returns new Context joining the claim information
func AddClaimToContext(claim *njwt.Claim, ctx context.Context) (context.Context, error) {
// add the claim information to the context metadata
authMap := claim.GetAuthxClaim().ToMap()
authMap[helper.JWTID] = claim.Id
Expand All @@ -107,7 +107,6 @@ func addClaimToContext(claim *njwt.Claim, ctx context.Context) (context.Context,
fullMD := metadata.Join(oldMD, md)
// and create new context with this one
newCtx := metadata.NewIncomingContext(ctx, fullMD)

return newCtx, nil
}

Expand Down Expand Up @@ -210,7 +209,7 @@ func JwtStreamInterceptor(config config.JWTConfig) grpc.StreamServerInterceptor
}

// add the claim information to the context metadata
newCtx, err := addClaimToContext(authClaim, ctx)
newCtx, err := AddClaimToContext(authClaim, ctx)
if err != nil {
return err
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/interceptors/mocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Copyright 2023 Napptive
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

//go:generate mockgen -destination secret_provider_mock_test.go -package=interceptors github.com/napptive/njwt/pkg/interceptors SecretProvider
//go:generate mockgen -destination secret_client_mock_test.go -package=interceptors github.com/napptive/grpc-jwt-go SecretsClient

package interceptors
57 changes: 57 additions & 0 deletions pkg/interceptors/secret_client_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 49 additions & 0 deletions pkg/interceptors/secret_provider_mock_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions pkg/interceptors/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* Copyright 2023 Napptive
*
* 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package interceptors

import (
"context"
"sync"
"time"

"github.com/napptive/grpc-jwt-go"
"github.com/napptive/nerrors/pkg/nerrors"
"github.com/napptive/njwt/pkg/config"
"github.com/rs/zerolog/log"
)

const ClientTimeout = 30 * time.Second

// CachedSecret stores a JWT secret with the timestamp in which it has been retrieved.
type CachedSecret struct {
timestamp time.Time
secret string
}

// Clone a cached secret.
func (cs *CachedSecret) Clone() *CachedSecret {
return &CachedSecret{
timestamp: cs.timestamp,
secret: cs.secret,
}
}

// InterceptorZoneSecretManager offers a cached zone JWT signing secret retrieval interface. Elements
// retrieved from the SecretsClient are stored in an internal cache for a period of time before being evicted.
type InterceptorZoneSecretManager struct {
sync.RWMutex
// Config with the default signing secret. This is used for backward compatibility with old versions,
// and it will be returned if the zone found in the JWT is empty.
config config.JWTConfig
secretsClient grpc_jwt_go.SecretsClient
zoneCacheTTL time.Duration
SecretCache map[string]*CachedSecret
}

// NewInterceptorZoneSecretManager creates a zone manager that communicates with the secrets service
// to retrieve zone signing secrets.
func NewInterceptorZoneSecretManager(config config.JWTConfig, secretsClient grpc_jwt_go.SecretsClient, zoneCacheTTL time.Duration) SecretProvider {
manager := &InterceptorZoneSecretManager{
config: config,
secretsClient: secretsClient,
zoneCacheTTL: zoneCacheTTL,
SecretCache: make(map[string]*CachedSecret),
}
go manager.evictLoop()
return manager
}

// evictLoop cleans the cache triggering the eviction method.
func (izsm *InterceptorZoneSecretManager) evictLoop() {
ticker := time.NewTicker(izsm.zoneCacheTTL / 2)
for range ticker.C {
izsm.Evict()
}
}

// Evict old entries of the cache attending to the creation timestamp.
func (izsm *InterceptorZoneSecretManager) Evict() {
timeLimit := time.Now().Add(-1 * izsm.zoneCacheTTL)
izsm.Lock()
defer izsm.Unlock()
for zoneID, secret := range izsm.SecretCache {
if secret.timestamp.Before(timeLimit) {
delete(izsm.SecretCache, zoneID)
}
}
}

// GetZoneSecret retrieves JWT signing secret associated with a given zone identifier.
func (izsm *InterceptorZoneSecretManager) GetZoneSecret(zoneID string) (*string, error) {
izsm.RLock()
secret, exists := izsm.SecretCache[zoneID]
izsm.RUnlock()

if exists {
return &secret.secret, nil
}

if zoneID == "" && izsm.config.Secret != "" {
// Preload the cache with the default secret
izsm.Lock()
izsm.SecretCache[""] = &CachedSecret{
timestamp: time.Now(),
secret: izsm.config.Secret,
}
izsm.Unlock()
return &izsm.config.Secret, nil
}
// Retrieve it from the secret provider
ctx, cancel := context.WithTimeout(context.Background(), ClientTimeout)
defer cancel()

zoneSigningSecret, err := izsm.secretsClient.Get(ctx, &grpc_jwt_go.GetSecretRequest{SecretId: zoneID})
if err != nil {
log.Error().Err(err).Str("zone_id", zoneID).Msg("unable to retrieve zone signing secret")
return nil, nerrors.NewInternalError("cannot verify token")
}
izsm.Lock()
izsm.SecretCache[zoneID] = &CachedSecret{
timestamp: time.Now(),
secret: zoneSigningSecret.JwtSecret,
}
izsm.Unlock()
return &zoneSigningSecret.JwtSecret, nil
}
Loading

0 comments on commit 50724d6

Please sign in to comment.