authserver.go 13.9 KB
Newer Older
ale's avatar
ale committed
1 2 3 4
package server

import (
	"context"
5
	"errors"
ale's avatar
ale committed
6 7
	"fmt"
	"log"
ale's avatar
ale committed
8
	"path/filepath"
ale's avatar
ale committed
9 10

	"github.com/pquerna/otp/totp"
ale's avatar
ale committed
11
	"github.com/prometheus/client_golang/prometheus"
ale's avatar
ale committed
12 13
	"github.com/tstranex/u2f"

ale's avatar
ale committed
14
	"git.autistici.org/ai3/go-common/pwhash"
ale's avatar
ale committed
15
	"git.autistici.org/id/auth"
ale's avatar
ale committed
16
	"git.autistici.org/id/auth/backend"
ale's avatar
ale committed
17

ale's avatar
ale committed
18 19 20 21
	fileBE "git.autistici.org/id/auth/backend/file"
	ldapBE "git.autistici.org/id/auth/backend/ldap"
	sqlBE "git.autistici.org/id/auth/backend/sql"
)
22

23 24 25 26 27 28 29
// OTPShortTermStorage stores short-term otp tokens for replay
// protection purposes.
type OTPShortTermStorage interface {
	AddToken(string, string) error
	HasToken(string, string) bool
}

ale's avatar
ale committed
30 31 32 33 34 35
// U2FShortTermStorage stores short-term u2f challenges.
type U2FShortTermStorage interface {
	SetUserChallenge(string, *u2f.Challenge) error
	GetUserChallenge(string) (*u2f.Challenge, bool)
}

ale's avatar
ale committed
36
// A filter that can be applied to a request, potentially changing it.
37
type requestFilter interface {
ale's avatar
ale committed
38
	Filter(*backend.User, *auth.Request, *auth.Response) *auth.Response
39
}
40

ale's avatar
ale committed
41 42 43 44 45
// Authenticate users for a specific service.
type service struct {
	rl                []*authRatelimiter
	bl                []*authBlacklist
	filters           []requestFilter
ale's avatar
ale committed
46
	backends          []backend.ServiceBackend
ale's avatar
ale committed
47 48
	challengeResponse bool
	enforce2FA        bool
49 50
}

ale's avatar
ale committed
51
func (s *service) checkRateLimits(user *backend.User, req *auth.Request) bool {
ale's avatar
ale committed
52
	for _, rl := range s.rl {
53 54 55 56
		if !rl.AllowIncr(user, req) {
			return false
		}
	}
ale's avatar
ale committed
57
	for _, bl := range s.bl {
58 59 60 61 62 63 64
		if !bl.Allow(user, req) {
			return false
		}
	}
	return true
}

ale's avatar
ale committed
65
func (s *service) notifyBlacklists(user *backend.User, req *auth.Request, resp *auth.Response) {
ale's avatar
ale committed
66
	for _, bl := range s.bl {
67 68
		bl.Incr(user, req, resp)
	}
ale's avatar
ale committed
69 70
}

ale's avatar
ale committed
71 72
// A serviceBackend wrapper that sets additional group memberships.
type staticGroupsServiceBackend struct {
ale's avatar
ale committed
73
	backend.ServiceBackend
ale's avatar
ale committed
74 75
	groups []string
}
ale's avatar
ale committed
76

ale's avatar
ale committed
77
func withStaticGroups(wrap backend.ServiceBackend, groups []string) backend.ServiceBackend {
ale's avatar
ale committed
78
	return &staticGroupsServiceBackend{
ale's avatar
ale committed
79
		ServiceBackend: wrap,
ale's avatar
ale committed
80 81 82
		groups:         groups,
	}
}
ale's avatar
ale committed
83

ale's avatar
ale committed
84 85
func (b *staticGroupsServiceBackend) GetUser(ctx context.Context, name string) (*backend.User, bool) {
	user, ok := b.ServiceBackend.GetUser(ctx, name)
ale's avatar
ale committed
86 87 88 89 90 91 92
	if ok {
		user.Groups = append(user.Groups, b.groups...)
	}
	return user, ok
}

