mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-05-07 04:39:25 -05:00
156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"codeberg.org/shroff/phylum/server/internal/db"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/mileusna/useragent"
|
|
)
|
|
|
|
type APIKey struct {
|
|
ID string
|
|
Created time.Time
|
|
Expires pgtype.Timestamptz
|
|
Description string
|
|
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 {
|
|
q += " AND (expires IS NULL OR expires > current_timestamp)"
|
|
}
|
|
q += " ORDER BY id DESC"
|
|
if rows, err := db.Query(q, userID); err != nil {
|
|
return nil, err
|
|
} else {
|
|
return pgx.CollectRows(rows, scanAPIKey)
|
|
}
|
|
}
|
|
|
|
func RevokeAPIKey(db db.Handler, userID int32, keyIDStr string, delete bool) error {
|
|
const qUpdate = "UPDATE api_keys SET expires = CURRENT_TIMESTAMP WHERE id = $2 AND user_id = $1"
|
|
const qDelete = "DELETE FROM api_keys WHERE id = $2 AND user_id = $1"
|
|
q := qUpdate
|
|
if delete {
|
|
q = qDelete
|
|
}
|
|
if keyID, err := uuid.Parse(keyIDStr); err != nil {
|
|
return errors.New("failed to parse key ID: " + err.Error())
|
|
} else if tag, err := db.Exec(q, userID, keyID); err != nil {
|
|
return err
|
|
} else if tag.RowsAffected() == 0 {
|
|
return errors.New("key not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func scanAPIKey(row pgx.CollectableRow) (APIKey, error) {
|
|
var apiKey APIKey
|
|
var id uuid.UUID
|
|
err := row.Scan(
|
|
&id,
|
|
&apiKey.Created,
|
|
&apiKey.Expires,
|
|
&apiKey.Description,
|
|
&apiKey.Scopes,
|
|
)
|
|
apiKey.ID = id.String()
|
|
return apiKey, err
|
|
}
|
|
|
|
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:], clientInfo)
|
|
}
|
|
}
|
|
|
|
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, clientInfo)
|
|
}
|
|
}
|
|
|
|
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 = $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[:], clientInfo)
|
|
|
|
auth = new(Auth)
|
|
err = row.Scan(&auth.expires, &auth.userID, &auth.scopes, &auth.userPermissions, &auth.homeID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
err = ErrCredentialsInvalid
|
|
}
|
|
} else if auth.expires.Valid && time.Now().After(auth.expires.Time) {
|
|
err = ErrCredentialsInvalid
|
|
}
|
|
return
|
|
}
|
|
|
|
func GenerateAPIKey(db db.Handler, auth *Auth, description string) (string, string, string, error) {
|
|
const q = `INSERT INTO api_keys(id, expires, user_id, hash, description, scopes) VALUES (@id, @expires, @user_id, @hash, @description, @scopes)`
|
|
|
|
id, _ := uuid.NewV7()
|
|
key := generateSecureKey(apiKeyLength)
|
|
hash := sha256.Sum256(key)
|
|
args := pgx.NamedArgs{
|
|
"id": id,
|
|
"expires": auth.expires,
|
|
"user_id": auth.userID,
|
|
"hash": hash[:],
|
|
"description": description,
|
|
"scopes": auth.scopes,
|
|
}
|
|
|
|
if _, err := db.Exec(q, args); err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return id.String(), b32Encoder.EncodeToString(key), b64Encoder.EncodeToString(append(id[:], key...)), nil
|
|
}
|