Commit f674f505 authored by ale's avatar ale

Move DeviceInfo struct to this project

Removes the dependency loop between id/auth project and this one.
parent 9095e7a8
Pipeline #10577 passed with stages
in 1 minute and 56 seconds
......@@ -4,14 +4,12 @@ import (
"context"
"git.autistici.org/ai3/go-common/clientutil"
"git.autistici.org/id/auth"
"git.autistici.org/id/usermetadb"
)
// Client for the user-meta-server API.
type Client interface {
CheckDevice(context.Context, string, string, *auth.DeviceInfo) (bool, error)
CheckDevice(context.Context, string, string, *usermetadb.DeviceInfo) (bool, error)
AddLog(context.Context, string, *usermetadb.LogEntry) error
GetUserDevices(context.Context, string, string) ([]*usermetadb.MetaDeviceInfo, error)
GetUserLogs(context.Context, string, string, int, int) ([]*usermetadb.LogEntry, error)
......@@ -32,7 +30,7 @@ func New(config *clientutil.BackendConfig) (Client, error) {
return &udbClient{be}, nil
}
func (c *udbClient) CheckDevice(ctx context.Context, shard, username string, dev *auth.DeviceInfo) (bool, error) {
func (c *udbClient) CheckDevice(ctx context.Context, shard, username string, dev *usermetadb.DeviceInfo) (bool, error) {
req := usermetadb.CheckDeviceRequest{
Username: username,
DeviceInfo: dev,
......
......@@ -4,7 +4,6 @@ go 1.14
require (
git.autistici.org/ai3/go-common v0.0.0-20210110180225-a05c683cfe23
git.autistici.org/id/auth v0.0.0-20210110171913-dd493db32815
github.com/google/go-cmp v0.5.4
github.com/gorilla/mux v1.8.0 // indirect
github.com/mattes/migrate v0.0.0-20180508041624-4768a648fbd9
......
......@@ -2,12 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
contrib.go.opencensus.io/exporter/zipkin v0.1.2 h1:YqE293IZrKtqPnpwDPH/lOqTWD/s3Iwabycam74JV3g=
contrib.go.opencensus.io/exporter/zipkin v0.1.2/go.mod h1:mP5xM3rrgOjpn79MM8fZbj3gsxcuytSqtH0dxSWW1RE=
git.autistici.org/ai3/go-common v0.0.0-20210109170950-49f8d26bcc81/go.mod h1:nuLJyKZZaC3DBPN4gA1qdGXcm0U5WCcus1z3pI8RdTE=
git.autistici.org/ai3/go-common v0.0.0-20210110180225-a05c683cfe23 h1:YHSG7Vr8nPRmXa7tW2UR8kfzwerjA5GD+bI84nxq2kA=
git.autistici.org/ai3/go-common v0.0.0-20210110180225-a05c683cfe23/go.mod h1:Iik+i0XmqNPTBjWl3vicFz0kjfFK5HBvyFsfIC4S1Ik=
git.autistici.org/id/auth v0.0.0-20210110171913-dd493db32815 h1:gjBHxd2voc+mqHKQqUxkgVqQWGlysWZKJFIRcINpI40=
git.autistici.org/id/auth v0.0.0-20210110171913-dd493db32815/go.mod h1:Hq4zcqE2hbrXsC9j79kzfnBf2BqlGmuVCRIz+AwX/FY=
git.autistici.org/id/usermetadb v0.0.0-20190209105239-61e5a7b24130/go.mod h1:mChOzl9ekSRcfHoFwe3Uv1mccoaZwurlDsIuKRxM8no=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
......@@ -39,8 +35,6 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/boombuler/barcode v0.0.0-20170618053812-56ef0af91246/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
......@@ -54,9 +48,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7 h1:u9SHYsPQNyt5tgDm3YN7+9dYrpK96E5wFilTFWIDZOM=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.1.0 h1:kq/SbG2BCKLkDKkjQf5OWwKWUKj1lgs3lFI4PxnR5lg=
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
......@@ -190,7 +183,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v0.0.0-20190326042056-d6156e141ac6/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
......@@ -201,7 +193,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v0.0.0-20180926090220-0a88db3545c4/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v0.0.0-20200618132256-aa77c03e2fcb h1:bbMxpirqnxN6EmkaG8nxhaDyqDjjOfA59ha+uYWeaMU=
github.com/mattn/go-sqlite3 v0.0.0-20200618132256-aa77c03e2fcb/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
......@@ -247,7 +238,6 @@ github.com/openzipkin/zipkin-go v0.2.5 h1:UwtQQx2pyPIgWYHRg+epgdx1/HnBQTgN3/oIYE
github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v0.0.0-20180815053127-5633e0862627/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
......@@ -258,7 +248,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.0.0/go.mod h1:Zad1CMQfSQZI5KLpahDiSUX4tMMREnXw98IvL1nhgMk=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
......@@ -467,7 +456,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
......
......@@ -5,13 +5,11 @@ package usermetadb
import (
"errors"
"time"
"git.autistici.org/id/auth"
)
type CheckDeviceRequest struct {
Username string `json:"username"`
DeviceInfo *auth.DeviceInfo `json:"device_info"`
Username string `json:"username"`
DeviceInfo *DeviceInfo `json:"device_info"`
}
type CheckDeviceResponse struct {
......@@ -33,14 +31,54 @@ const (
LoginMethodU2F = "u2f"
)
// DeviceInfo holds information about the client device. We use a
// simple persistent cookie to track the same client device across
// multiple session.
type DeviceInfo struct {
ID string `json:"id"`
RemoteAddr string `json:"remote_addr"`
RemoteZone string `json:"remote_zone"`
UserAgent string `json:"user_agent"`
Browser string `json:"browser"`
OS string `json:"os"`
Mobile bool `json:"mobile"`
}
func (d *DeviceInfo) EncodeToMap(m map[string]string, prefix string) {
m[prefix+"id"] = d.ID
m[prefix+"remote_addr"] = d.RemoteAddr
m[prefix+"remote_zone"] = d.RemoteZone
m[prefix+"browser"] = d.Browser
m[prefix+"os"] = d.OS
m[prefix+"user_agent"] = d.UserAgent
if d.Mobile {
m[prefix+"mobile"] = "true"
} else {
m[prefix+"mobile"] = "false"
}
}
func DecodeDeviceInfoFromMap(m map[string]string, prefix string) *DeviceInfo {
return &DeviceInfo{
ID: m[prefix+"id"],
RemoteAddr: m[prefix+"remote_addr"],
RemoteZone: m[prefix+"remote_zone"],
Browser: m[prefix+"browser"],
OS: m[prefix+"os"],
UserAgent: m[prefix+"user_agent"],
Mobile: m[prefix+"mobile"] == "true",
}
}
// LogEntry represents an authentication event in the user-specific log.
type LogEntry struct {
Timestamp time.Time `json:"timestamp"`
Username string `json:"username"`
Type string `json:"log_type"`
Message string `json:"message,omitempty"`
Service string `json:"service,omitempty"`
LoginMethod string `json:"login_method,omitempty"`
DeviceInfo *auth.DeviceInfo `json:"device_info,omitempty"`
Timestamp time.Time `json:"timestamp"`
Username string `json:"username"`
Type string `json:"log_type"`
Message string `json:"message,omitempty"`
Service string `json:"service,omitempty"`
LoginMethod string `json:"login_method,omitempty"`
DeviceInfo *DeviceInfo `json:"device_info,omitempty"`
}
func (e *LogEntry) Validate() error {
......@@ -73,10 +111,10 @@ type GetUserDevicesRequest struct {
}
type MetaDeviceInfo struct {
DeviceInfo *auth.DeviceInfo `json:"device_info"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
NumLogins int `json:"num_logins"`
DeviceInfo *DeviceInfo `json:"device_info"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
NumLogins int `json:"num_logins"`
}
type GetUserDevicesResponse struct {
......
......@@ -4,7 +4,7 @@ import (
"context"
"database/sql"
"git.autistici.org/id/auth"
"git.autistici.org/id/usermetadb"
)
var analysisStatements = map[string]string{
......@@ -31,7 +31,7 @@ func (d *analysisService) Close() {
d.stmts.Close()
}
func (d *analysisService) CheckDevice(ctx context.Context, username string, deviceInfo *auth.DeviceInfo) (bool, error) {
func (d *analysisService) CheckDevice(ctx context.Context, username string, deviceInfo *usermetadb.DeviceInfo) (bool, error) {
var seen bool
err := withReadonlyTX(d.db, func(tx *sql.Tx) error {
stmt := d.stmts.get(tx, "check_device_info")
......
......@@ -6,7 +6,6 @@ import (
"log"
"time"
"git.autistici.org/id/auth"
"git.autistici.org/id/usermetadb"
)
......@@ -164,7 +163,7 @@ func (u *userlogDB) Close() {
}
// Update or create an entry in the 'devices' table.
func (u *userlogDB) updateDeviceInfo(tx *sql.Tx, username string, deviceInfo *auth.DeviceInfo) error {
func (u *userlogDB) updateDeviceInfo(tx *sql.Tx, username string, deviceInfo *usermetadb.DeviceInfo) error {
now := roundTimestamp(time.Now().UTC())
var seen bool
......@@ -279,7 +278,7 @@ func (u *userlogDB) GetUserLogs(ctx context.Context, username string, maxDays, l
}
defer rows.Close()
for rows.Next() {
var di auth.DeviceInfo
var di usermetadb.DeviceInfo
e := &usermetadb.LogEntry{
Username: username,
}
......@@ -319,7 +318,7 @@ func (u *userlogDB) GetUserDevices(ctx context.Context, username string) ([]*use
}
defer rows.Close()
for rows.Next() {
mdi := &usermetadb.MetaDeviceInfo{DeviceInfo: &auth.DeviceInfo{}}
mdi := &usermetadb.MetaDeviceInfo{DeviceInfo: &usermetadb.DeviceInfo{}}
if err := rows.Scan(
&mdi.DeviceInfo.ID,
&mdi.DeviceInfo.Browser,
......
......@@ -12,7 +12,6 @@ import (
"testing"
"time"
"git.autistici.org/id/auth"
"git.autistici.org/id/usermetadb"
)
......@@ -34,11 +33,11 @@ func choose(l []string) string {
return l[rand.Intn(len(l))]
}
func generateRandomDeviceInfo() *auth.DeviceInfo {
func generateRandomDeviceInfo() *usermetadb.DeviceInfo {
var buf [8]byte
crand.Read(buf[:]) // nolint
id := hex.EncodeToString(buf[:])
return &auth.DeviceInfo{
return &usermetadb.DeviceInfo{
ID: id,
Browser: choose(randomBrowsers),
UserAgent: choose(randomUserAgents),
......@@ -47,8 +46,8 @@ func generateRandomDeviceInfo() *auth.DeviceInfo {
}
}
func generateAllRandomDevices() map[string][]*auth.DeviceInfo {
byUser := make(map[string][]*auth.DeviceInfo)
func generateAllRandomDevices() map[string][]*usermetadb.DeviceInfo {
byUser := make(map[string][]*usermetadb.DeviceInfo)
for _, u := range randomUsernames {
for i := 0; i < 3; i++ {
byUser[u] = append(byUser[u], generateRandomDeviceInfo())
......@@ -64,7 +63,7 @@ func randomTime() time.Time {
return time.Now().Add(-time.Duration(rand.Intn(twoYearsSeconds)) * time.Second)
}
func generateTestLogs(n int, userDevices map[string][]*auth.DeviceInfo) []*usermetadb.LogEntry {
func generateTestLogs(n int, userDevices map[string][]*usermetadb.DeviceInfo) []*usermetadb.LogEntry {
out := make([]*usermetadb.LogEntry, 0, n)
for i := 0; i < n; i++ {
u := choose(randomUsernames)
......
include: "https://git.autistici.org/ai3/build-deb/raw/master/ci-nextstable.yml"
Copyright (c) 2017 Autistici/Inventati <info@autistici.org>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
auth-server
===========
A low-level authentication server with pluggable backends and some
advanced features:
* two-factor authentication support (TOTP, U2F)
* application-specific passwords
* rate limiting and brute force protection
* new device detection
Its purpose is to be the single point of authentication for all
authentication flows in a service.
# Deployment
The auth-server is fully stateless: it delegates state to other
backends such as Memcached for short-term storage, and
[usermetadb](https://git.autistici.org/id/usermetadb) for long-term
anonymized user activity data. For this reason, it is recommended to
install an auth-server on every host.
The authentication protocol is a simple line-based text protocol. The
auth-server can listen on a UNIX or TCP socket: in the first case,
filesystem permissions should be used to control access to the socket,
while in the second case there is support for SSL, with optional
checks on the provided client certificates.
## Services
A *service* in auth-server is a specific scope for an authentication
workflow, normally associated with a specific user-facing
service. Multiple services can be defined, each with its own
functionality and user backends.
## User backends
The authentication server data model is based on the concept of a
*user account*. The server knows how to retrieve user accounts stored
in LDAP or SQL databases, but it has to be told the specific details
of how to find them and how to map the information there to what it
needs.
## Other Dependencies
The auth-server can optionally use *memcached* to store short-term
data with a relatively high probability of retrieval. This is used to
store U2F challenges, and used OTP tokens for replay protection. If no
memcache servers are configured, such functionality will be disabled
but the auth-server will still run (useful for tests, or simpler
deployments).
It is possible to specify multiple memcached servers for HA purposes,
with a *write-all / read-any* model.
# Configuration
The behavior of auth-server can be configured with a YAML file.
The YAML file should contain a dictionary with the following attributes:
* `services` is a dictionary describing all known services and their
authentication parameters. See the *Service definition* section below
* `services_dir` (optional) points at a directory containing service
configuration. Besides describing services in the main configuration
file (using the `services` attribute), it is possible to define
additional services in YAML-encoded files (having a *.yml*
extension), which is more automation-friendly.
* `backends` is a dictionary describing all known backends and their
configuration parameters. The *file* backend is predefined and always
exists (it requires no configuration).
* `backends_dir` (optional) points at a directory containing backend
configuration as YAML-encoded files: all files with a *.yml*
extension will be loaded.
* `rate_limits` defines the global rate limiters and blacklists. See
the *Rate limiting* section below.
* `user_meta_server` holds the configuration for the user-meta-server
backend:
* `url` is the URL of the service
* `tls` configures TLS for the client:
* `cert` is the path to the client certificate
* `key` is the path to the client private key
* `ca` is the path to the CA store to verify the server certificate
* `memcache_servers` contains a list of memcached server addresses (in
host:port format)
## Example configuration
An example configuration using both the *sql* backend (with default
schema and queries) for normal users, and the *file* backend for admin
users could look like this:
```yaml
---
backends:
sql:
driver: sqlite3
db_uri: users.db
services:
example_service:
backends:
- backend: sql
- backend: file
params:
src: admins.yml
static_groups: [admins]
```
## Rate limiting
Rate limits and blacklists are global (available to all services), to
allow brute force protection to work across multiple services. The
top-level configuration attribute `rate_limits` is a dictionary of
named rate limiting configurations, that can be later referenced in
the service-specific `rate_limits` list. Each rate limiter definition
should specify the following attributes:
* `limit` counts the number of events to allow over a period of time
* `period` defines the period of time
* `blacklist_for` adds the client to a blacklist if their request rate
goes above the specified threshold
* `on_failure` is a boolean value, when true the rate limiter will
only be applied to failed authentication requests
* `keys` is a list of strings specifying the request identifiers that
will make up the rate limiter key. The list can include one or both
of *ip* (referring to the remote client's IP) and *user* (username).
* `bypass` is a list of criteria that will cause the request to skip
the enforcement of this ratelimit/blacklist. Criteria are objects
with `key` (one of *ip* or *user*) and *value* attributes, which
specify an exact equality match.
The following is an example of an IP-based ratelimit with blacklist
period of 1 hour, that will allow an arbitrary amount of requests from
localhost:
```yaml
rate_limits:
blacklist_10qps_1h:
limit: 100
period: 10
blacklist_for: 3600
keys: [ip]
bypass:
- key: ip
value: "127.0.0.1"
- key: ip
value: "::1"
```
## Service definition
Each service definition is a dictionary with the following attributes:
* `backends` is a list of user backend specifications, each one a
dictionary/map with the following attributes:
* `backend` must be the name of a backend that appears in the
top-level configuration map *backends*.
* `params` is a map of backend-specific attributes that configure
the backend for this service.
* `static_groups` is a list of group names that users sourced from
this backend will automatically be added to
* `challenge_response` is a boolean parameter that, when true, enables
two-factor authentication for this service (it should be enabled
only for interactive services)
* `enforce_2fa` is a boolean flag that, when true, will disable
non-2FA logins for this service
* `ignore_2fa` is a boolean flag that, when set, will ignore the
presence of application-specific passwords for the user, and will
always authenticate against the primary password
* `enable_last_login_reporting` is a boolean flag that enables last login
reporting to usermetadb
* `enable_device_tracking` is a boolean flag that enables device
tracking for this service (assuming the client provides device
information)
* `rate_limits` is a list of names of global rate limiters to be
applied to this service.
## File backend
The *file* backend reads users and their credentials from a
YAML-encoded file. The service-specific configuration parameters are:
* `src` should point at the users file.
This file should contain a list of dictionaries, each representing a
user, with the following attributes:
* `name` is the username
* `email` is the email associated with the user (optional)
* `password` stores the encrypted password, see *Password Encoding*
below for details on the supported algorithms etc.
* `totp_secret` stores the *unencrypted* TOTP secret seed
(base32-encoded)
* `u2f_registrations` is a list of U2F registrations with `key_handle`
and `public_key` attributes, in the format used by *pamu2fcfg* (for
convenience)
* `groups` is a list of group names that the user belongs to
## LDAP backend
The *ldap* backend will look up user information in a LDAP database.
The backend connects to a single LDAP server and requires the
following top-level configuration:
* `uri` of the LDAP server (like *ldapi:///var/run/ldap/ldapi*)
* `bind_dn` is the DN to bind with
* `bind_pw_file` points at a file containing the bind password
Each service can then use different queries, as shown in the next
section.
### Query definition
LDAP queries are meant to return a single user account object from the
database using a *search* operation. There's two parts to it: first
the right object needs to be located, then we need to map the object's
attributes to someting that the auth-server understands.
The LDAP query for a service is defined by the following standard LDAP
parameters:
* `search_base` specifies a base DN for the search
* `search_filter` specifies a filter to apply to the search
* `scope` specifies the scope of the LDAP search, must be one of
*base*, *one* or *sub*
* `attrs` is a dictionary mapping LDAP attributes to their auth-server
metadata counterparts, see *Schema definition* below.
The `search_filter` should contain somewhere the literal string `%s`,
which will be replaced with the username in the final LDAP query.
### Schema definition
In order to retrieve authentication information from the LDAP object,
the authentication server needs to know which attributes to use. To do
so, we use a so-called *schema definition* (a map of symbolic names to
LDAP attributes). The following attribute names are defined:
* `password` contains the encrypted password. Since this attribute is
often also used for authentication of the LDAP protocol itself, an
eventual `{crypt}` prefix is ignored. Passwords should be encrypted,
see *Password Encoding* below for details on the supported
algorithms etc.
* `otp_secret` should contain the base32-encoded TOTP secret
* `app_specific_password` (possibly repeated) contains an encrypted
app-specific password
The default attribute mapping looks like this:
password: userPassword
totp_secret: totpSecret
app_specific_password: appSpecificPassword
Except for *userPassword*, the others are custom LDAP attributes and
are not part of any standard schema definition. You should create your
own.
App-specific passwords should be encoded as colon-separated strings:
service:encrypted_password:comment
The password should be encrypted. The comment is a free-form string
set by the user to tell the various credentials apart.
## SQL backend
The SQL backend allows you to use a SQL database to store user
information. It can adapt to any schema, provided that you can write
the queries it expects.
The parameters for the SQL backend configuration are:
* `driver` is the name of the database/sql driver (currently it must
be one of `sqlite3`, `mysql` or `postgres`, the built-in drivers)
* `db_uri` is the database URI (a.k.a. DSN), whose exact syntax will
depend on the chosen driver. Check out the documentation for the
database/sql [sqlite](https://github.com/mattn/go-sqlite3),
[mysql](https://github.com/go-sql-driver/mysql) and
[postgres](https://godoc.org/github.com/lib/pq) drivers.
### Query definition
Each service can specify a set of different SQL queries. It can be
configured with the following attributes:
* `queries` holds the map of SQL queries that tell the auth-server
how to query your database.
The known queries are identified by name. It does not matter what
operations you do as long as the queries take the expected input
substitution parameters, and return rows with the expected number of
fields (column names do not matter). You should use the parameter
substitution symbol `?` as placeholder for query parameters.
* `get_user` takes a single parameter (the user name) and must return