// Create backend implementations from the given Config.
ale's avatar
ale committed
93 94 95 96 97
func createBackends(config *Config) (map[string]backend.UserBackend, error) {
	// Get the config base dir to evaluate relative paths in
	// backend configuration.
	configDir := filepath.Dir(config.path)
	backends := make(map[string]backend.UserBackend)
ale's avatar
ale committed
98
	for name, params := range config.Backends {
ale's avatar
ale committed
99
		var b backend.UserBackend
ale's avatar
ale committed
100 101 102
		var err error
		switch name {
		case "file":
ale's avatar
ale committed
103
			b, err = fileBE.New(params, configDir)
ale's avatar
ale committed
104
		case "ldap":
ale's avatar
ale committed
105
			b, err = ldapBE.New(params, configDir)
ale's avatar
ale committed
106
		case "sql":
ale's avatar
ale committed
107
			b, err = sqlBE.New(params, configDir)
ale's avatar
ale committed
108
		default:
ale's avatar
ale committed
109
			err = fmt.Errorf("unknown backend type %s", name)
ale's avatar
ale committed
110 111 112
		}
		if err != nil {
			return nil, err
ale's avatar
ale committed
113
		}
ale's avatar
ale committed
114
		backends[name] = b
ale's avatar
ale committed
115
	}
ale's avatar
ale committed
116
	return backends, nil
ale's avatar
ale committed
117 118
}

ale's avatar
ale committed
119
// Create a service backend from the given BackendSpec.
ale's avatar
ale committed
120
func createServiceBackend(spec *backend.Spec, backends map[string]backend.UserBackend) (backend.ServiceBackend, error) {
ale's avatar
ale committed
121 122 123 124 125 126 127 128 129 130
	b, ok := backends[spec.BackendName]
	if !ok {
		return nil, fmt.Errorf("unknown backend %s", spec.BackendName)
	}
	bb, err := b.NewServiceBackend(spec)
	if err != nil {
		return nil, err
	}
	if len(spec.StaticGroups) > 0 {
		bb = withStaticGroups(bb, spec.StaticGroups)
ale's avatar
ale committed
131
	}
ale's avatar
ale committed
132
	return bb, nil
ale's avatar
ale committed
133 134
}

ale's avatar
ale committed
135
// Create a service from the given ServiceConfig.
ale's avatar
ale committed
136
func createService(config *Config, sc *ServiceConfig, backends map[string]backend.UserBackend, ratelimiters map[string]*authRatelimiter, blacklists map[string]*authBlacklist) (*service, error) {
ale's avatar
ale committed
137 138 139 140 141 142 143 144 145 146
	s := &service{
		enforce2FA:        sc.Enforce2FA,
		challengeResponse: sc.ChallengeResponse,
	}

	// Parse all BackendSpecs.
	for _, spec := range sc.BackendSpecs {
		b, err := createServiceBackend(spec, backends)
		if err != nil {
			return nil, err
147
		}
ale's avatar
ale committed
148 149 150 151 152 153 154 155 156
		s.backends = append(s.backends, b)
	}

	// Link to the configured ratelimit objects.
	for _, name := range sc.Ratelimits {
		if rl, ok := ratelimiters[name]; ok {
			s.rl = append(s.rl, rl)
		} else if bl, ok := blacklists[name]; ok {
			s.bl = append(s.bl, bl)
157
		} else {
ale's avatar
ale committed
158
			return nil, fmt.Errorf("unknown rate limiter '%s'", name)
159 160 161
		}
	}

shammash's avatar
shammash committed
162 163 164 165 166 167 168 169 170 171 172
	if sc.EnableLastLoginReporting {
		if config.UserMetaDBConfig == nil {
			return nil, errors.New("usermetadb config is missing")
		}
		llr, err := newLastLoginFilter(config.UserMetaDBConfig)
		if err != nil {
			return nil, err
		}
		s.filters = append(s.filters, llr)
	}

ale's avatar
ale committed
173 174 175 176 177
	// Enabling device tracking also enables user activity
	// logging.
	if sc.EnableDeviceTracking {
		if config.UserMetaDBConfig == nil {
			return nil, errors.New("usermetadb config is missing")
178
		}
ale's avatar
ale committed
179 180 181 182 183
		dt, err := newDeviceFilter(config.UserMetaDBConfig)
		if err != nil {
			return nil, err
		}
		s.filters = append(s.filters, dt)
184

ale's avatar
ale committed
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
		// The logger conveniently comes last.
		lf, err := newUserActivityLogFilter(config.UserMetaDBConfig)
		if err != nil {
			return nil, err
		}
		s.filters = append(s.filters, lf)
	}

	return s, nil
}

