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 ( // 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") // NewID returns a random string which can be used as an ID for objects. func NewID() 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), "=") } // 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 RefreshToken) 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) (RefreshToken, error) ListClients() ([]Client, error) ListRefreshTokens() ([]RefreshToken, 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 `json:"id" yaml:"id"` Secret string `json:"secret" yaml:"secret"` RedirectURIs []string `json:"redirectURIs" yaml:"redirectURIs"` // TrustedPeers are a list of peers which can issue tokens on this client's behalf. // Clients inherently trust themselves. TrustedPeers []string `json:"trustedPeers" yaml:"trustedPeers"` // Public clients must use either use a redirectURL 127.0.0.1:X or "urn:ietf:wg:oauth:2.0:oob" Public bool `json:"public" yaml:"public"` Name string `json:"name" yaml:"name"` LogoURL string `json:"logoURL" yaml:"logoURL"` } // Claims represents the ID Token claims supported by the server. type Claims struct { UserID string Username string Email string EmailVerified bool Groups []string } // 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. Claims *Claims // The connector used to login the user and any data the connector wishes to persists. // Set when the user authenticates. ConnectorID string ConnectorData []byte 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 ConnectorData []byte Nonce string Scopes []string Claims Claims Expiry time.Time } // RefreshToken is an OAuth2 refresh token. type RefreshToken struct { // The actual refresh token. RefreshToken string // Client this refresh token is valid for. ClientID string ConnectorID string ConnectorData []byte // 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 Claims Claims } // 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() }