initial commit
This commit is contained in:
263
storage/storage.go
Normal file
263
storage/storage.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base32"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
drivers = make(map[string]Driver)
|
||||
|
||||
// stubbed out for testing
|
||||
now = time.Now
|
||||
)
|
||||
|
||||
// ErrNotFound is the error returned by storages if a resource cannot be found.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// Kubernetes only allows lower case letters for names.
|
||||
//
|
||||
// TODO(ericchiang): refactor ID creation onto the storage.
|
||||
var encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
|
||||
|
||||
// NewNonce returns a new ID for the objects.
|
||||
func NewNonce() string {
|
||||
buff := make([]byte, 8) // 64 bit random ID.
|
||||
if _, err := io.ReadFull(rand.Reader, buff); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Trim padding
|
||||
return strings.TrimRight(encoding.EncodeToString(buff), "=")
|
||||
}
|
||||
|
||||
// Driver is the interface implemented by storage drivers.
|
||||
type Driver interface {
|
||||
// Open returns a storage implementation. It should only validate its
|
||||
// arguments and not return an error if the underlying storage is
|
||||
// unavailable.
|
||||
Open(config map[string]string) (Storage, error)
|
||||
}
|
||||
|
||||
// Register makes a storage driver available by the provided name. If Register
|
||||
// is called twice with the same name or if driver is nil, it panics.
|
||||
func Register(name string, driver Driver) {
|
||||
if driver == nil {
|
||||
panic("driver cannot be nil")
|
||||
}
|
||||
if _, ok := drivers[name]; ok {
|
||||
panic("driver " + name + " is already registered")
|
||||
}
|
||||
drivers[name] = driver
|
||||
}
|
||||
|
||||
// Open returns a new storage object with a given key rotation strategy.
|
||||
func Open(driverName string, config map[string]string) (Storage, error) {
|
||||
driver, ok := drivers[driverName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no driver of type %s found", driverName)
|
||||
}
|
||||
return driver.Open(config)
|
||||
}
|
||||
|
||||
// Storage is the storage interface used by the server. Implementations, at minimum
|
||||
// require compare-and-swap atomic actions.
|
||||
//
|
||||
// Implementations are expected to perform their own garbage collection of
|
||||
// expired objects (expect keys which are handled by rotation).
|
||||
type Storage interface {
|
||||
Close() error
|
||||
|
||||
CreateAuthRequest(a AuthRequest) error
|
||||
CreateClient(c Client) error
|
||||
CreateAuthCode(c AuthCode) error
|
||||
CreateRefresh(r Refresh) error
|
||||
|
||||
// TODO(ericchiang): return (T, bool, error) so we can indicate not found
|
||||
// requests that way.
|
||||
GetAuthRequest(id string) (AuthRequest, error)
|
||||
GetAuthCode(id string) (AuthCode, error)
|
||||
GetClient(id string) (Client, error)
|
||||
GetKeys() (Keys, error)
|
||||
GetRefresh(id string) (Refresh, error)
|
||||
|
||||
ListClients() ([]Client, error)
|
||||
ListRefreshTokens() ([]Refresh, error)
|
||||
|
||||
// Delete methods MUST be atomic.
|
||||
DeleteAuthRequest(id string) error
|
||||
DeleteAuthCode(code string) error
|
||||
DeleteClient(id string) error
|
||||
DeleteRefresh(id string) error
|
||||
|
||||
// Update functions are assumed to be a performed within a single object transaction.
|
||||
UpdateClient(id string, updater func(old Client) (Client, error)) error
|
||||
UpdateKeys(updater func(old Keys) (Keys, error)) error
|
||||
UpdateAuthRequest(id string, updater func(a AuthRequest) (AuthRequest, error)) error
|
||||
}
|
||||
|
||||
// Client is an OAuth2 client.
|
||||
//
|
||||
// For further reading see:
|
||||
// * Trusted peers: https://developers.google.com/identity/protocols/CrossClientAuth
|
||||
// * Public clients: https://developers.google.com/api-client-library/python/auth/installed-app
|
||||
type Client struct {
|
||||
ID string
|
||||
Secret string
|
||||
RedirectURIs []string
|
||||
|
||||
// TrustedPeers are a list of peers which can issue tokens on this client's behalf.
|
||||
// Clients inherently trust themselves.
|
||||
TrustedPeers []string
|
||||
|
||||
// Public clients must use either use a redirectURL 127.0.0.1:X or "urn:ietf:wg:oauth:2.0:oob"
|
||||
Public bool
|
||||
|
||||
Name string
|
||||
LogoURL string
|
||||
}
|
||||
|
||||
// Identity represents the ID Token claims supported by the server.
|
||||
type Identity struct {
|
||||
UserID string
|
||||
Username string
|
||||
Email string
|
||||
EmailVerified bool
|
||||
|
||||
Groups []string
|
||||
|
||||
// ConnectorData holds data used by the connector for subsequent requests after initial
|
||||
// authentication, such as access tokens for upstream provides.
|
||||
//
|
||||
// This data is never shared with end users, OAuth clients, or through the API.
|
||||
ConnectorData []byte
|
||||
}
|
||||
|
||||
// AuthRequest represents a OAuth2 client authorization request. It holds the state
|
||||
// of a single auth flow up to the point that the user authorizes the client.
|
||||
type AuthRequest struct {
|
||||
ID string
|
||||
ClientID string
|
||||
|
||||
ResponseTypes []string
|
||||
Scopes []string
|
||||
RedirectURI string
|
||||
|
||||
Nonce string
|
||||
State string
|
||||
|
||||
// The client has indicated that the end user must be shown an approval prompt
|
||||
// on all requests. The server cannot cache their initial action for subsequent
|
||||
// attempts.
|
||||
ForceApprovalPrompt bool
|
||||
|
||||
// The identity of the end user. Generally nil until the user authenticates
|
||||
// with a backend.
|
||||
Identity *Identity
|
||||
// The connector used to login the user. Set when the user authenticates.
|
||||
ConnectorID string
|
||||
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
// AuthCode represents a code which can be exchanged for an OAuth2 token response.
|
||||
type AuthCode struct {
|
||||
ID string
|
||||
|
||||
ClientID string
|
||||
RedirectURI string
|
||||
ConnectorID string
|
||||
|
||||
Nonce string
|
||||
|
||||
Scopes []string
|
||||
|
||||
Identity Identity
|
||||
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
// Refresh is an OAuth2 refresh token.
|
||||
type Refresh struct {
|
||||
// The actual refresh token.
|
||||
RefreshToken string
|
||||
|
||||
// Client this refresh token is valid for.
|
||||
ClientID string
|
||||
ConnectorID string
|
||||
|
||||
// Scopes present in the initial request. Refresh requests may specify a set
|
||||
// of scopes different from the initial request when refreshing a token,
|
||||
// however those scopes must be encompassed by this set.
|
||||
Scopes []string
|
||||
|
||||
Nonce string
|
||||
|
||||
Identity Identity
|
||||
}
|
||||
|
||||
// VerificationKey is a rotated signing key which can still be used to verify
|
||||
// signatures.
|
||||
type VerificationKey struct {
|
||||
PublicKey *jose.JSONWebKey `json:"publicKey"`
|
||||
Expiry time.Time `json:"expiry"`
|
||||
}
|
||||
|
||||
// Keys hold encryption and signing keys.
|
||||
type Keys struct {
|
||||
// Key for creating and verifying signatures. These may be nil.
|
||||
SigningKey *jose.JSONWebKey
|
||||
SigningKeyPub *jose.JSONWebKey
|
||||
// Old signing keys which have been rotated but can still be used to validate
|
||||
// existing signatures.
|
||||
VerificationKeys []VerificationKey
|
||||
|
||||
// The next time the signing key will rotate.
|
||||
//
|
||||
// For caching purposes, implementations MUST NOT update keys before this time.
|
||||
NextRotation time.Time
|
||||
}
|
||||
|
||||
// Sign creates a JWT using the signing key.
|
||||
func (k Keys) Sign(payload []byte) (jws string, err error) {
|
||||
if k.SigningKey == nil {
|
||||
return "", fmt.Errorf("no key to sign payload with")
|
||||
}
|
||||
signingKey := jose.SigningKey{Key: k.SigningKey}
|
||||
|
||||
switch key := k.SigningKey.Key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
// TODO(ericchiang): Allow different cryptographic hashes.
|
||||
signingKey.Algorithm = jose.RS256
|
||||
case *ecdsa.PrivateKey:
|
||||
switch key.Params() {
|
||||
case elliptic.P256().Params():
|
||||
signingKey.Algorithm = jose.ES256
|
||||
case elliptic.P384().Params():
|
||||
signingKey.Algorithm = jose.ES384
|
||||
case elliptic.P521().Params():
|
||||
signingKey.Algorithm = jose.ES512
|
||||
default:
|
||||
return "", errors.New("unsupported ecdsa curve")
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("new signier: %v", err)
|
||||
}
|
||||
signature, err := signer.Sign(payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("signing payload: %v", err)
|
||||
}
|
||||
return signature.CompactSerialize()
|
||||
}
|
Reference in New Issue
Block a user