// Create the global rate limiters and blacklists.
func createRatelimiters(config *Config) (map[string]*authRatelimiter, map[string]*authBlacklist, error) {
	ratelimiters := make(map[string]*authRatelimiter)
	blacklists := make(map[string]*authBlacklist)
	for name, params := range config.RateLimiters {
		if params.BlacklistTime > 0 {
			bl, err := newAuthBlacklist(params)
203
			if err != nil {
ale's avatar
ale committed
204
				return nil, nil, err
205
			}
ale's avatar
ale committed
206 207 208
			blacklists[name] = bl
		} else {
			rl, err := newAuthRatelimiter(params)
209
			if err != nil {
ale's avatar
ale committed
210
				return nil, nil, err
211
			}
ale's avatar
ale committed
212
			ratelimiters[name] = rl
213
		}
214
	}
ale's avatar
ale committed
215 216
	return ratelimiters, blacklists, nil
}
217

ale's avatar
ale committed
218 219
// Server is the main authentication server object.
type Server struct {
ale's avatar
ale committed
220
	services     map[string]*service
ale's avatar
ale committed
221
	backends     map[string]backend.UserBackend
ale's avatar
ale committed
222
	u2fShortTerm U2FShortTermStorage
223
	otpShortTerm OTPShortTermStorage
ale's avatar
ale committed
224 225 226
}

// NewServer creates a Server using the given configuration.
227
func NewServer(config *Config) (*Server, error) {
ale's avatar
ale committed
228 229 230 231 232
	// Build backends, rate limiters and blacklists. These are global
	// objects which are then shared by the various services.
	backends, err := createBackends(config)
	if err != nil {
		return nil, err
ale's avatar
ale committed
233
	}
234

ale's avatar
ale committed
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
	ratelimiters, blacklists, err := createRatelimiters(config)
	if err != nil {
		return nil, err
	}

	// Build the service map, connecting services to backends.
	services := make(map[string]*service)
	for name, sc := range config.Services {
		svc, err := createService(config, sc, backends, ratelimiters, blacklists)
		if err != nil {
			return nil, err
		}
		services[name] = svc
	}

	// Connect to memcache, if configured to do so, otherwise fall back to
	// a simple in-memory cache.
252
	var cache cacheClient
ale's avatar
ale committed
253
	if len(config.MemcacheServers) > 0 {
254 255 256 257 258
		var err error
		cache, err = newMemcacheReplicatedClient(config.MemcacheServers)
		if err != nil {
			return nil, err
		}
259
	} else {
260
		cache = newInprocessCache()
261
	}
262

ale's avatar
ale committed
263 264 265 266 267 268
	return &Server{
		services:     services,
		backends:     backends,
		u2fShortTerm: newU2FStorage(cache),
		otpShortTerm: newOTPStorage(cache),
	}, nil
ale's avatar
ale committed
269 270 271 272 273 274 275 276 277
}

// Close the authentication server and release all associated resources.
func (s *Server) Close() {
	for _, b := range s.backends {
		b.Close()
	}
}

ale's avatar
ale committed
278 279
func (s *Server) getService(name string) (*service, bool) {
	svc, ok := s.services[name]
ale's avatar
ale committed
280
	if !ok {
ale's avatar
ale committed
281
		svc, ok = s.services["default"]
ale's avatar
ale committed
282
	}
ale's avatar
ale committed
283
	return svc, ok
ale's avatar
ale committed
284 285
}

ale's avatar
ale committed
286
func (s *Server) getUser(ctx context.Context, service *service, username string) (*backend.User, bool) {
ale's avatar
ale committed
287 288 289
	for _, b := range service.backends {
		if user, ok := b.GetUser(ctx, username); ok {
			return user, true
290 291 292 293 294
		}
	}
	return nil, false
}

