This repository has been archived on 2023-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
dex/server/server.go

261 lines
6.0 KiB
Go
Raw Normal View History

2016-07-25 20:00:28 +00:00
package server
import (
"errors"
"fmt"
"log"
2016-07-25 20:00:28 +00:00
"net/http"
"net/url"
"path"
2016-08-11 03:51:58 +00:00
"sync/atomic"
2016-07-25 20:00:28 +00:00
"time"
"golang.org/x/crypto/bcrypt"
2016-07-25 20:00:28 +00:00
"github.com/gorilla/mux"
2016-08-11 05:31:42 +00:00
"github.com/coreos/dex/connector"
"github.com/coreos/dex/storage"
2016-07-25 20:00:28 +00:00
)
// 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 00:24:49 +00:00
//
// Multiple servers using the same storage are expected to be configured identically.
2016-07-25 20:00:28 +00:00
type Config struct {
Issuer string
// The backing persistence layer.
Storage storage.Storage
// Strategies for federated identity.
Connectors []Connector
2016-08-20 00:24:49 +00:00
// 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-07-25 20:00:28 +00:00
RotateKeysAfter time.Duration // Defaults to 6 hours.
IDTokensValidFor time.Duration // Defaults to 24 hours
// If specified, the server will use this function for determining time.
Now func() time.Time
2016-08-25 20:10:19 +00:00
EnablePasswordDB bool
2016-08-25 20:10:19 +00:00
TemplateConfig TemplateConfig
2016-07-25 20:00:28 +00:00
}
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-25 20:10:19 +00:00
templates *templates
2016-07-25 20:00:28 +00:00
// If enabled, don't prompt user for approval after logging in through connector.
// No package level API to set this, only used in tests.
skipApproval bool
2016-08-20 00:24:49 +00:00
supportedResponseTypes map[string]bool
2016-07-25 20:00:28 +00:00
now func() time.Time
idTokensValidFor time.Duration
}
// NewServer constructs a server from the provided config.
func NewServer(c Config) (*Server, error) {
2016-07-25 20:00:28 +00:00
return newServer(c, defaultRotationStrategy(
value(c.RotateKeysAfter, 6*time.Hour),
value(c.IDTokensValidFor, 24*time.Hour),
))
}
func newServer(c Config, rotationStrategy rotationStrategy) (*Server, error) {
issuerURL, err := url.Parse(c.Issuer)
if err != nil {
return nil, fmt.Errorf("server: can't parse issuer URL")
}
if c.EnablePasswordDB {
c.Connectors = append(c.Connectors, Connector{
ID: "local",
DisplayName: "Email",
Connector: newPasswordDB(c.Storage),
})
}
2016-07-25 20:00:28 +00:00
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 00:24:49 +00:00
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-25 20:00:28 +00:00
2016-08-25 20:10:19 +00:00
tmpls, err := loadTemplates(c.TemplateConfig)
if err != nil {
return nil, fmt.Errorf("server: failed to load templates: %v", err)
}
2016-07-25 20:00:28 +00:00
now := c.Now
if now == nil {
now = time.Now
}
s := &Server{
2016-08-11 03:51:58 +00:00
issuerURL: *issuerURL,
connectors: make(map[string]Connector),
storage: newKeyCacher(
storageWithKeyRotation(
c.Storage, rotationStrategy, now,
),
now,
),
2016-08-20 00:24:49 +00:00
supportedResponseTypes: supported,
idTokensValidFor: value(c.IDTokensValidFor, 24*time.Hour),
now: now,
2016-08-25 20:10:19 +00:00
templates: tmpls,
2016-07-25 20:00:28 +00:00
}
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)
discoveryHandler, err := s.discoveryHandler()
if err != nil {
return nil, err
}
handleFunc("/.well-known/openid-configuration", discoveryHandler)
2016-07-25 20:00:28 +00:00
// 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-04 23:45:52 +00:00
handleFunc("/healthz", s.handleHealth)
2016-07-25 20:00:28 +00:00
s.mux = r
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 03:51:58 +00:00
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 03:51:58 +00:00
// 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
}