[server][auth] Record last use info [#45]

This commit is contained in:
Abhishek Shroff
2025-08-04 22:58:20 +05:30
parent 64bfb27af5
commit a2d84bfe2f
6 changed files with 55 additions and 15 deletions

View File

@@ -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
)

View File

@@ -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=

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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;