forked from mystiq/dex
234 lines
6.5 KiB
Go
234 lines
6.5 KiB
Go
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()
|
|
}
|