ale's avatar
ale committed
295 296
// Authenticate a user with the parameters specified in the incoming AuthRequest.
func (s *Server) Authenticate(ctx context.Context, req *auth.Request) *auth.Response {
ale's avatar
ale committed
297
	svc, ok := s.getService(req.Service)
ale's avatar
ale committed
298 299 300 301 302
	if !ok {
		log.Printf("unknown service %s", req.Service)
		return newError()
	}

ale's avatar
ale committed
303
	user, ok := s.getUser(ctx, svc, req.Username)
ale's avatar
ale committed
304
	if !ok {
ale's avatar
ale committed
305 306
		// User is unknown to all backends. Do not proceed
		// further, but log and increment stats counters.
ale's avatar
ale committed
307
		log.Printf("unknown user %s", req.Username)
ale's avatar
ale committed
308 309 310 311
		authRequestsCounter.With(prometheus.Labels{
			"service": req.Service,
			"status":  "unknown_user",
		})
ale's avatar
ale committed
312 313 314
		return newError()
	}

315 316
	// Apply rate limiting and blacklisting _before_ invoking the
	// authentication handlers, as they may be CPU intensive.
ale's avatar
ale committed
317
	if allowed := svc.checkRateLimits(user, req); !allowed {
ale's avatar
ale committed
318 319 320
		ratelimitCounter.With(prometheus.Labels{
			"service": req.Service,
		}).Inc()
321 322 323
		return newError()
	}

ale's avatar
ale committed
324
	resp, err := s.authenticateUser(req, svc, user)
ale's avatar
ale committed
325 326
	if err != nil {
		resp = newError()
ale's avatar
ale committed
327
		log.Printf("auth: user=%s service=%s status=%s error=%s", req.Username, req.Service, resp.Status.String(), err)
ale's avatar
ale committed
328 329
	} else {
		// Log the request and response.
ale's avatar
ale committed
330
		log.Printf("auth: user=%s service=%s status=%s", req.Username, req.Service, resp.Status.String())
ale's avatar
ale committed
331
	}
332 333

	// Notify blacklists of the result.
ale's avatar
ale committed
334
	svc.notifyBlacklists(user, req, resp)
335

ale's avatar
ale committed
336 337 338 339 340 341
	// Increment stats counters.
	authRequestsCounter.With(prometheus.Labels{
		"service": req.Service,
		"status":  resp.Status.String(),
	}).Inc()

342 343 344
	return resp
}

ale's avatar
ale committed
345 346
// Authenticate a user. Returning an error should result in an
// AuthResponse with StatusError.
ale's avatar
ale committed
347
func (s *Server) authenticateUser(req *auth.Request, svc *service, user *backend.User) (resp *auth.Response, err error) {
ale's avatar
ale committed
348 349 350
	// Verify different credentials depending on whether the user
	// has 2FA enabled or not, and on whether the service itself
	// supports challenge-response authentication.
ale's avatar
ale committed
351 352
	if svc.enforce2FA || user.Has2FA() {
		if svc.challengeResponse {
ale's avatar
ale committed
353
			resp, err = s.authenticateUserWith2FA(user, req)
ale's avatar
ale committed
354
		} else {
ale's avatar
ale committed
355
			resp, err = s.authenticateUserWithASP(user, req)
ale's avatar
ale committed
356 357
		}
	} else {
ale's avatar
ale committed
358 359 360 361
		resp, err = s.authenticateUserWithPassword(user, req)
	}
	if err != nil {
		return
362 363 364 365
	}

	// Process the response through filters (device info checks,
	// etc) that may or may not change the response itself.
366 367 368 369 370 371
	//
	// TODO: it's unclear why we'd want to bail on errors, it
	// makes it quite confusing for StatusInsufficientCredentials.
	//
	// Perhaps it would be easier to just run filters only on
	// StatusOK?
ale's avatar
ale committed
372
	for _, f := range svc.filters {
373 374 375
		if resp.Status == auth.StatusError {
			break
		}
376
		resp = f.Filter(user, req, resp)
ale's avatar
ale committed
377 378 379 380 381 382
	}

	// If the response is successful, augment it with user information.
	if resp.Status == auth.StatusOK {
		resp.UserInfo = user.UserInfo()
	}
ale's avatar
ale committed
383
	return
ale's avatar
ale committed
384 385
}

ale's avatar
ale committed
386
func (s *Server) authenticateUserWithPassword(user *backend.User, req *auth.Request) (*auth.Response, error) {
ale's avatar
ale committed
387 388
	// Ok we only need to check the password here.
	if checkPassword(req.Password, user.EncryptedPassword) {
ale's avatar
ale committed
389
		return newOK(), nil
ale's avatar
ale committed
390
	}
ale's avatar
ale committed
391
	return nil, errors.New("wrong password")
ale's avatar
ale committed
392 393
}

ale's avatar
ale committed
394
func (s *Server) authenticateUserWithASP(user *backend.User, req *auth.Request) (*auth.Response, error) {
ale's avatar
ale committed
395 396
	for _, asp := range user.AppSpecificPasswords {
		if asp.Service == req.Service && checkPassword(req.Password, asp.EncryptedPassword) {
ale's avatar
ale committed
397
			return newOK(), nil
ale's avatar
ale committed
398 399
		}
	}
ale's avatar
ale committed
400
	return nil, errors.New("wrong app-specific password")
ale's avatar
ale committed
401 402
}

