diff --git a/server/go.mod b/server/go.mod index a51e660a..4af876ca 100644 --- a/server/go.mod +++ b/server/go.mod @@ -34,6 +34,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mileusna/useragent v1.3.5 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/server/go.sum b/server/go.sum index ee020aea..0b1941b9 100644 --- a/server/go.sum +++ b/server/go.sum @@ -141,6 +141,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= +github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= diff --git a/server/internal/api/authenticator/authenticator.go b/server/internal/api/authenticator/authenticator.go index 3abdb402..be612c7f 100644 --- a/server/internal/api/authenticator/authenticator.go +++ b/server/internal/api/authenticator/authenticator.go @@ -39,10 +39,15 @@ func Require(c *gin.Context) { func getAuth(c *gin.Context) (*auth.Auth, error) { db := db.Get(c.Request.Context()) + connectionDetails := auth.ConnectionDetails( + c.RemoteIP(), + c.Request.Header.Get("X-Device"), + c.Request.Header.Get("User-Agent"), + ) if header := c.Request.Header.Get("Authorization"); header == "" { if cookie, err := c.Request.Cookie("api_token"); err == nil { encodedKey := cookie.Value - if a, err := auth.ReadAPIToken(db, encodedKey); err == nil { + if a, err := auth.ReadAPIToken(db, encodedKey, connectionDetails); err == nil { return a, nil } else { if errors.Is(err, auth.ErrCredentialsInvalid) { @@ -60,9 +65,9 @@ func getAuth(c *gin.Context) (*auth.Auth, error) { var err error if keyIDStr == "" { - a, err = auth.ReadAPIToken(db, keyStr) + a, err = auth.ReadAPIToken(db, keyStr, connectionDetails) } else { - a, err = auth.ReadAPIKey(db, keyIDStr, keyStr) + a, err = auth.ReadAPIKey(db, keyIDStr, keyStr, connectionDetails) } if err == nil { @@ -75,7 +80,7 @@ func getAuth(c *gin.Context) (*auth.Auth, error) { } } } else if encodedKey, ok := checkAuthHeader(header, "bearer"); ok { - if a, err := auth.ReadAPIToken(db, encodedKey); err == nil { + if a, err := auth.ReadAPIToken(db, encodedKey, connectionDetails); err == nil { return a, nil } else { if errors.Is(err, auth.ErrCredentialsInvalid) { diff --git a/server/internal/api/webdav/handler.go b/server/internal/api/webdav/handler.go index 8b97e192..2abec648 100644 --- a/server/internal/api/webdav/handler.go +++ b/server/internal/api/webdav/handler.go @@ -47,10 +47,15 @@ func (h *handler) HandleRequest(c *gin.Context) { var a *auth.Auth var err error + connectionDetails := auth.ConnectionDetails( + c.RemoteIP(), + c.Request.Header.Get("X-Device"), + c.Request.Header.Get("User-Agent"), + ) if keyIDStr == "" { - a, err = auth.ReadAPIToken(db, keyStr) + a, err = auth.ReadAPIToken(db, keyStr, connectionDetails) } else { - a, err = auth.ReadAPIKey(db, keyIDStr, keyStr) + a, err = auth.ReadAPIKey(db, keyIDStr, keyStr, connectionDetails) } if err == nil { diff --git a/server/internal/auth/api_key.go b/server/internal/auth/api_key.go index 251e82ae..5a8f10cf 100644 --- a/server/internal/auth/api_key.go +++ b/server/internal/auth/api_key.go @@ -2,6 +2,7 @@ package auth import ( "crypto/sha256" + "encoding/json" "errors" "time" @@ -9,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + "github.com/mileusna/useragent" ) type APIKey struct { @@ -19,6 +21,31 @@ type APIKey struct { Scopes []string } +type LastUsed struct { + Time int64 `json:"time"` + IP string `json:"ip"` + Device string `json:"device"` +} + +func ConnectionDetails(ip, device, userAgent string) []byte { + if device == "" { + ua := useragent.Parse(userAgent) + device = ua.Name + if ua.Device != "" { + device += " on " + ua.Device + } else if ua.OS != "" { + device += " on " + ua.OS + } + } + info := LastUsed{ + Time: time.Now().Unix(), + IP: ip, + Device: device, + } + b, _ := json.Marshal(info) + return b +} + func ListAPIKeys(db db.Handler, userID int32, includeExpired bool) ([]APIKey, error) { q := "SELECT id, created, expires, description, scopes FROM api_keys WHERE user_id = $1" if !includeExpired { @@ -63,36 +90,36 @@ func scanAPIKey(row pgx.CollectableRow) (APIKey, error) { return apiKey, err } -func ReadAPIToken(db db.Handler, encodedKey string) (*Auth, error) { +func ReadAPIToken(db db.Handler, encodedKey string, clientInfo []byte) (*Auth, error) { if b, err := b64Encoder.DecodeString(encodedKey); err != nil { return nil, ErrCredentialsInvalid } else if len(b) < 16 { return nil, ErrCredentialsInvalid } else { keyID, _ := uuid.FromBytes(b[:16]) - return readAPIKey(db, keyID, b[16:]) + return readAPIKey(db, keyID, b[16:], clientInfo) } } -func ReadAPIKey(db db.Handler, keyIDStr, keyStr string) (*Auth, error) { +func ReadAPIKey(db db.Handler, keyIDStr, keyStr string, clientInfo []byte) (*Auth, error) { if keyID, err := uuid.Parse(keyIDStr); err != nil { return nil, err } else if key, err := b32Encoder.DecodeString(keyStr); err != nil { return nil, err } else { - return readAPIKey(db, keyID, key) + return readAPIKey(db, keyID, key, clientInfo) } } -func readAPIKey(db db.Handler, keyID uuid.UUID, key []byte) (auth *Auth, err error) { +func readAPIKey(db db.Handler, keyID uuid.UUID, key []byte, clientInfo []byte) (auth *Auth, err error) { const q = `WITH cte(id) AS ( - UPDATE api_keys SET last_used = NOW() + UPDATE api_keys SET last_used = $3 WHERE id = $1 AND hash = $2 RETURNING id, user_id, expires, scopes ) SELECT k.expires, k.user_id, k.scopes, u.permissions, u.home FROM cte k JOIN users u ON k.user_id = u.id` hash := sha256.Sum256(key) - row := db.QueryRow(q, keyID, hash[:]) + row := db.QueryRow(q, keyID, hash[:], clientInfo) auth = new(Auth) err = row.Scan(&auth.expires, &auth.userID, &auth.scopes, &auth.userPermissions, &auth.homeID) diff --git a/server/internal/db/migrations/025_api_keys_last_used.sql b/server/internal/db/migrations/025_api_keys_last_used.sql index 0ef24528..d1b971df 100644 --- a/server/internal/db/migrations/025_api_keys_last_used.sql +++ b/server/internal/db/migrations/025_api_keys_last_used.sql @@ -1,5 +1,5 @@ -ALTER TABLE api_keys ADD COLUMN last_used TIMESTAMPTZ; +ALTER TABLE api_keys ADD COLUMN last_used TEXT; ---- create above / drop below ---- -ALTER TABLE api_keys DROP COLUMN last_used_by; +ALTER TABLE api_keys DROP COLUMN last_used;