mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
264 lines
7.3 KiB
264 lines
7.3 KiB
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 |
|
|
|
// 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() |
|
}
|
|
|