2016-07-26 01:30:28 +05:30
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
2016-10-06 05:08:20 +05:30
|
|
|
"log"
|
2016-07-26 01:30:28 +05:30
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
2016-08-11 09:21:58 +05:30
|
|
|
"sync/atomic"
|
2016-07-26 01:30:28 +05:30
|
|
|
"time"
|
|
|
|
|
2016-10-06 05:08:20 +05:30
|
|
|
"golang.org/x/crypto/bcrypt"
|
2016-10-13 07:21:32 +05:30
|
|
|
"golang.org/x/net/context"
|
2016-10-06 05:08:20 +05:30
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
2016-08-11 11:01:42 +05:30
|
|
|
"github.com/coreos/dex/connector"
|
|
|
|
"github.com/coreos/dex/storage"
|
2016-07-26 01:30:28 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
// Connector is a connector with metadata.
|
|
|
|
type Connector struct {
|
|
|
|
ID string
|
|
|
|
DisplayName string
|
|
|
|
Connector connector.Connector
|
|
|
|
}
|
|
|
|
|
|
|
|
// Config holds the server's configuration options.
|
2016-08-20 05:54:49 +05:30
|
|
|
//
|
|
|
|
// Multiple servers using the same storage are expected to be configured identically.
|
2016-07-26 01:30:28 +05:30
|
|
|
type Config struct {
|
|
|
|
Issuer string
|
|
|
|
|
|
|
|
// The backing persistence layer.
|
|
|
|
Storage storage.Storage
|
|
|
|
|
|
|
|
// Strategies for federated identity.
|
|
|
|
Connectors []Connector
|
|
|
|
|
2016-08-20 05:54:49 +05:30
|
|
|
// Valid values are "code" to enable the code flow and "token" to enable the implicit
|
|
|
|
// flow. If no response types are supplied this value defaults to "code".
|
|
|
|
SupportedResponseTypes []string
|
|
|
|
|
2016-10-08 00:23:01 +05:30
|
|
|
// If enabled, the server won't prompt the user to approve authorization requests.
|
|
|
|
// Logging in implies approval.
|
|
|
|
SkipApprovalScreen bool
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
RotateKeysAfter time.Duration // Defaults to 6 hours.
|
|
|
|
IDTokensValidFor time.Duration // Defaults to 24 hours
|
|
|
|
|
2016-10-13 07:21:32 +05:30
|
|
|
GCFrequency time.Duration // Defaults to 5 minutes
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
// If specified, the server will use this function for determining time.
|
|
|
|
Now func() time.Time
|
2016-08-26 01:40:19 +05:30
|
|
|
|
2016-10-06 05:08:20 +05:30
|
|
|
EnablePasswordDB bool
|
|
|
|
|
2016-08-26 01:40:19 +05:30
|
|
|
TemplateConfig TemplateConfig
|
2016-07-26 01:30:28 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
func value(val, defaultValue time.Duration) time.Duration {
|
|
|
|
if val == 0 {
|
|
|
|
return defaultValue
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
|
|
|
// Server is the top level object.
|
|
|
|
type Server struct {
|
|
|
|
issuerURL url.URL
|
|
|
|
|
|
|
|
// Read-only map of connector IDs to connectors.
|
|
|
|
connectors map[string]Connector
|
|
|
|
|
|
|
|
storage storage.Storage
|
|
|
|
|
|
|
|
mux http.Handler
|
|
|
|
|
2016-08-26 01:40:19 +05:30
|
|
|
templates *templates
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
// If enabled, don't prompt user for approval after logging in through connector.
|
|
|
|
skipApproval bool
|
|
|
|
|
2016-08-20 05:54:49 +05:30
|
|
|
supportedResponseTypes map[string]bool
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
now func() time.Time
|
|
|
|
|
|
|
|
idTokensValidFor time.Duration
|
|
|
|
}
|
|
|
|
|
2016-10-04 12:56:04 +05:30
|
|
|
// NewServer constructs a server from the provided config.
|
2016-10-13 07:21:32 +05:30
|
|
|
func NewServer(ctx context.Context, c Config) (*Server, error) {
|
|
|
|
return newServer(ctx, c, defaultRotationStrategy(
|
2016-07-26 01:30:28 +05:30
|
|
|
value(c.RotateKeysAfter, 6*time.Hour),
|
|
|
|
value(c.IDTokensValidFor, 24*time.Hour),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
2016-10-13 07:21:32 +05:30
|
|
|
func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) (*Server, error) {
|
2016-07-26 01:30:28 +05:30
|
|
|
issuerURL, err := url.Parse(c.Issuer)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("server: can't parse issuer URL")
|
|
|
|
}
|
2016-10-06 05:08:20 +05:30
|
|
|
if c.EnablePasswordDB {
|
|
|
|
c.Connectors = append(c.Connectors, Connector{
|
|
|
|
ID: "local",
|
|
|
|
DisplayName: "Email",
|
|
|
|
Connector: newPasswordDB(c.Storage),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
if len(c.Connectors) == 0 {
|
|
|
|
return nil, errors.New("server: no connectors specified")
|
|
|
|
}
|
|
|
|
if c.Storage == nil {
|
|
|
|
return nil, errors.New("server: storage cannot be nil")
|
|
|
|
}
|
2016-08-20 05:54:49 +05:30
|
|
|
if len(c.SupportedResponseTypes) == 0 {
|
|
|
|
c.SupportedResponseTypes = []string{responseTypeCode}
|
|
|
|
}
|
|
|
|
|
|
|
|
supported := make(map[string]bool)
|
|
|
|
for _, respType := range c.SupportedResponseTypes {
|
|
|
|
switch respType {
|
|
|
|
case responseTypeCode, responseTypeToken:
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unsupported response_type %q", respType)
|
|
|
|
}
|
|
|
|
supported[respType] = true
|
|
|
|
}
|
2016-07-26 01:30:28 +05:30
|
|
|
|
2016-08-26 01:40:19 +05:30
|
|
|
tmpls, err := loadTemplates(c.TemplateConfig)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("server: failed to load templates: %v", err)
|
|
|
|
}
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
now := c.Now
|
|
|
|
if now == nil {
|
|
|
|
now = time.Now
|
|
|
|
}
|
|
|
|
|
|
|
|
s := &Server{
|
2016-10-13 07:21:32 +05:30
|
|
|
issuerURL: *issuerURL,
|
|
|
|
connectors: make(map[string]Connector),
|
|
|
|
storage: newKeyCacher(c.Storage, now),
|
2016-08-20 05:54:49 +05:30
|
|
|
supportedResponseTypes: supported,
|
|
|
|
idTokensValidFor: value(c.IDTokensValidFor, 24*time.Hour),
|
2016-10-08 00:23:01 +05:30
|
|
|
skipApproval: c.SkipApprovalScreen,
|
2016-08-20 05:54:49 +05:30
|
|
|
now: now,
|
2016-08-26 01:40:19 +05:30
|
|
|
templates: tmpls,
|
2016-07-26 01:30:28 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
for _, conn := range c.Connectors {
|
|
|
|
s.connectors[conn.ID] = conn
|
|
|
|
}
|
|
|
|
|
|
|
|
r := mux.NewRouter()
|
|
|
|
handleFunc := func(p string, h http.HandlerFunc) {
|
|
|
|
r.HandleFunc(path.Join(issuerURL.Path, p), h)
|
|
|
|
}
|
|
|
|
r.NotFoundHandler = http.HandlerFunc(s.notFound)
|
|
|
|
|
2016-08-26 04:48:09 +05:30
|
|
|
discoveryHandler, err := s.discoveryHandler()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
handleFunc("/.well-known/openid-configuration", discoveryHandler)
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
// TODO(ericchiang): rate limit certain paths based on IP.
|
|
|
|
handleFunc("/token", s.handleToken)
|
|
|
|
handleFunc("/keys", s.handlePublicKeys)
|
|
|
|
handleFunc("/auth", s.handleAuthorization)
|
|
|
|
handleFunc("/auth/{connector}", s.handleConnectorLogin)
|
|
|
|
handleFunc("/callback/{connector}", s.handleConnectorCallback)
|
|
|
|
handleFunc("/approval", s.handleApproval)
|
2016-10-05 05:15:52 +05:30
|
|
|
handleFunc("/healthz", s.handleHealth)
|
2016-07-26 01:30:28 +05:30
|
|
|
s.mux = r
|
|
|
|
|
2016-10-13 07:21:32 +05:30
|
|
|
startKeyRotation(ctx, c.Storage, rotationStrategy, now)
|
|
|
|
startGarbageCollection(ctx, c.Storage, value(c.GCFrequency, 5*time.Minute), now)
|
|
|
|
|
2016-07-26 01:30:28 +05:30
|
|
|
return s, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
s.mux.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) absPath(pathItems ...string) string {
|
|
|
|
paths := make([]string, len(pathItems)+1)
|
|
|
|
paths[0] = s.issuerURL.Path
|
|
|
|
copy(paths[1:], pathItems)
|
|
|
|
return path.Join(paths...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) absURL(pathItems ...string) string {
|
|
|
|
u := s.issuerURL
|
|
|
|
u.Path = s.absPath(pathItems...)
|
|
|
|
return u.String()
|
|
|
|
}
|
2016-08-11 09:21:58 +05:30
|
|
|
|
2016-10-06 05:08:20 +05:30
|
|
|
func newPasswordDB(s storage.Storage) interface {
|
|
|
|
connector.Connector
|
|
|
|
connector.PasswordConnector
|
|
|
|
} {
|
|
|
|
return passwordDB{s}
|
|
|
|
}
|
|
|
|
|
|
|
|
type passwordDB struct {
|
|
|
|
s storage.Storage
|
|
|
|
}
|
|
|
|
|
|
|
|
func (db passwordDB) Close() error { return nil }
|
|
|
|
|
|
|
|
func (db passwordDB) Login(email, password string) (connector.Identity, bool, error) {
|
|
|
|
p, err := db.s.GetPassword(email)
|
|
|
|
if err != nil {
|
|
|
|
if err != storage.ErrNotFound {
|
|
|
|
log.Printf("get password: %v", err)
|
|
|
|
}
|
|
|
|
return connector.Identity{}, false, err
|
|
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword(p.Hash, []byte(password)); err != nil {
|
|
|
|
return connector.Identity{}, false, nil
|
|
|
|
}
|
|
|
|
return connector.Identity{
|
|
|
|
UserID: p.UserID,
|
|
|
|
Username: p.Username,
|
|
|
|
Email: p.Email,
|
|
|
|
EmailVerified: true,
|
|
|
|
}, true, nil
|
|
|
|
}
|
|
|
|
|
2016-08-11 09:21:58 +05:30
|
|
|
// newKeyCacher returns a storage which caches keys so long as the next
|
|
|
|
func newKeyCacher(s storage.Storage, now func() time.Time) storage.Storage {
|
|
|
|
if now == nil {
|
|
|
|
now = time.Now
|
|
|
|
}
|
|
|
|
return &keyCacher{Storage: s, now: now}
|
|
|
|
}
|
|
|
|
|
|
|
|
type keyCacher struct {
|
|
|
|
storage.Storage
|
|
|
|
|
|
|
|
now func() time.Time
|
|
|
|
keys atomic.Value // Always holds nil or type *storage.Keys.
|
|
|
|
}
|
|
|
|
|
|
|
|
func (k *keyCacher) GetKeys() (storage.Keys, error) {
|
|
|
|
keys, ok := k.keys.Load().(*storage.Keys)
|
|
|
|
if ok && keys != nil && k.now().Before(keys.NextRotation) {
|
|
|
|
return *keys, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
storageKeys, err := k.Storage.GetKeys()
|
|
|
|
if err != nil {
|
|
|
|
return storageKeys, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if k.now().Before(storageKeys.NextRotation) {
|
|
|
|
k.keys.Store(&storageKeys)
|
|
|
|
}
|
|
|
|
return storageKeys, nil
|
|
|
|
}
|
2016-10-13 07:21:32 +05:30
|
|
|
|
|
|
|
func startGarbageCollection(ctx context.Context, s storage.Storage, frequency time.Duration, now func() time.Time) {
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
return
|
|
|
|
case <-time.After(frequency):
|
|
|
|
if r, err := s.GarbageCollect(now()); err != nil {
|
|
|
|
log.Printf("garbage collection failed: %v", err)
|
|
|
|
} else {
|
|
|
|
log.Printf("garbage collection run, delete auth requests=%d, auth codes=%d", r.AuthRequests, r.AuthCodes)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return
|
|
|
|
}
|