Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions datastores/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ var (
Error = log.New(os.Stdout, "ERROR: ", log.LstdFlags)
)

type ApiKeyResult struct {
KeyId uint
UserId uint
ScopeRead bool
}

type Datastore interface {
FindProfilesById(userId ...uint64) ([]*proto.Profile, error)
FindProfilesByUsername(username string) ([]*proto.Profile, error)
Expand All @@ -45,4 +51,5 @@ type Datastore interface {
FindAwol() ([]*proto.Awol, error)
GetTableUpdates() ([]xenforo.TableInfo, error)
FindProfileByGamertag(gamertag string) (*proto.Profile, error)
ValidateApiKey(rawKey string) (*ApiKeyResult, error)
}
21 changes: 21 additions & 0 deletions datastores/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,27 @@ func (ds Mysql) GetTableUpdates() ([]xenforo.TableInfo, error) {
return updates, nil
}

func (ds Mysql) ValidateApiKey(rawKey string) (*ApiKeyResult, error) {
var row struct {
KeyId uint `gorm:"column:key_id"`
UserId uint `gorm:"column:user_id"`
ScopeRead bool `gorm:"column:scope_read"`
}
tx := ds.Db.Raw(`
SELECT key_id, user_id, scope_read
FROM xf_cav7_api_key
WHERE key_hash = UNHEX(SHA2(?, 256))
AND is_active = 1`, rawKey).Scan(&row)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
go ds.Db.Exec(`UPDATE xf_cav7_api_key SET last_used_date = UNIX_TIMESTAMP() WHERE key_id = ?`, row.KeyId)
return &ApiKeyResult{KeyId: row.KeyId, UserId: row.UserId, ScopeRead: row.ScopeRead}, nil
}

func (ds Mysql) FindProfileByGamertag(gamertag string) (*proto.Profile, error) {
var profile milpacs.Profile

Expand Down
27 changes: 18 additions & 9 deletions servers/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ package gateway
import (
"compress/gzip"
"context"
"crypto/subtle"
"fmt"
"log"
"mime"
Expand All @@ -30,6 +29,7 @@ import (
"strings"

"github.com/7cav/api/cache"
"github.com/7cav/api/datastores"
"github.com/7cav/api/middleware"
"github.com/7cav/api/proto"
_ "github.com/7cav/api/statik" // static files import - unused in the codebase, but required cuz reasons
Expand All @@ -39,9 +39,9 @@ import (
)

type Service struct {
Address string
Cache *cache.RedisCache
APISecret string
Address string
Cache *cache.RedisCache
Datastore datastores.Datastore
}

var (
Expand All @@ -62,12 +62,21 @@ func getOpenAPIHandler() http.Handler {
return http.FileServer(statikFs)
}

func authMiddleware(secret string, next http.Handler) http.Handler {
// maxTokenLen is the maximum length of a raw API key we'll accept.
// cav7_ prefix (5) + 64 hex chars = 69; 128 gives generous headroom.
const maxTokenLen = 128

func authMiddleware(ds datastores.Datastore, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
token := strings.TrimPrefix(authHeader, "Bearer ")
token := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if token == "" || len(token) > maxTokenLen {
Warn.Printf("Unauthorized HTTP access attempt from %s", r.RemoteAddr)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

if subtle.ConstantTimeCompare([]byte(token), []byte(secret)) != 1 {
key, err := ds.ValidateApiKey(token)
if err != nil || key == nil || !key.ScopeRead {
Warn.Printf("Unauthorized HTTP access attempt from %s", r.RemoteAddr)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
Expand Down Expand Up @@ -136,7 +145,7 @@ func (service *Service) Server() *http.Server {

openApi := getOpenAPIHandler()

handler := authMiddleware(service.APISecret,
handler := authMiddleware(service.Datastore,
middleware.CacheMiddleware(service.Cache, compressionMiddleware(gwMux)))

// if requests start with /api then forward it on to the grpc-gateway client
Expand Down
33 changes: 15 additions & 18 deletions servers/grpc/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ package grpc

import (
"context"
"crypto/subtle"
"strings"

"github.com/7cav/api/datastores"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

func NewAuthInterceptor(secret string) grpc.UnaryServerInterceptor {
const maxTokenLen = 128

func NewAuthInterceptor(ds datastores.Datastore) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
// You can use your global logger here if accessible, or fmt
return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
}

Expand All @@ -42,24 +43,20 @@ func NewAuthInterceptor(secret string) grpc.UnaryServerInterceptor {
return nil, status.Errorf(codes.Unauthenticated, "missing authorization token")
}

authHeader := strings.TrimSpace(authHeaders[0])
raw := strings.TrimSpace(authHeaders[0])
if !strings.HasPrefix(raw, "Bearer ") {
return nil, status.Errorf(codes.Unauthenticated, "missing authorization token")
}
token := strings.TrimSpace(raw[len("Bearer "):])
if token == "" || len(token) > maxTokenLen {
return nil, status.Errorf(codes.Unauthenticated, "missing authorization token")
}

// Pass the baked-in secret to the check
if !isValidToken(authHeader, secret) {
// logic to log warning if needed
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
key, err := ds.ValidateApiKey(token)
if err != nil || key == nil || !key.ScopeRead {
return nil, status.Errorf(codes.Unauthenticated, "invalid api key")
}

return handler(ctx, req)
}
}

func isValidToken(authHeader, secret string) bool {
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
return false
}

// Compare the token against the secret passed from startup
return subtle.ConstantTimeCompare([]byte(token), []byte(secret)) == 1
}
20 changes: 4 additions & 16 deletions servers/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,6 @@ var (
Error = log.New(os.Stdout, "ERROR: ", log.LstdFlags)
)

func setupAuth() string {
secret := viper.GetString("API_SECRET")
if secret == "" {
// It is critical to fail fast if this is missing
Error.Println("CRITICAL: API_SECRET is not set in environment/config")
os.Exit(1)
}
return secret
}

func setupRedis() *cache.RedisCache {
redisHost := viper.GetString("REDIS_HOST")
if redisHost == "" {
Expand Down Expand Up @@ -145,7 +135,6 @@ func (server *MicroServer) Start() {
Error.Fatalf("Failed to listen on %s: %w", server.addr, err)
}

apiSecret := setupAuth()
ds := setupDatasource()
server.cache = setupRedis()
go cache.CacheManager(server.cache, ds)
Expand All @@ -155,13 +144,13 @@ func (server *MicroServer) Start() {
// If this needed to change in the future, then we will need to refactor this method
opts := []grpc.ServerOption{
// Intercept request to check the token.
grpc.UnaryInterceptor(grpcServices.NewAuthInterceptor(apiSecret)),
grpc.UnaryInterceptor(grpcServices.NewAuthInterceptor(ds)),
//grpc.Creds(creds),
}

// launch goroutines for multiplexed listener
Info.Println("Starting HTTP listener")
go servHTTP(server, httpL)
go servHTTP(server, httpL, ds)
Info.Println("Starting GRPC listener")
servGRPC(server, grpcL, opts, ds)
}
Expand All @@ -180,9 +169,8 @@ func servGRPC(server *MicroServer, lis net.Listener, grpcOpts []grpc.ServerOptio
}
}

func servHTTP(server *MicroServer, lis net.Listener) {
secret := setupAuth()
service := httpServices.Service{Address: server.addr, Cache: server.cache, APISecret: secret,}
func servHTTP(server *MicroServer, lis net.Listener, ds datastores.Datastore) {
service := httpServices.Service{Address: server.addr, Cache: server.cache, Datastore: ds}
server.httpServer = service.Server()
if err := server.httpServer.Serve(lis); err != nil {
Error.Fatalf("unable to start HTTP servers: ", err)
Expand Down