ale's avatar
ale committed
403
func (s *Server) authenticateUserWith2FA(user *backend.User, req *auth.Request) (*auth.Response, error) {
ale's avatar
ale committed
404 405
	// First of all verify the password.
	if !checkPassword(req.Password, user.EncryptedPassword) {
ale's avatar
ale committed
406
		return nil, errors.New("wrong password")
ale's avatar
ale committed
407 408 409 410 411 412 413 414 415
	}

	// If the request contains one of the 2FA attributes, verify
	// it. But if it contains none, we return with
	// AuthStatusInsufficientCredentials and potentially a 2FA
	// hint (for U2F).
	switch {
	case req.U2FResponse != nil:
		if user.HasU2F() && s.checkU2F(user, req.U2FResponse) {
ale's avatar
ale committed
416
			return newOK(), nil
ale's avatar
ale committed
417
		}
ale's avatar
ale committed
418
		return nil, errors.New("bad U2F response")
ale's avatar
ale committed
419
	case req.OTP != "":
420 421 422 423 424
		if user.HasOTP() && s.checkOTP(user, req.OTP, user.TOTPSecret) {
			// Save the token for replay protection.
			if err := s.otpShortTerm.AddToken(user.Name, req.OTP); err != nil {
				log.Printf("error saving OTP token to short-term storage: %v", err)
			}
ale's avatar
ale committed
425
			return newOK(), nil
ale's avatar
ale committed
426
		}
ale's avatar
ale committed
427
		return nil, errors.New("bad OTP")
ale's avatar
ale committed
428 429 430 431 432 433 434 435
	default:
		resp := &auth.Response{
			Status: auth.StatusInsufficientCredentials,
		}
		if req.U2FAppID != "" && user.HasU2F() {
			resp.TFAMethod = auth.TFAMethodU2F
			signReq, err := s.u2fSignRequest(user, req.U2FAppID)
			if err != nil {
ale's avatar
ale committed
436
				return nil, err
ale's avatar
ale committed
437 438 439 440 441
			}
			resp.U2FSignRequest = signReq
		} else if user.HasOTP() {
			resp.TFAMethod = auth.TFAMethodOTP
		}
ale's avatar
ale committed
442
		return resp, nil
ale's avatar
ale committed
443 444 445
	}
}

ale's avatar
ale committed
446
func (s *Server) u2fSignRequest(user *backend.User, appID string) (*u2f.WebSignRequest, error) {
ale's avatar
ale committed
447 448 449 450 451 452 453 454 455 456 457 458
	challenge, err := u2f.NewChallenge(appID, []string{appID})
	if err != nil {
		return nil, err
	}

	if err := s.u2fShortTerm.SetUserChallenge(user.Name, challenge); err != nil {
		return nil, err
	}

	return challenge.SignRequest(user.U2FRegistrations), nil
}

ale's avatar
ale committed
459
func (s *Server) checkU2F(user *backend.User, resp *u2f.SignResponse) bool {
ale's avatar
ale committed
460 461 462 463 464 465 466 467 468 469 470 471 472 473
	challenge, ok := s.u2fShortTerm.GetUserChallenge(user.Name)
	if !ok {
		return false
	}
	for _, reg := range user.U2FRegistrations {
		_, err := reg.Authenticate(*resp, *challenge, 0)
		if err == nil {
			return true
		}
	}
	return false
}

func checkPassword(password, hash []byte) bool {
ale's avatar
ale committed
474
	return pwhash.ComparePassword(string(hash), string(password))
ale's avatar
ale committed
475 476
}

ale's avatar
ale committed
477
func (s *Server) checkOTP(user *backend.User, otp, secret string) bool {
478 479 480 481 482 483
	// Check our short-ttl blacklist for the token (replay protection).
	if s.otpShortTerm.HasToken(user.Name, otp) {
		log.Printf("replay protection triggered for %s", user.Name)
		return false
	}

ale's avatar
ale committed
484 485
	return totp.Validate(otp, secret)
}
ale's avatar
ale committed
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516

func newError() *auth.Response {
	return &auth.Response{Status: auth.StatusError}
}

func newOK() *auth.Response {
	return &auth.Response{Status: auth.StatusOK}
}

// Instrumentation.
var (
	authRequestsCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "auth_requests",
			Help: "Number of authentication requests.",
		},
		[]string{"service", "status"},
	)
	ratelimitCounter = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "auth_requests_ratelimited",
			Help: "Number of rate-limited authentication requests.",
		},
		[]string{"service"},
	)
)

func init() {
	prometheus.MustRegister(authRequestsCounter)
	prometheus.MustRegister(ratelimitCounter)
}