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
Added image resizer for responsive web apps
This commit is contained in:
@ -1,10 +1,19 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"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
|
||||
}
|
||||
|
Reference in New Issue
Block a user