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 the server). type Storage interface { Close() error // TODO(ericchiang): Let the storages set the IDs of these objects. 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 instead of using ErrNotFound. 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() }