mirror of
https://github.com/1f349/mjwt.git
synced 2024-12-21 23:14:04 +00:00
More documentation, and alias function for loading a KeyStore from a filepath path
This commit is contained in:
parent
7eaf420bb9
commit
87774ec45e
@ -1,3 +1,3 @@
|
|||||||
# MJWT
|
# MJWT
|
||||||
|
|
||||||
A simple wrapper for JWT. Contains an AccessToken and RefreshToken model.
|
A simple wrapper for JWT. Contains an AccessToken and RefreshToken model.
|
||||||
|
@ -7,6 +7,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Issuer provides the signing for a PrivateKey identified by the KID in the
|
||||||
|
// provided KeyStore
|
||||||
type Issuer struct {
|
type Issuer struct {
|
||||||
issuer string
|
issuer string
|
||||||
kid string
|
kid string
|
||||||
@ -14,10 +16,12 @@ type Issuer struct {
|
|||||||
keystore *KeyStore
|
keystore *KeyStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIssuer creates an Issuer with an empty KeyStore
|
||||||
func NewIssuer(name, kid string, signing jwt.SigningMethod) (*Issuer, error) {
|
func NewIssuer(name, kid string, signing jwt.SigningMethod) (*Issuer, error) {
|
||||||
return NewIssuerWithKeyStore(name, kid, signing, NewKeyStore())
|
return NewIssuerWithKeyStore(name, kid, signing, NewKeyStore())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIssuerWithKeyStore creates an Issuer with a provided KeyStore
|
||||||
func NewIssuerWithKeyStore(name, kid string, signing jwt.SigningMethod, keystore *KeyStore) (*Issuer, error) {
|
func NewIssuerWithKeyStore(name, kid string, signing jwt.SigningMethod, keystore *KeyStore) (*Issuer, error) {
|
||||||
i := &Issuer{name, kid, signing, keystore}
|
i := &Issuer{name, kid, signing, keystore}
|
||||||
if i.keystore.HasPrivateKey(kid) {
|
if i.keystore.HasPrivateKey(kid) {
|
||||||
@ -31,10 +35,12 @@ func NewIssuerWithKeyStore(name, kid string, signing jwt.SigningMethod, keystore
|
|||||||
return i, i.keystore.SaveSingleKey(kid)
|
return i, i.keystore.SaveSingleKey(kid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateJwt produces a signed JWT in string form
|
||||||
func (i *Issuer) GenerateJwt(sub, id string, aud jwt.ClaimStrings, dur time.Duration, claims Claims) (string, error) {
|
func (i *Issuer) GenerateJwt(sub, id string, aud jwt.ClaimStrings, dur time.Duration, claims Claims) (string, error) {
|
||||||
return i.SignJwt(wrapClaims[Claims](sub, id, i.issuer, aud, dur, claims))
|
return i.SignJwt(wrapClaims[Claims](sub, id, i.issuer, aud, dur, claims))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignJwt produces a signed JWT in string form from a raw jwt.Claims structure
|
||||||
func (i *Issuer) SignJwt(wrapped jwt.Claims) (string, error) {
|
func (i *Issuer) SignJwt(wrapped jwt.Claims) (string, error) {
|
||||||
key, err := i.PrivateKey()
|
key, err := i.PrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -45,10 +51,12 @@ func (i *Issuer) SignJwt(wrapped jwt.Claims) (string, error) {
|
|||||||
return token.SignedString(key)
|
return token.SignedString(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrivateKey outputs the rsa.PrivateKey from the KID of the Issuer
|
||||||
func (i *Issuer) PrivateKey() (*rsa.PrivateKey, error) {
|
func (i *Issuer) PrivateKey() (*rsa.PrivateKey, error) {
|
||||||
return i.keystore.GetPrivateKey(i.kid)
|
return i.keystore.GetPrivateKey(i.kid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyStore outputs the underlying KeyStore used by the Issuer
|
||||||
func (i *Issuer) KeyStore() *KeyStore {
|
func (i *Issuer) KeyStore() *KeyStore {
|
||||||
return i.keystore
|
return i.keystore
|
||||||
}
|
}
|
||||||
|
1
jwks.go
1
jwks.go
@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WriteJwkSetJson outputs the public keys used by the Issuers
|
||||||
func WriteJwkSetJson(w io.Writer, issuers []*Issuer) error {
|
func WriteJwkSetJson(w io.Writer, issuers []*Issuer) error {
|
||||||
enc := json.NewEncoder(w)
|
enc := json.NewEncoder(w)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
|
64
keystore.go
64
keystore.go
@ -26,12 +26,14 @@ const PemExt = ".pem"
|
|||||||
const PrivatePemExt = PrivateStr + PemExt
|
const PrivatePemExt = PrivateStr + PemExt
|
||||||
const PublicPemExt = PublicStr + PemExt
|
const PublicPemExt = PublicStr + PemExt
|
||||||
|
|
||||||
|
// KeyStore provides a store for a collection of private/public keypair structs
|
||||||
type KeyStore struct {
|
type KeyStore struct {
|
||||||
mu *sync.RWMutex
|
mu *sync.RWMutex
|
||||||
store map[string]*keyPair
|
store map[string]*keyPair
|
||||||
dir afero.Fs
|
dir afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewKeyStore creates an empty KeyStore
|
||||||
func NewKeyStore() *KeyStore {
|
func NewKeyStore() *KeyStore {
|
||||||
return &KeyStore{
|
return &KeyStore{
|
||||||
mu: new(sync.RWMutex),
|
mu: new(sync.RWMutex),
|
||||||
@ -39,12 +41,28 @@ func NewKeyStore() *KeyStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewKeyStoreWithDir creates an empty KeyStore with an underlying afero.Fs
|
||||||
|
// filesystem for saving the internal store data
|
||||||
func NewKeyStoreWithDir(dir afero.Fs) *KeyStore {
|
func NewKeyStoreWithDir(dir afero.Fs) *KeyStore {
|
||||||
keyStore := NewKeyStore()
|
keyStore := NewKeyStore()
|
||||||
keyStore.dir = dir
|
keyStore.dir = dir
|
||||||
return keyStore
|
return keyStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewKeyStoreFromPath creates an empty KeyStore. The provided path is walked to
|
||||||
|
// load the private/public keys. See implementation in NewKeyStoreFromDir.
|
||||||
|
func NewKeyStoreFromPath(dir string) (*KeyStore, error) {
|
||||||
|
abs, err := filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewKeyStoreFromDir(afero.NewBasePathFs(afero.NewOsFs(), abs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewKeyStoreFromDir creates an empty KeyStore. The provided afero.Fs is walked
|
||||||
|
// to find all private/public keys in files named `.private.pem` and
|
||||||
|
// `.public.pem` respectively. The keys are loaded into the KeyStore and any
|
||||||
|
// errors are returned immediately.
|
||||||
func NewKeyStoreFromDir(dir afero.Fs) (*KeyStore, error) {
|
func NewKeyStoreFromDir(dir afero.Fs) (*KeyStore, error) {
|
||||||
keyStore := NewKeyStoreWithDir(dir)
|
keyStore := NewKeyStoreWithDir(dir)
|
||||||
err := afero.Walk(dir, ".", func(path string, d fs.FileInfo, err error) error {
|
err := afero.Walk(dir, ".", func(path string, d fs.FileInfo, err error) error {
|
||||||
@ -94,6 +112,7 @@ type keyPair struct {
|
|||||||
public *rsa.PublicKey
|
public *rsa.PublicKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadPrivateKey sets the rsa.PrivateKey/rsa.PublicKey for the KID
|
||||||
func (k *KeyStore) LoadPrivateKey(kid string, key *rsa.PrivateKey) {
|
func (k *KeyStore) LoadPrivateKey(kid string, key *rsa.PrivateKey) {
|
||||||
k.mu.Lock()
|
k.mu.Lock()
|
||||||
if k.store[kid] == nil {
|
if k.store[kid] == nil {
|
||||||
@ -104,6 +123,7 @@ func (k *KeyStore) LoadPrivateKey(kid string, key *rsa.PrivateKey) {
|
|||||||
k.mu.Unlock()
|
k.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadPublicKey sets the rsa.PublicKey for the KID
|
||||||
func (k *KeyStore) LoadPublicKey(kid string, key *rsa.PublicKey) {
|
func (k *KeyStore) LoadPublicKey(kid string, key *rsa.PublicKey) {
|
||||||
k.mu.Lock()
|
k.mu.Lock()
|
||||||
if k.store[kid] == nil {
|
if k.store[kid] == nil {
|
||||||
@ -113,12 +133,14 @@ func (k *KeyStore) LoadPublicKey(kid string, key *rsa.PublicKey) {
|
|||||||
k.mu.Unlock()
|
k.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveKey deletes the KID keypair from the KeyStore
|
||||||
func (k *KeyStore) RemoveKey(kid string) {
|
func (k *KeyStore) RemoveKey(kid string) {
|
||||||
k.mu.Lock()
|
k.mu.Lock()
|
||||||
delete(k.store, kid)
|
delete(k.store, kid)
|
||||||
k.mu.Unlock()
|
k.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListKeys provides a slice of the KIDs for all keys loaded in the KeyStore
|
||||||
func (k *KeyStore) ListKeys() []string {
|
func (k *KeyStore) ListKeys() []string {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
@ -129,6 +151,7 @@ func (k *KeyStore) ListKeys() []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPrivateKey outputs the rsa.PrivateKey for the KID from the KeyStore
|
||||||
func (k *KeyStore) GetPrivateKey(kid string) (*rsa.PrivateKey, error) {
|
func (k *KeyStore) GetPrivateKey(kid string) (*rsa.PrivateKey, error) {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
@ -138,6 +161,7 @@ func (k *KeyStore) GetPrivateKey(kid string) (*rsa.PrivateKey, error) {
|
|||||||
return k.store[kid].private, nil
|
return k.store[kid].private, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPublicKey outputs the rsa.PublicKey for the KID from the KeyStore
|
||||||
func (k *KeyStore) GetPublicKey(kid string) (*rsa.PublicKey, error) {
|
func (k *KeyStore) GetPublicKey(kid string) (*rsa.PublicKey, error) {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
@ -147,12 +171,15 @@ func (k *KeyStore) GetPublicKey(kid string) (*rsa.PublicKey, error) {
|
|||||||
return k.store[kid].public, nil
|
return k.store[kid].public, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClearKeys clears the internal map and makes a new map to release used memory
|
||||||
func (k *KeyStore) ClearKeys() {
|
func (k *KeyStore) ClearKeys() {
|
||||||
k.mu.Lock()
|
k.mu.Lock()
|
||||||
clear(k.store)
|
clear(k.store)
|
||||||
|
k.store = make(map[string]*keyPair)
|
||||||
k.mu.Unlock()
|
k.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasPrivateKey outputs true if the KID is found in the KeyStore
|
||||||
func (k *KeyStore) HasPrivateKey(kid string) bool {
|
func (k *KeyStore) HasPrivateKey(kid string) bool {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
@ -164,6 +191,7 @@ func (k *KeyStore) internalHasPrivateKey(kid string) bool {
|
|||||||
return v != nil && v.private != nil
|
return v != nil && v.private != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasPublicKey outputs true if the KID is found in the KeyStore
|
||||||
func (k *KeyStore) HasPublicKey(kid string) bool {
|
func (k *KeyStore) HasPublicKey(kid string) bool {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
@ -175,6 +203,9 @@ func (k *KeyStore) internalHasPublicKey(kid string) bool {
|
|||||||
return v != nil && v.public != nil
|
return v != nil && v.public != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyJwt parses the provided token string and validates it against the KID
|
||||||
|
// using the KeyStore. An error is returned if the token fails to parse or if
|
||||||
|
// there is no matching KID in the KeyStore.
|
||||||
func (k *KeyStore) VerifyJwt(token string, claims baseTypeClaim) (*jwt.Token, error) {
|
func (k *KeyStore) VerifyJwt(token string, claims baseTypeClaim) (*jwt.Token, error) {
|
||||||
withClaims, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
|
withClaims, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
|
||||||
kid, ok := token.Header["kid"].(string)
|
kid, ok := token.Header["kid"].(string)
|
||||||
@ -189,6 +220,8 @@ func (k *KeyStore) VerifyJwt(token string, claims baseTypeClaim) (*jwt.Token, er
|
|||||||
return withClaims, claims.Valid()
|
return withClaims, claims.Valid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveSingleKey writes the rsa.PrivateKey/rsa.PublicKey for the requested KID to
|
||||||
|
// the underlying afero.Fs.
|
||||||
func (k *KeyStore) SaveSingleKey(kid string) error {
|
func (k *KeyStore) SaveSingleKey(kid string) error {
|
||||||
if k.dir == nil {
|
if k.dir == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -201,16 +234,11 @@ func (k *KeyStore) SaveSingleKey(kid string) error {
|
|||||||
return ErrMissingKeyPair
|
return ErrMissingKeyPair
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
return writeSingleKey(k.dir, kid, pair)
|
||||||
if pair.private != nil {
|
|
||||||
errs = append(errs, afero.WriteFile(k.dir, kid+PrivatePemExt, rsaprivate.Encode(pair.private), 0600))
|
|
||||||
}
|
|
||||||
if pair.public != nil {
|
|
||||||
errs = append(errs, afero.WriteFile(k.dir, kid+PublicPemExt, rsapublic.Encode(pair.public), 0600))
|
|
||||||
}
|
|
||||||
return errors.Join(errs...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveKeys writes the rsa.PrivateKey/rsa.PublicKey for the requested KID to the
|
||||||
|
// underlying afero.Fs.
|
||||||
func (k *KeyStore) SaveKeys() error {
|
func (k *KeyStore) SaveKeys() error {
|
||||||
k.mu.RLock()
|
k.mu.RLock()
|
||||||
defer k.mu.RUnlock()
|
defer k.mu.RUnlock()
|
||||||
@ -219,15 +247,19 @@ func (k *KeyStore) SaveKeys() error {
|
|||||||
workers.SetLimit(runtime.NumCPU())
|
workers.SetLimit(runtime.NumCPU())
|
||||||
for kid, pair := range k.store {
|
for kid, pair := range k.store {
|
||||||
workers.Go(func() error {
|
workers.Go(func() error {
|
||||||
var errs []error
|
return writeSingleKey(k.dir, kid, pair)
|
||||||
if pair.private != nil {
|
|
||||||
errs = append(errs, afero.WriteFile(k.dir, kid+PrivatePemExt, rsaprivate.Encode(pair.private), 0600))
|
|
||||||
}
|
|
||||||
if pair.public != nil {
|
|
||||||
errs = append(errs, afero.WriteFile(k.dir, kid+PublicPemExt, rsapublic.Encode(pair.public), 0600))
|
|
||||||
}
|
|
||||||
return errors.Join(errs...)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return workers.Wait()
|
return workers.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeSingleKey(dir afero.Fs, kid string, pair *keyPair) error {
|
||||||
|
var errs []error
|
||||||
|
if pair.private != nil {
|
||||||
|
errs = append(errs, afero.WriteFile(dir, kid+PrivatePemExt, rsaprivate.Encode(pair.private), 0600))
|
||||||
|
}
|
||||||
|
if pair.public != nil {
|
||||||
|
errs = append(errs, afero.WriteFile(dir, kid+PublicPemExt, rsapublic.Encode(pair.public), 0600))
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user