You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-06-15 00:15:15 +02:00
JWT authenticator with AWS Secret Manager
Updated authenticator to store JWT private keys in AWS secret manager. This improvement reduces the need to generate a local privite key for the application to start on its initial run.
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
aws.lee
|
@ -2,10 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
@ -19,9 +17,12 @@ import (
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/db"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/flag"
|
||||
itrace "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/trace"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/kelseyhightower/envconfig"
|
||||
"go.opencensus.io/trace"
|
||||
awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
|
||||
)
|
||||
|
||||
/*
|
||||
@ -68,10 +69,19 @@ func main() {
|
||||
SendInterval time.Duration `default:"15s" envconfig:"SEND_INTERVAL"`
|
||||
SendTimeout time.Duration `default:"500ms" envconfig:"SEND_TIMEOUT"`
|
||||
}
|
||||
AwsAccount struct {
|
||||
AccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"`
|
||||
SecretAccessKey string `envconfig:"AWS_SECRET_ACCESS_KEY"`
|
||||
Region string `default:"us-east-1" envconfig:"AWS_REGION"`
|
||||
|
||||
// Get an AWS session from an implicit source if no explicit
|
||||
// configuration is provided. This is useful for taking advantage of
|
||||
// EC2/ECS instance roles.
|
||||
UseRole bool `envconfig:"AWS_USE_ROLE"`
|
||||
}
|
||||
Auth struct {
|
||||
KeyID string `envconfig:"KEY_ID"`
|
||||
PrivateKeyFile string `default:"/app/private.pem" envconfig:"PRIVATE_KEY_FILE"`
|
||||
Algorithm string `default:"RS256" envconfig:"ALGORITHM"`
|
||||
AwsSecretID string `default:"auth-secret-key" envconfig:"AUTH_AWS_SECRET_ID"`
|
||||
KeyExpiration time.Duration `default:"3600s" envconfig:"AUTH_KEY_EXPIRATION"`
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,21 +114,22 @@ func main() {
|
||||
log.Printf("main : Config : %v\n", string(cfgJSON))
|
||||
|
||||
// =========================================================================
|
||||
// Find auth keys
|
||||
|
||||
keyContents, err := ioutil.ReadFile(cfg.Auth.PrivateKeyFile)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Reading auth private key : %v", err)
|
||||
// Init AWS Session
|
||||
var awsSession *session.Session
|
||||
if cfg.AwsAccount.UseRole {
|
||||
// Get an AWS session from an implicit source if no explicit
|
||||
// configuration is provided. This is useful for taking advantage of
|
||||
// EC2/ECS instance roles.
|
||||
awsSession = session.Must(session.NewSession())
|
||||
} else {
|
||||
creds := credentials.NewStaticCredentials(cfg.AwsAccount.AccessKeyID, cfg.AwsAccount.SecretAccessKey, "")
|
||||
awsSession = session.New(&aws.Config{Region: aws.String(cfg.AwsAccount.Region), Credentials: creds})
|
||||
}
|
||||
awsSession = awstrace.WrapSession(awsSession)
|
||||
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyContents)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Parsing auth private key : %v", err)
|
||||
}
|
||||
|
||||
publicKeyLookup := auth.NewSingleKeyFunc(cfg.Auth.KeyID, key.Public().(*rsa.PublicKey))
|
||||
|
||||
authenticator, err := auth.NewAuthenticator(key, cfg.Auth.KeyID, cfg.Auth.Algorithm, publicKeyLookup)
|
||||
// =========================================================================
|
||||
// Load auth keys from AWS and init new Authenticator
|
||||
authenticator, err := auth.NewAuthenticator(awsSession, cfg.Auth.AwsSecretID, time.Now().UTC(), cfg.Auth.KeyExpiration)
|
||||
if err != nil {
|
||||
log.Fatalf("main : Constructing authenticator : %v", err)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
module geeks-accelerator/oss/saas-starter-kit/example-project
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.19.32
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/dimfeld/httptreemux v5.0.1+incompatible
|
||||
github.com/go-playground/locales v0.12.1
|
||||
@ -11,12 +12,15 @@ require (
|
||||
github.com/leodido/go-urn v1.1.0 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.1.1
|
||||
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3
|
||||
github.com/philhofer/fwd v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.8.0
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.0 // indirect
|
||||
go.opencensus.io v0.14.0
|
||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.13.0
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
|
||||
gopkg.in/go-playground/validator.v9 v9.28.0
|
||||
|
@ -1,3 +1,5 @@
|
||||
github.com/aws/aws-sdk-go v1.19.32 h1:/usjSR6qsKfOKzk4tDNvZq7LqmP5+J0Cq/Uwsr2XVG8=
|
||||
github.com/aws/aws-sdk-go v1.19.32/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
@ -10,6 +12,8 @@ github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rm
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM=
|
||||
github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
@ -23,6 +27,8 @@ github.com/openzipkin/zipkin-go v0.1.1 h1:A/ADD6HaPnAKj3yS7HjGHRK77qi41Hi0DirOOI
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3 h1:9J0mOv1rXIBlRjQCiAGyx9C3dZZh5uIa3HU0oTV8v1E=
|
||||
github.com/pborman/uuid v0.0.0-20180122190007-c65b2f87fee3/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
|
||||
github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@ -30,6 +36,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
go.opencensus.io v0.14.0 h1:1eTLxqxSIAylcKoxnNkdhvvBNZDA8JwkKNXxgyma0IA=
|
||||
go.opencensus.io v0.14.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
|
||||
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ=
|
||||
@ -38,6 +46,8 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmy
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.13.0 h1:8e4PNOH1Iq9ZwaY/IUjMIpzpsCweNNakKTiDsWc3ZJE=
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.13.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -1,10 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/secretsmanager"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -19,15 +28,15 @@ import (
|
||||
// endpoint. See https://auth0.com/docs/jwks for more details.
|
||||
type KeyFunc func(keyID string) (*rsa.PublicKey, error)
|
||||
|
||||
// NewSingleKeyFunc is a simple implementation of KeyFunc that only ever
|
||||
// supports one key. This is easy for development but in projection should be
|
||||
// replaced with a caching layer that calls a JWKS endpoint.
|
||||
func NewSingleKeyFunc(id string, key *rsa.PublicKey) KeyFunc {
|
||||
// NewKeyFunc is a multiple implementation of KeyFunc that
|
||||
// supports a map of keys.
|
||||
func NewKeyFunc(keys map[string]*rsa.PrivateKey) KeyFunc {
|
||||
return func(kid string) (*rsa.PublicKey, error) {
|
||||
if id != kid {
|
||||
key, ok := keys[kid]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unrecognized kid %q", kid)
|
||||
}
|
||||
return key, nil
|
||||
return key.Public().(*rsa.PublicKey), nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,21 +50,193 @@ type Authenticator struct {
|
||||
parser *jwt.Parser
|
||||
}
|
||||
|
||||
// NewAuthenticator creates an *Authenticator for use. It will error if:
|
||||
// - The private key is nil.
|
||||
// - The public key func is nil.
|
||||
// - The key ID is blank.
|
||||
// NewAuthenticator creates an *Authenticator for use.
|
||||
// key expiration is optional to filter out old keys
|
||||
// It will error if:
|
||||
// - The aws session is nil.
|
||||
// - The aws secret id is blank.
|
||||
// - The specified algorithm is unsupported.
|
||||
func NewAuthenticator(key *rsa.PrivateKey, keyID, algorithm string, publicKeyFunc KeyFunc) (*Authenticator, error) {
|
||||
if key == nil {
|
||||
return nil, errors.New("private key cannot be nil")
|
||||
func NewAuthenticator(awsSession *session.Session, awsSecretID string, now time.Time, keyExpiration time.Duration) (*Authenticator, error) {
|
||||
if awsSession == nil {
|
||||
return nil, errors.New("aws session cannot be nil")
|
||||
}
|
||||
if publicKeyFunc == nil {
|
||||
return nil, errors.New("public key function cannot be nil")
|
||||
|
||||
if awsSecretID == "" {
|
||||
return nil, errors.New("aws secret id cannot be empty")
|
||||
}
|
||||
if keyID == "" {
|
||||
return nil, errors.New("keyID cannot be blank")
|
||||
|
||||
if now.IsZero() {
|
||||
now = time.Now().UTC()
|
||||
}
|
||||
|
||||
// Time threshold to stop loading keys, any key with a created date
|
||||
// before this value will not be loaded.
|
||||
var disabledCreatedDate time.Time
|
||||
|
||||
// Time threshold to create a new key. If a current key exists and the
|
||||
// created date of the key is before this value, a new key will be created.
|
||||
var activeCreatedDate time.Time
|
||||
|
||||
// If an expiration duration is included, convert to past time from now.
|
||||
if keyExpiration.Seconds() != 0 {
|
||||
// Ensure the expiration is a time in the past for comparison below.
|
||||
if keyExpiration.Seconds() > 0 {
|
||||
keyExpiration = keyExpiration * -1
|
||||
}
|
||||
// Stop loading keys when the created date exceeds two times the key expiration
|
||||
disabledCreatedDate = now.UTC().Add(keyExpiration * 2)
|
||||
|
||||
// Time used to determine when a new key should be created.
|
||||
activeCreatedDate = now.UTC().Add(keyExpiration)
|
||||
}
|
||||
|
||||
// Init new AWS Secret Manager using provided AWS session.
|
||||
secretManager := secretsmanager.New(awsSession)
|
||||
|
||||
// A List of version ids for the stored secret. All keys will be stored under
|
||||
// the same name in AWS secret manager. We still want to load old keys for a
|
||||
// short period of time to ensure any requests in flight have the opportunity
|
||||
// to be completed.
|
||||
var versionIds []string
|
||||
|
||||
// Exec call to AWS secret manager to return a list of version ids for the
|
||||
// provided secret ID.
|
||||
listParams := &secretsmanager.ListSecretVersionIdsInput{
|
||||
SecretId: aws.String(awsSecretID),
|
||||
}
|
||||
err := secretManager.ListSecretVersionIdsPages(listParams,
|
||||
func(page *secretsmanager.ListSecretVersionIdsOutput, lastPage bool) bool {
|
||||
for _, v := range page.Versions {
|
||||
// When disabled CreatedDate is not empty, compare the created date
|
||||
// for each key version to the disabled cut off time.
|
||||
if !disabledCreatedDate.IsZero() && v.CreatedDate != nil && !v.CreatedDate.IsZero() {
|
||||
// Skip any version ids that are less than the expiration time.
|
||||
if v.CreatedDate.UTC().Unix() < disabledCreatedDate.UTC().Unix() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if v.VersionId != nil {
|
||||
versionIds = append(versionIds, *v.VersionId)
|
||||
}
|
||||
}
|
||||
return !lastPage
|
||||
},
|
||||
)
|
||||
|
||||
// Flag whether the secret exists and update needs to be used
|
||||
// instead of create.
|
||||
var awsSecretIDNotFound bool
|
||||
if err != nil {
|
||||
if aerr, ok := err.(awserr.Error); ok {
|
||||
switch aerr.Code() {
|
||||
case secretsmanager.ErrCodeResourceNotFoundException:
|
||||
awsSecretIDNotFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !awsSecretIDNotFound {
|
||||
return nil, errors.Wrapf(err, "aws list secret version ids for secret ID %s failed", awsSecretID)
|
||||
}
|
||||
}
|
||||
|
||||
// Map of keys stored by version id. version id is kid.
|
||||
keyContents := make(map[string][]byte)
|
||||
|
||||
// The current key id if there is an active one.
|
||||
var curKeyId string
|
||||
|
||||
// If the list of version ids is not empty, load the keys from secret manager.
|
||||
if len(versionIds) > 0 {
|
||||
// The max created data to determine the most recent key.
|
||||
var lastCreatedDate time.Time
|
||||
|
||||
for _, id := range versionIds {
|
||||
res, err := secretManager.GetSecretValue(&secretsmanager.GetSecretValueInput{
|
||||
SecretId: aws.String(awsSecretID),
|
||||
VersionId: aws.String(id),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "aws secret id %s, version id %s value failed", awsSecretID, id)
|
||||
}
|
||||
|
||||
if len(res.SecretBinary) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
keyContents[*res.VersionId] = res.SecretBinary
|
||||
|
||||
if lastCreatedDate.IsZero() || res.CreatedDate.UTC().Unix() > lastCreatedDate.UTC().Unix() {
|
||||
curKeyId = *res.VersionId
|
||||
lastCreatedDate = res.CreatedDate.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
if !activeCreatedDate.IsZero() && lastCreatedDate.UTC().Unix() < activeCreatedDate.UTC().Unix() {
|
||||
curKeyId = ""
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no keys stored in secret manager, create a new one or
|
||||
// if the current key needs to be rotated, generate a new key and update the secret.
|
||||
// @TODO: When a new key is generated and there are multiple instances of the service running
|
||||
// its possible based on the key expiration set that requests fail because keys are only
|
||||
// refreshed on instance launch. Could store keys in a kv store and update that value
|
||||
// when new keys are generated
|
||||
if len(keyContents) == 0 || curKeyId == "" {
|
||||
privateKey, err := keygen()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to generate new private key")
|
||||
}
|
||||
|
||||
if awsSecretIDNotFound {
|
||||
res, err := secretManager.CreateSecret(&secretsmanager.CreateSecretInput{
|
||||
Name: aws.String(awsSecretID),
|
||||
SecretBinary: privateKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create new secret with private key")
|
||||
}
|
||||
curKeyId = *res.VersionId
|
||||
} else {
|
||||
res, err := secretManager.UpdateSecret(&secretsmanager.UpdateSecretInput{
|
||||
SecretId: aws.String(awsSecretID),
|
||||
SecretBinary: privateKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create new secret with private key")
|
||||
}
|
||||
curKeyId = *res.VersionId
|
||||
}
|
||||
|
||||
keyContents[curKeyId] = privateKey
|
||||
}
|
||||
|
||||
// Map of keys by kid (version id).
|
||||
keys := make(map[string]*rsa.PrivateKey)
|
||||
|
||||
// The current active key to be used.
|
||||
var curPrivateKey *rsa.PrivateKey
|
||||
|
||||
// Loop through all the key bytes and load the private key.
|
||||
for kid, keyContent := range keyContents {
|
||||
key, err := jwt.ParseRSAPrivateKeyFromPEM(keyContent)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing auth private key")
|
||||
}
|
||||
keys[kid] = key
|
||||
if kid == curKeyId {
|
||||
curPrivateKey = key
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup function to be used by the middleware to validate the kid and
|
||||
// Return the associated public key.
|
||||
publicKeyLookup := NewKeyFunc(keys)
|
||||
|
||||
// Algorithm to be used to for the private key.
|
||||
algorithm := "RS256"
|
||||
if jwt.GetSigningMethod(algorithm) == nil {
|
||||
return nil, errors.Errorf("unknown algorithm %v", algorithm)
|
||||
}
|
||||
@ -68,10 +249,10 @@ func NewAuthenticator(key *rsa.PrivateKey, keyID, algorithm string, publicKeyFun
|
||||
}
|
||||
|
||||
a := Authenticator{
|
||||
privateKey: key,
|
||||
keyID: keyID,
|
||||
privateKey: curPrivateKey,
|
||||
keyID: curKeyId,
|
||||
algorithm: algorithm,
|
||||
kf: publicKeyFunc,
|
||||
kf: publicKeyLookup,
|
||||
parser: &parser,
|
||||
}
|
||||
|
||||
@ -125,3 +306,23 @@ func (a *Authenticator) ParseClaims(tknStr string) (Claims, error) {
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// keygen creates an x509 private key for signing auth tokens.
|
||||
func keygen() ([]byte, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return []byte{}, errors.Wrap(err, "generating keys")
|
||||
}
|
||||
|
||||
block := pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := pem.Encode(buf, &block); err != nil {
|
||||
return []byte{}, errors.Wrap(err, "encoding to private file")
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
@ -1,29 +1,43 @@
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/auth"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests"
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
|
||||
var test *tests.Test
|
||||
|
||||
// TestMain is the entry point for testing.
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testMain(m))
|
||||
}
|
||||
|
||||
func testMain(m *testing.M) int {
|
||||
test = tests.New()
|
||||
defer test.TearDown()
|
||||
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func TestAuthenticator(t *testing.T) {
|
||||
|
||||
// Parse the private key used to generate the token.
|
||||
prvKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateRSAKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
awsSecretID := "jwt-key" + uuid.NewRandom().String()
|
||||
|
||||
// Parse the public key used to validate the token.
|
||||
pubKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(publicRSAKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a, err := auth.NewAuthenticator(prvKey, privateRSAKeyID, "RS256", auth.NewSingleKeyFunc(privateRSAKeyID, pubKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
var authTests = []struct {
|
||||
name string
|
||||
awsSecretID string
|
||||
now time.Time
|
||||
keyExpiration time.Duration
|
||||
error error
|
||||
}{
|
||||
{"NoKeyExpiration", awsSecretID, time.Now(), time.Duration(0), nil},
|
||||
{"KeyExpirationOk", awsSecretID, time.Now(), time.Duration(time.Second * 3600), nil},
|
||||
{"KeyExpirationDisabled", awsSecretID, time.Now().Add(time.Second * 3600 * 3), time.Duration(time.Second * 3600), nil},
|
||||
}
|
||||
|
||||
// Generate the token.
|
||||
@ -31,67 +45,44 @@ func TestAuthenticator(t *testing.T) {
|
||||
Roles: []string{auth.RoleAdmin},
|
||||
}
|
||||
|
||||
tknStr, err := a.GenerateToken(signedClaims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log("Given the need to validate initiating a new Authenticator by key expiration.")
|
||||
{
|
||||
for i, tt := range authTests {
|
||||
t.Logf("\tTest: %d\tWhen running test: %s", i, tt.name)
|
||||
{
|
||||
a, err := auth.NewAuthenticator(test.AwsSession, tt.awsSecretID, tt.now, tt.keyExpiration)
|
||||
if err != tt.error {
|
||||
t.Log("\t\tGot :", err)
|
||||
t.Log("\t\tWant:", tt.error)
|
||||
t.Fatalf("\t%s\tNewAuthenticator failed.", tests.Failed)
|
||||
}
|
||||
|
||||
parsedClaims, err := a.ParseClaims(tknStr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tknStr, err := a.GenerateToken(signedClaims)
|
||||
if err != nil {
|
||||
t.Log("\t\tGot :", err)
|
||||
t.Fatalf("\t%s\tGenerateToken failed.", tests.Failed)
|
||||
}
|
||||
|
||||
// Assert expected claims.
|
||||
if exp, got := len(signedClaims.Roles), len(parsedClaims.Roles); exp != got {
|
||||
t.Fatalf("expected %v roles, got %v", exp, got)
|
||||
}
|
||||
if exp, got := signedClaims.Roles[0], parsedClaims.Roles[0]; exp != got {
|
||||
t.Fatalf("expected roles[0] == %v, got %v", exp, got)
|
||||
parsedClaims, err := a.ParseClaims(tknStr)
|
||||
if err != nil {
|
||||
t.Log("\t\tGot :", err)
|
||||
t.Fatalf("\t%s\tParseClaims failed.", tests.Failed)
|
||||
}
|
||||
|
||||
// Assert expected claims.
|
||||
if exp, got := len(signedClaims.Roles), len(parsedClaims.Roles); exp != got {
|
||||
t.Log("\t\tGot :", got)
|
||||
t.Log("\t\tWant:", exp)
|
||||
t.Fatalf("\t%s\tShould got the same number of roles.", tests.Failed)
|
||||
}
|
||||
if exp, got := signedClaims.Roles[0], parsedClaims.Roles[0]; exp != got {
|
||||
t.Log("\t\tGot :", got)
|
||||
t.Log("\t\tWant:", exp)
|
||||
t.Fatalf("\t%s\tShould got the same role name.", tests.Failed)
|
||||
}
|
||||
|
||||
t.Logf("\t%s\tNewAuthenticator ok.", tests.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The key id we would have generated for the private below key
|
||||
const privateRSAKeyID = "54bb2165-71e1-41a6-af3e-7da4a0e1e2c1"
|
||||
|
||||
// Output of:
|
||||
// openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
|
||||
const privateRSAKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDdiBDU4jqRYuHl
|
||||
yBmo5dWB1j9aeDrXzUTJbRKlgo+DWDQzIzJQvackvRu8/f7B5cseoqmeJcmBu6pc
|
||||
4DmQ+puGNHxzCyYVFSMwRtHBZvfWS3P+UqIXCKRAX/NZbLkUEeqPnn5WXjA+YXKk
|
||||
sfniE0xDH8W22o0OXHOzRhDWORjNTulpMpLv8tKnnLKh2Y/kCL/4vo0SZ+RWh8F9
|
||||
4+JTZx/47RHWb6fkxkikyTO3zO3efIkrKjfRx2CwFwO2rQ/3T04GQB/Lgr5lfJQU
|
||||
iofvvVYuj2xBJao+3t9Ir0OeSbw1T5Rz03VLtN8SZhvaxWaBfwkUuUNL1glJO+Yd
|
||||
LkMxGS0zAgMBAAECggEBAKM6m7RQUPlJE8u8qfOCDdSSKbIefrT9wZ5tKN0dG2Oa
|
||||
/TNkzrEhXOO8F5Ek0a7LA+Q51KL7ksNtpLS0XpZNoYS8bapS36ePIJN0yx8nIJwc
|
||||
koYlGtu/+U6ZpHQSoTiBjwRtswcudXuxT8i8frOupnWbFpKJ7H9Vbcb9bHB8N6Mm
|
||||
D63wSBR08ZMrZXheKHQCQcxSQ2ZQZ+X3LBIOdXZH1aaptU2KpMEU5oyxXPShTVMg
|
||||
0f748yU2njXCF0ZABEanXgp13egr/MPqHwnS/h0PH45bNy3IgFtMEHEouQFsAzoS
|
||||
qNe8/9WnrpY87UdSZMnzF/IAXV0bmollDnqfM8/EqxkCgYEA96ThXYGzAK5RKNqp
|
||||
RqVdRVA0UTT48sJvrxLMuHpyUzg6cl8FZE5rrNxFbouxvyN192Ctv1q8yfv4/HfM
|
||||
KpmtEjt3fYtITHVXII6O3qNaRoIEPwKT4eK/ar+JO59vI0YvweXvDH5TkS9aiFr+
|
||||
pPGf3a7EbE24BKhgiI8eT6K0VuUCgYEA5QGg11ZVoUut4ERAPouwuSdWwNe0HYqJ
|
||||
A1m5vTvF5ghUHAb023lrr7Psq9DPJQQe7GzPfXafsat9hGenyqiyxo1gwClIyoEH
|
||||
fOg753kdHcy60VVzumsPXece3OOSnd0rRMgfsSsclgYO7z0g9YZPAjt2w9NVw6uN
|
||||
UDqX3eO2WjcCgYEA015eoNHv99fRG96udsbz+hI/5UQibAl7C+Iu7BJO/CrU8AOc
|
||||
dYXdr5f+hyEioDLjIDbbdaU71+aCGPMjRwUNzK8HCRfVqLTKndYvqWWhyuZ0O1e2
|
||||
4ykHGlTLDCHD2Uaxwny/8VjteNEDI7kO+bfmLG9b5djcBNW2Nzh4tZ348OUCgYEA
|
||||
vIrTppbhF1QciqkGj7govrgBt/GfzDaTyZtkzcTZkSNIRG8Bx3S3UUh8UZUwBpTW
|
||||
9OY9ClnQ7tF3HLzOq46q6cfaYTtcP8Vtqcv2DgRsEW3OXazSBChC1ZgEk+4Vdz1x
|
||||
c0akuRP6jBXe099rNFno0LiudlmXoeqrBOPIxxnEt48CgYEAxNZBc/GKiHXz/ZRi
|
||||
IZtRT5rRRof7TEiDxSKOXHSG7HhIRDCrpwn4Dfi+GWNHIwsIlom8FzZTSHAN6pqP
|
||||
E8Imrlt3vuxnUE1UMkhDXrlhrxslRXU9enynVghAcSrg6ijs8KuN/9RB/I7H03cT
|
||||
77mx9eHMcYcRUciY5C8AOaArmMA=
|
||||
-----END PRIVATE KEY-----`
|
||||
|
||||
// Output of:
|
||||
// openssl rsa -pubout -in private.pem -out public.pem
|
||||
const publicRSAKey = `-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3YgQ1OI6kWLh5cgZqOXV
|
||||
gdY/Wng6181EyW0SpYKPg1g0MyMyUL2nJL0bvP3+weXLHqKpniXJgbuqXOA5kPqb
|
||||
hjR8cwsmFRUjMEbRwWb31ktz/lKiFwikQF/zWWy5FBHqj55+Vl4wPmFypLH54hNM
|
||||
Qx/FttqNDlxzs0YQ1jkYzU7paTKS7/LSp5yyodmP5Ai/+L6NEmfkVofBfePiU2cf
|
||||
+O0R1m+n5MZIpMkzt8zt3nyJKyo30cdgsBcDtq0P909OBkAfy4K+ZXyUFIqH771W
|
||||
Lo9sQSWqPt7fSK9Dnkm8NU+Uc9N1S7TfEmYb2sVmgX8JFLlDS9YJSTvmHS5DMRkt
|
||||
MwIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
|
@ -3,6 +3,7 @@ package tests
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
@ -23,9 +24,10 @@ const (
|
||||
|
||||
// Test owns state for running/shutting down tests.
|
||||
type Test struct {
|
||||
Log *log.Logger
|
||||
MasterDB *db.DB
|
||||
container *docker.Container
|
||||
Log *log.Logger
|
||||
MasterDB *db.DB
|
||||
container *docker.Container
|
||||
AwsSession *session.Session
|
||||
}
|
||||
|
||||
// New is the entry point for tests.
|
||||
@ -36,6 +38,10 @@ func New() *Test {
|
||||
|
||||
log := log.New(os.Stdout, "TEST : ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)
|
||||
|
||||
// ============================================================
|
||||
// Init AWS Session
|
||||
awsSession := session.Must(session.NewSession())
|
||||
|
||||
// ============================================================
|
||||
// Startup Mongo container
|
||||
|
||||
@ -59,7 +65,7 @@ func New() *Test {
|
||||
log.Fatalf("startup : Register DB : %v", err)
|
||||
}
|
||||
|
||||
return &Test{log, masterDB, container}
|
||||
return &Test{log, masterDB, container, awsSession}
|
||||
}
|
||||
|
||||
// TearDown is used for shutting down tests. Calling this should be
|
||||
|
Reference in New Issue
Block a user