Files

83 lines
2.8 KiB
Go
Raw Permalink Normal View History

package auth
import (
"context"
"errors"
"github.com/J3vb/OwnCord/Server/db"
)
// tokenStore is the DB surface bearer-token resolution needs. *db.DB satisfies
// it directly; tests use a fake. Kept as a tiny interface (like db.Auditor) so
// the security-critical resolution logic is unit-testable without a real DB.
type tokenStore interface {
GetSessionByTokenHash(ctx context.Context, tokenHash string) (*db.Session, error)
GetActiveAPIToken(ctx context.Context, tokenHash string) (*db.APIToken, error)
GetUserByID(ctx context.Context, id int64) (*db.User, error)
GetRoleByID(ctx context.Context, id int64) (*db.Role, error)
}
// Sentinel outcomes, so each caller can reproduce its existing 401/403 responses
// exactly. A DB outage is NOT one of these — it surfaces as a wrapped error.
var (
ErrTokenNotFound = errors.New("auth: no matching session or api token")
ErrTokenExpired = errors.New("auth: session expired")
ErrUserNotFound = errors.New("auth: user not found")
ErrRoleNotFound = errors.New("auth: role not found")
)
// ResolveTokenHash resolves a hashed bearer token to its principal (user + role).
//
// It matches a login session FIRST — so every existing session code path is
// preserved byte-for-byte — and only falls through to an API token when no
// session row matches. The returned *db.Session is nil for an API-token
// principal (downstream consumers already guard a nil session).
//
// A DB error is returned WRAPPED (never a sentinel) so callers can distinguish
// an outage from a bad token and never fall through to API-token lookup on an
// outage. On ErrTokenExpired the matched (expired) session is returned so the
// caller can schedule its cleanup by hash, exactly as the api middleware does today.
func ResolveTokenHash(ctx context.Context, store tokenStore, hash string) (*db.User, *db.Role, *db.Session, error) {
sess, err := store.GetSessionByTokenHash(ctx, hash)
if err != nil {
return nil, nil, nil, err // DB outage — do not fall through to API tokens
}
var userID int64
switch {
case sess != nil:
if IsSessionExpired(sess.ExpiresAt) {
return nil, nil, sess, ErrTokenExpired
}
userID = sess.UserID
default:
tok, err := store.GetActiveAPIToken(ctx, hash)
if err != nil {
return nil, nil, nil, err
}
if tok == nil {
return nil, nil, nil, ErrTokenNotFound
}
userID = tok.UserID
}
user, err := store.GetUserByID(ctx, userID)
if err != nil {
return nil, nil, nil, err
}
if user == nil {
return nil, nil, nil, ErrUserNotFound
}
// A dangling role_id returns (nil, nil): the nil check is load-bearing so a
// nil role never reaches the context and every downstream permission check.
role, err := store.GetRoleByID(ctx, user.RoleID)
if err != nil {
return nil, nil, nil, err
}
if role == nil {
return nil, nil, nil, ErrRoleNotFound
}
return user, role, sess, nil
}