From eb339f549572f9936a2d554bcc40c53868d669b4 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Wed, 13 May 2026 21:32:10 +0200 Subject: [PATCH 1/8] Add token creation methods Signed-off-by: Nikolai Rodionov --- go.mod | 2 ++ go.sum | 4 +++ internal/controllers/tokens.go | 39 ++++++++++++++++++++++-- internal/helpers/token/token.go | 45 ++++++++++++++++++++++++++++ internal/helpers/token/token_test.go | 16 ++++++++++ 5 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 internal/helpers/token/token.go create mode 100644 internal/helpers/token/token_test.go diff --git a/go.mod b/go.mod index c7676d3..0c96d25 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.9 require ( github.com/alecthomas/assert/v2 v2.11.0 github.com/alecthomas/kong v1.15.0 + github.com/db-operator/can-haz-password v0.1.1 github.com/go-logr/logr v1.4.3 github.com/go-logr/zapr v1.3.0 github.com/golang-jwt/jwt/v5 v5.2.2 @@ -24,6 +25,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/geozelot/intree v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lib/pq v1.10.9 // indirect diff --git a/go.sum b/go.sum index 7c3a9e9..2045178 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/db-operator/can-haz-password v0.1.1 h1:g0OC+9e4L751aWS9h02p/G4WrAaWgbR9LFjKLLC/hnA= +github.com/db-operator/can-haz-password v0.1.1/go.mod h1:0iO+taMqfWqNF3ltVhDWfIghD4VkXnnaQf06xiEIX8M= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= @@ -51,6 +53,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/geozelot/intree v1.0.0 h1:xUyiXMt0wD9zbPMOjy2rVShiUc3PGMPddPuTmi+Jy2s= +github.com/geozelot/intree v1.0.0/go.mod h1:JrqfsNwe17AgzOM023tCXPyUB89NhaZAb8o5rzfZQ7Q= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index 5789702..8a64e37 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -3,15 +3,28 @@ package controllers import ( "context" "database/sql" + "errors" "time" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" + "github.com/google/uuid" ) type TokenController struct { - DB *sql.DB + DB *sql.DB + HashCost int16 } +// Errors +var ( + ErrServerError = errors.New("internal server error") +) + type TokenData struct { UUID string + Name string UserID string CreatedAt time.Time LastUserAt time.Time @@ -23,7 +36,29 @@ type TokenData struct { type Scopes struct{} // Create a new token, store its hash in the database and return the token value -func (ctrl *TokenController) Create(ctx context.Context) (string, error) { +func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, error) { + id := uuid.NewString() + log := logger.FromContext(ctx).WithValues("uuid", id) + log.V(2).Info("Creating a new token") + + tokenValue, err := token.GenerateToken() + if err != nil { + log.Error(err, "Couldn't create a token") + return "", ErrServerError + } + + tokenHash, err := hash.HashPassword(tokenValue, int(ctrl.HashCost)) + if err != nil { + log.Error(err, "Couldn't calculate token hash") + return "", ErrServerError + } + + query := "INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6)" + if _, err := ctrl.DB.Query(query, id, tokenHash, "dummy", time.Now(), data.ExpiredAt); err != nil { + log.Error(err, "Couldn't insert a token in the database") + return "", ErrServerError + } + return "", nil } diff --git a/internal/helpers/token/token.go b/internal/helpers/token/token.go new file mode 100644 index 0000000..b37d7d4 --- /dev/null +++ b/internal/helpers/token/token.go @@ -0,0 +1,45 @@ +// Package token should be used to generate secure tokens +package token + +import ( + "fmt" + + "github.com/db-operator/can-haz-password/password" +) + +const ( + TokenPrefix = "sft" +) + +// GenerateToken generates secure password string +func GenerateToken() (string, error) { + generator := password.NewGenerator(newTokenRule()) + password, err := generator.Generate() + if err != nil { + return "", err + } + return fmt.Sprintf("%s_%s", TokenPrefix, password), nil +} + +// Minimum length of 20 characters, maximum length of 30 characters. +// Varied composition including special characters and uppercase and lowercase letters. +// Excludes consecutive dashes (for hybris compatibility) and uses only url safe special characters. +type tokenRule struct{} + +func newTokenRule() *tokenRule { + return &tokenRule{} +} + +func (r *tokenRule) Config() *password.Configuration { + return &password.Configuration{ + Length: 40, + CharacterClasses: []password.CharacterClassConfiguration{ // codespell:ignore + {Characters: password.LowercaseCharacters + password.UppercaseCharacters, Minimum: 10}, + {Characters: password.DigitCharacters, Minimum: 8}, + }, + } +} + +func (r *tokenRule) Valid(password []rune) bool { + return true +} diff --git a/internal/helpers/token/token_test.go b/internal/helpers/token/token_test.go new file mode 100644 index 0000000..6b851b3 --- /dev/null +++ b/internal/helpers/token/token_test.go @@ -0,0 +1,16 @@ +package token_test + +import ( + "strings" + "testing" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" + "github.com/stretchr/testify/assert" +) + +func TestUnitTokenGeneration(t *testing.T) { + token, err := token.GenerateToken() + assert.NoError(t, err) + assert.Len(t, token, 44) + assert.True(t, strings.HasPrefix(token, "sft_")) +} -- 2.49.1 From 327410f76e5dd29549b83f56c8889a4dd576fc45 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Wed, 13 May 2026 23:25:50 +0200 Subject: [PATCH 2/8] Add unit tests to the task file Signed-off-by: Nikolai Rodionov --- Taskfile.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 581df46..43a0445 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -13,6 +13,11 @@ tasks: cmd: go build -o ./build/softplayer silent: true + unit: + desc: Run unit tests + cmd: go test ./... + silent: true + run-migrations-dev: desc: Execute database migrations env: -- 2.49.1 From e96ec08a869f303f90fed446389e9324d88f7701 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 12:26:03 +0200 Subject: [PATCH 3/8] List possible services and filter unavailable Signed-off-by: Nikolai Rodionov --- api/v1/test.go | 20 ++++++++ api/v1/tokens.go | 91 ++++++++++++++++++++++++++++++++++ cmd/server.go | 36 ++++++++++++-- go.mod | 2 +- go.sum | 4 +- internal/controllers/tokens.go | 54 +++++++++++++++++++- 6 files changed, 197 insertions(+), 10 deletions(-) diff --git a/api/v1/test.go b/api/v1/test.go index c3739b0..94617f9 100644 --- a/api/v1/test.go +++ b/api/v1/test.go @@ -4,6 +4,8 @@ import ( "context" test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" ) func NewTestServer() *TestServer { @@ -18,6 +20,15 @@ func (t *TestServer) Pong(ctx context.Context, in *test.PongRequest) (*test.Pong return &test.PongResponse{}, nil } +func (t *TestServer) PongStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PongStreamResponse]) error { + for i := 0; i < 10; i++ { + if err := stream.Send(&test.PongStreamResponse{Dummy: "test"}); err != nil { + return err + } + } + return nil +} + func NewPublicTestServer() *PublicTestServer { return &PublicTestServer{} } @@ -29,3 +40,12 @@ type PublicTestServer struct { func (t *PublicTestServer) Ping(ctx context.Context, in *test.PingRequest) (*test.PingResponse, error) { return &test.PingResponse{}, nil } + +func (t *PublicTestServer) PingStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PingStreamResponse]) error { + for i := 0; i < 10; i++ { + if err := stream.Send(&test.PingStreamResponse{Dummy: "test"}); err != nil { + return err + } + } + return nil +} diff --git a/api/v1/tokens.go b/api/v1/tokens.go index 4c115eb..dcd0e99 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -1,9 +1,100 @@ package v1 import ( + "context" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" ) +// var _ tokens.TokensServiceServer = (*TokensServer)(nil) type TokensServer struct { tokens.UnimplementedTokensServiceServer + tokenCtrl *controllers.TokenController + authorizationCtrl *controllers.AuthController +} + +func NewTokensServer( + tokenCtrl *controllers.TokenController, + authorizationCtrl *controllers.AuthController, +) *TokensServer { + return &TokensServer{ + tokenCtrl: tokenCtrl, + authorizationCtrl: authorizationCtrl, + } +} + +// CreateToken implements [v1.TokensServiceServer]. +func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateTokenRequest) (*tokens.CreateTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + tokenData := &controllers.TokenData{ + Name: in.TokenMetadata.GetName(), + UserID: claims.UserID, + Scopes: &controllers.Scopes{}, + } + + token, err := srv.tokenCtrl.Create(ctx, tokenData) + if err != nil { + return nil, err + } + + return &tokens.CreateTokenResponse{ + TokenUuid: &tokens.TokenUUID{}, + TokenMetadata: &tokens.TokenMetadata{}, + TokenPermissions: &tokens.TokenPermissions{}, + TokenValue: &tokens.TokenValue{Token: token}, + }, status.Error(codes.Unimplemented, "Method is not implemented") +} + +// ForceTokenExpiration implements [v1.TokensServiceServer]. +func (t *TokensServer) ForceTokenExpiration(context.Context, *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "Method is not implemented") +} + +// GetToken implements [v1.TokensServiceServer]. +func (t *TokensServer) GetToken(context.Context, *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "Method is not implemented") +} + +// ListTokens implements [v1.TokensServiceServer]. +func (t *TokensServer) ListTokens(*emptypb.Empty, grpc.ServerStreamingServer[tokens.ListTokensResponse]) error { + return status.Error(codes.Unimplemented, "Method is not implemented") +} + +// RegenerateToken implements [v1.TokensServiceServer]. +func (t *TokensServer) RegenerateToken(context.Context, *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "Method is not implemented") +} + +// UpdateToken implements [v1.TokensServiceServer]. +func (t *TokensServer) UpdateToken(context.Context, *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) { + return nil, status.Error(codes.Unimplemented, "Method is not implemented") +} + +// ListPermissions implements [v1.TokensServiceServer]. +func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListPermissionsResponse]) error { + data := srv.tokenCtrl.ListPermissions(stream.Context()) + for key, data := range data { + result := &tokens.ListPermissionsResponse{ + Permissions: &tokens.Permissions{ + AvailabiePermissions: map[string]*tokens.MethodList{ + key: {Methods: data}, + }, + }, + } + if err := stream.Send(result); err != nil { + return status.Error(codes.Aborted, "Couldn't send data to the client") + } + } + return nil } diff --git a/cmd/server.go b/cmd/server.go index 9b9dfc9..a4eb2b1 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -13,6 +13,7 @@ import ( "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" + tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" @@ -82,13 +83,24 @@ func (cmd *Server) Run(ctx context.Context) error { selector.MatchFunc(selectorRequireAuth), ), ), - grpc.StreamInterceptor(grpc_zap.StreamServerInterceptor(logger.SetupLogger("info"))), + grpc.ChainStreamInterceptor( + grpc_zap.StreamServerInterceptor(logger.SetupLogger("info")), + selector.StreamServerInterceptor( + auth.StreamServerInterceptor(authInterceptor.AuthInterceptorFN), + selector.MatchFunc(selectorRequireAuth), + ), + ), ) if cmd.Reflection { reflection.Register(grpcServer) } + tokenCtrl := &controllers.TokenController{ + DB: db, + HashCost: cmd.HashCost, + } + accountCtrl := &controllers.AccountController{ HashCost: cmd.HashCost, DB: db, @@ -99,10 +111,17 @@ func (cmd *Server) Run(ctx context.Context) error { Redis: rdb, } - accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authInterceptor)) + // Services that should be accessible for tokens should go here accounts.RegisterAccountsServiceServer(grpcServer, v1.NewAccountServer(accountCtrl, authInterceptor)) test.RegisterTestServiceServer(grpcServer, v1.NewTestServer()) test.RegisterPublicTestServiceServer(grpcServer, v1.NewPublicTestServer()) + tokens.RegisterTokensServiceServer(grpcServer, v1.NewTokensServer(tokenCtrl, authInterceptor)) + accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authInterceptor)) + + info := grpcServer.GetServiceInfo() + tokenCtrl.SetGRPCInfo(info) + tokenCtrl.SetRules() + if err := grpcServer.Serve(lis); err != nil { return err } @@ -116,8 +135,15 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo if len(serviceParts) == 0 { return false } - serviceName := serviceParts[len(serviceParts)-1] - fmt.Println(serviceName) - return !strings.HasPrefix(serviceName, "Public") + + if strings.HasPrefix(serviceName, "Public") { + return false + } + + if strings.Contains(serviceName, "ServerReflection") { + return false + } + + return true } diff --git a/go.mod b/go.mod index 0c96d25..4da5701 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260511094640-0f4959475dc9 + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 2045178..606e00d 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260511094640-0f4959475dc9 h1:gVB4z3qZXriL8xfJnY8hEqiBTGHGeeWO5E3GY/7aNRM= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260511094640-0f4959475dc9/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a h1:F21MJw0xsiZf3cj4D+n8JPqkX38XlY+xFju2gQkC9eA= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index 8a64e37..fe8252d 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -4,19 +4,26 @@ import ( "context" "database/sql" "errors" + "regexp" "time" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" "github.com/google/uuid" + "google.golang.org/grpc" ) type TokenController struct { - DB *sql.DB - HashCost int16 + DB *sql.DB + HashCost int16 + ServiceInfo map[string]grpc.ServiceInfo + rules []rule } +// Services that are not available for tokens +var DisabledServicesRegex = []string{".*Accounts.*", ".*Tokens.*"} + // Errors var ( ErrServerError = errors.New("internal server error") @@ -35,6 +42,20 @@ type TokenData struct { type Scopes struct{} +// Set the grpc info, must happen after all the service are initialized +func (ctrl *TokenController) SetGRPCInfo(info map[string]grpc.ServiceInfo) { + ctrl.ServiceInfo = info +} + +func (ctrl *TokenController) SetRules() { + rules := []rule{ + {re: regexp.MustCompile(`.*Tokens.*`)}, + {re: regexp.MustCompile(`.*Accounts.*`)}, + {re: regexp.MustCompile(`.*Reflection.*`)}, + } + ctrl.rules = rules +} + // Create a new token, store its hash in the database and return the token value func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, error) { id := uuid.NewString() @@ -86,3 +107,32 @@ func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { func (ctrl *TokenController) List(ctx context.Context) error { return nil } + +// Lis all available permissions +func (ctrl *TokenController) ListPermissions(ctx context.Context) (result map[string][]string) { + result = map[string][]string{} + for key, val := range ctrl.ServiceInfo { + if shouldSkip(key, ctrl.rules) { + continue + } + var services []string + for _, svc := range val.Methods { + services = append(services, svc.Name) + } + result[key] = services + } + return +} + +type rule struct { + re *regexp.Regexp +} + +func shouldSkip(s string, rules []rule) bool { + for _, r := range rules { + if r.re.MatchString(s) { + return true + } + } + return false +} -- 2.49.1 From a534a4b92e1ba3de6757e04ec89656314c7cf467 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 14:30:09 +0200 Subject: [PATCH 4/8] WIP: Almost full token controller Signed-off-by: Nikolai Rodionov --- api/v1/tokens.go | 28 +++-- go.mod | 2 +- go.sum | 4 +- internal/controllers/tokens.go | 105 ++++++++++++++++-- migrations/20260510174348_tokens_init.up.sql | 1 + ...260510175121_accounts_timestamptz.down.sql | 3 - ...20260510175121_accounts_timestamptz.up.sql | 3 - 7 files changed, 118 insertions(+), 28 deletions(-) delete mode 100644 migrations/20260510175121_accounts_timestamptz.down.sql delete mode 100644 migrations/20260510175121_accounts_timestamptz.up.sql diff --git a/api/v1/tokens.go b/api/v1/tokens.go index dcd0e99..d774961 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "errors" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" @@ -37,23 +38,32 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken if claims.UserID == "" { return nil, status.Error(codes.Aborted, "Context is invalid") } + + if in.TokenPermissions == nil { + return nil, status.Error(codes.InvalidArgument, "Permissions must be set") + } + permissions := map[string][]string{} + for service, methods := range in.TokenPermissions.Permissions { + permissions[service] = methods.GetMethods() + } + tokenData := &controllers.TokenData{ Name: in.TokenMetadata.GetName(), UserID: claims.UserID, - Scopes: &controllers.Scopes{}, + Scopes: permissions, } token, err := srv.tokenCtrl.Create(ctx, tokenData) if err != nil { - return nil, err + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't create a token") } return &tokens.CreateTokenResponse{ - TokenUuid: &tokens.TokenUUID{}, - TokenMetadata: &tokens.TokenMetadata{}, - TokenPermissions: &tokens.TokenPermissions{}, - TokenValue: &tokens.TokenValue{Token: token}, - }, status.Error(codes.Unimplemented, "Method is not implemented") + TokenValue: &tokens.TokenValue{Token: token}, + }, nil } // ForceTokenExpiration implements [v1.TokensServiceServer]. @@ -86,8 +96,8 @@ func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerSt data := srv.tokenCtrl.ListPermissions(stream.Context()) for key, data := range data { result := &tokens.ListPermissionsResponse{ - Permissions: &tokens.Permissions{ - AvailabiePermissions: map[string]*tokens.MethodList{ + Permissions: &tokens.TokenPermissions{ + Permissions: map[string]*tokens.MethodList{ key: {Methods: data}, }, }, diff --git a/go.mod b/go.mod index 4da5701..c64afdb 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 606e00d..f986d32 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a h1:F21MJw0xsiZf3cj4D+n8JPqkX38XlY+xFju2gQkC9eA= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514095622-3ce39b865e5a/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f h1:zNKrOmQPnn+TPV/Zd6vMTYLb2GySSEyt2VawvjP7wb4= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index fe8252d..2548c6e 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -3,6 +3,7 @@ package controllers import ( "context" "database/sql" + "encoding/json" "errors" "regexp" "time" @@ -37,7 +38,7 @@ type TokenData struct { LastUserAt time.Time RevokedAt time.Time ExpiredAt time.Time - Scopes *Scopes + Scopes map[string][]string } type Scopes struct{} @@ -74,27 +75,97 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin return "", ErrServerError } - query := "INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, expires_at) VALUES ($1, $2, $3, $4, $5, $6)" - if _, err := ctrl.DB.Query(query, id, tokenHash, "dummy", time.Now(), data.ExpiredAt); err != nil { + scopesJson, err := json.Marshal(data.Scopes) + if err != nil { + log.Error(err, "Couldn't marshal permissions into json") + return "", ErrServerError + } + + query := ` + INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, generated_at, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)` + + if _, err := ctrl.DB.Query( + query, + id, + tokenHash, + data.UserID, + scopesJson, + time.Now(), + time.Now(), + data.ExpiredAt, + ); err != nil { log.Error(err, "Couldn't insert a token in the database") return "", ErrServerError } - return "", nil + return tokenValue, nil } // Update token name or permissions, other changes are ignored by this method -func (ctrl *TokenController) Update(ctx context.Context) (string, error) { - return "", nil +func (ctrl *TokenController) Update(ctx context.Context, data *TokenData) error { + log := logger.FromContext(ctx).WithValues("uuid", data.UUID) + log.V(2).Info("Updating a token") + + scopesJson, err := json.Marshal(data.Scopes) + if err != nil { + log.Error(err, "Couldn't marshal permissions into json") + return ErrServerError + } + + query := "UPDATE tokens SET name = $1, scopes = $2 WHERE uuid = $3;" + if _, err := ctrl.DB.Query(query, data.Name, scopesJson, data.UUID); err != nil { + log.Error(err, "Couldn't update a token in the database") + return ErrServerError + } + + return nil } // ForceExpiration of a token, so it can no longer be used -func (ctrl *TokenController) ForceExpiration(ctx context.Context) error { +func (ctrl *TokenController) ForceExpiration(ctx context.Context, id string) error { + log := logger.FromContext(ctx).WithValues("uuid", id) + log.V(2).Info("Forcing a token expiration") + + query := "UPDATE tokens SET revoked_at = $1 WHERE uuid = $2;" + if _, err := ctrl.DB.Query(query, time.Now(), id); err != nil { + log.Error(err, "Couldn't update a token in the database") + return ErrServerError + } + return nil } // Regenerate a token and get a new value -func (ctrl *TokenController) Regenerate(ctx context.Context) (string, error) { +func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string, error) { + log := logger.FromContext(ctx).WithValues("uuid", id) + log.V(2).Info("Regenerating a token") + + tokenValue, err := token.GenerateToken() + if err != nil { + log.Error(err, "Couldn't create a token") + return "", ErrServerError + } + + tokenHash, err := hash.HashPassword(tokenValue, int(ctrl.HashCost)) + if err != nil { + log.Error(err, "Couldn't calculate token hash") + return "", ErrServerError + } + + query := ` + UPDATE tokens + SET + token_hash = $1, + generated_at = $2, + expires_at = NOW() + (expires_at - generated_at), + WHERE uuid = $3;` + + if _, err := ctrl.DB.Query(query, tokenHash, time.Now(), data.UUID); err != nil { + log.Error(err, "Couldn't insert a token in the database") + return "", ErrServerError + } + return "", nil } @@ -104,8 +175,22 @@ func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { } // List all available token -func (ctrl *TokenController) List(ctx context.Context) error { - return nil +func (ctrl *TokenController) List(ctx context.Context, userID string) error { + query := ` + SELECT id, name, generated_at, expires_at + FROM tokens + WHERE user_id = $1` + err := ctrl.DB.QueryRowContext(ctx, query, userID).Scan( + &t.ID, + &t.UserID, + &t.Name, + &scopes, + &t.GeneratedAt, + &t.ExpiresAt, + ) + if err != nil { + return nil, err + } } // Lis all available permissions diff --git a/migrations/20260510174348_tokens_init.up.sql b/migrations/20260510174348_tokens_init.up.sql index aeeb408..eb74e73 100644 --- a/migrations/20260510174348_tokens_init.up.sql +++ b/migrations/20260510174348_tokens_init.up.sql @@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS tokens ( user_id UUID NOT NULL, scopes JSONB NOT NULL DEFAULT '[]', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + generated_at TIMESTAMPTZ NOT NULL DEFAULT now(), last_used_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, expires_at TIMESTAMPTZ diff --git a/migrations/20260510175121_accounts_timestamptz.down.sql b/migrations/20260510175121_accounts_timestamptz.down.sql deleted file mode 100644 index 56a57eb..0000000 --- a/migrations/20260510175121_accounts_timestamptz.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE accounts -ALTER COLUMN created_at TYPE TIMESTAMP -USING created_at AT TIME ZONE 'UTC'; diff --git a/migrations/20260510175121_accounts_timestamptz.up.sql b/migrations/20260510175121_accounts_timestamptz.up.sql deleted file mode 100644 index 3862462..0000000 --- a/migrations/20260510175121_accounts_timestamptz.up.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE accounts -ALTER COLUMN created_at TYPE TIMESTAMPTZ -USING created_at AT TIME ZONE 'UTC'; -- 2.49.1 From ee2aa77ceb91494ebec0bad2d9c9019ac020d065 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 15:09:21 +0200 Subject: [PATCH 5/8] Update the migration Signed-off-by: Nikolai Rodionov --- api/v1/tokens.go | 30 ++++++++- go.mod | 1 + go.sum | 1 + internal/controllers/tokens.go | 68 ++++++++++++++------ migrations/20260510174348_tokens_init.up.sql | 1 + 5 files changed, 79 insertions(+), 22 deletions(-) diff --git a/api/v1/tokens.go b/api/v1/tokens.go index d774961..b40c972 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -10,6 +10,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" ) // var _ tokens.TokensServiceServer = (*TokensServer)(nil) @@ -77,7 +78,34 @@ func (t *TokensServer) GetToken(context.Context, *tokens.GetTokenRequest) (*toke } // ListTokens implements [v1.TokensServiceServer]. -func (t *TokensServer) ListTokens(*emptypb.Empty, grpc.ServerStreamingServer[tokens.ListTokensResponse]) error { +func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListTokensResponse]) error { + claims, err := srv.authorizationCtrl.ClaimsFromContext(stream.Context()) + if err != nil { + return status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return status.Error(codes.Aborted, "Context is invalid") + } + + tokensRes, err := srv.tokenCtrl.List(stream.Context(), claims.UserID) + if err != nil { + if errors.Is(err, controllers.ErrServerError) { + return status.Error(codes.Internal, "Something is broken on our side") + } + return status.Error(codes.Aborted, "Couldn't create a token") + } + + for _, tokenRes := range tokensRes { + stream.Send(&tokens.ListTokensResponse{ + TokenUuid: &tokens.TokenUUID{ + Uuid: tokenRes.UUID, + }, + TokenMetadata: &tokens.TokenMetadata{ + Name: tokenRes.Name, + ExpiresAt: timestamppb.New(tokenRes.ExpiresAt), + }, + }) + } return status.Error(codes.Unimplemented, "Method is not implemented") } diff --git a/go.mod b/go.mod index c64afdb..b4b23a1 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/mattn/go-colorable v0.1.14 + github.com/opentracing/opentracing-go v1.1.0 github.com/redis/go-redis/v9 v9.18.0 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index f986d32..507b266 100644 --- a/go.sum +++ b/go.sum @@ -117,6 +117,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index 2548c6e..e191c0f 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -31,14 +31,15 @@ var ( ) type TokenData struct { - UUID string - Name string - UserID string - CreatedAt time.Time - LastUserAt time.Time - RevokedAt time.Time - ExpiredAt time.Time - Scopes map[string][]string + UUID string + Name string + UserID string + CreatedAt time.Time + LastUserAt time.Time + RevokedAt time.Time + ExpiresAt time.Time + GeneratedAt time.Time + Scopes map[string][]string } type Scopes struct{} @@ -93,7 +94,7 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin scopesJson, time.Now(), time.Now(), - data.ExpiredAt, + data.ExpiresAt, ); err != nil { log.Error(err, "Couldn't insert a token in the database") return "", ErrServerError @@ -161,7 +162,7 @@ func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string, expires_at = NOW() + (expires_at - generated_at), WHERE uuid = $3;` - if _, err := ctrl.DB.Query(query, tokenHash, time.Now(), data.UUID); err != nil { + if _, err := ctrl.DB.Query(query, tokenHash, time.Now(), id); err != nil { log.Error(err, "Couldn't insert a token in the database") return "", ErrServerError } @@ -175,22 +176,47 @@ func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { } // List all available token -func (ctrl *TokenController) List(ctx context.Context, userID string) error { +func (ctrl *TokenController) List(ctx context.Context, userID string) ([]TokenData, error) { + log := logger.FromContext(ctx).WithValues("user_id", userID) + log.V(2).Info("Regenerating a token") + + result := []TokenData{} + query := ` - SELECT id, name, generated_at, expires_at + SELECT uuid, name, generated_at, expires_at FROM tokens WHERE user_id = $1` - err := ctrl.DB.QueryRowContext(ctx, query, userID).Scan( - &t.ID, - &t.UserID, - &t.Name, - &scopes, - &t.GeneratedAt, - &t.ExpiresAt, - ) + + rows, err := ctrl.DB.QueryContext(ctx, query, userID) if err != nil { - return nil, err + log.Error(err, "Couldn't list tokens") + return nil, ErrServerError } + + defer rows.Close() + + if err := rows.Err(); err != nil { + log.Error(err, "Couldn't list tokens") + return nil, ErrServerError + } + + for rows.Next() { + var t TokenData + + err := rows.Scan( + &t.UUID, + &t.Name, + &t.GeneratedAt, + &t.ExpiresAt, + ) + if err != nil { + return nil, err + } + + result = append(result, t) + } + + return result, nil } // Lis all available permissions diff --git a/migrations/20260510174348_tokens_init.up.sql b/migrations/20260510174348_tokens_init.up.sql index eb74e73..0765384 100644 --- a/migrations/20260510174348_tokens_init.up.sql +++ b/migrations/20260510174348_tokens_init.up.sql @@ -1,5 +1,6 @@ CREATE TABLE IF NOT EXISTS tokens ( uuid UUID PRIMARY KEY, + name TEXT NOT NULL, token_hash TEXT NOT NULL, user_id UUID NOT NULL, scopes JSONB NOT NULL DEFAULT '[]', -- 2.49.1 From c5af2c75447965be959c3f1e72483c13c72e46a3 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 18:10:49 +0200 Subject: [PATCH 6/8] WIP: Almost full token controller Signed-off-by: Nikolai Rodionov --- Taskfile.yml | 10 +- api/v1/tokens.go | 133 ++++++++++++++++--- go.mod | 3 +- go.sum | 5 +- internal/controllers/tokens.go | 72 ++++++++-- migrations/20260510174348_tokens_init.up.sql | 2 +- 6 files changed, 192 insertions(+), 33 deletions(-) diff --git a/Taskfile.yml b/Taskfile.yml index 43a0445..eda0991 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -32,7 +32,15 @@ tasks: SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable SOFTPLAYER_REDIS_HOST: localhost:30379 cmd: go run main.go server --dev-mode --reflection - + psql: + desc: Connect to the dev database + env: + PGDATABASE: softplayer + PGUSER: softplayer + PGPORT: "30432" + PGHOST: localhost + PGPASSWORD: qwertyu9 + cmd: psql deploy-local-env: desc: Run a kind cluster and deploy deps deps: diff --git a/api/v1/tokens.go b/api/v1/tokens.go index b40c972..01519f3 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -49,9 +49,10 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken } tokenData := &controllers.TokenData{ - Name: in.TokenMetadata.GetName(), - UserID: claims.UserID, - Scopes: permissions, + Name: in.TokenMetadata.GetName(), + UserID: claims.UserID, + ExpiresAt: in.TokenMetadata.ExpiresAt.AsTime(), + Scopes: permissions, } token, err := srv.tokenCtrl.Create(ctx, tokenData) @@ -68,13 +69,56 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken } // ForceTokenExpiration implements [v1.TokensServiceServer]. -func (t *TokensServer) ForceTokenExpiration(context.Context, *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't create a token") + } + return &emptypb.Empty{}, nil } // GetToken implements [v1.TokensServiceServer]. -func (t *TokensServer) GetToken(context.Context, *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID) + if err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + + return &tokens.GetTokenResponse{ + TokenUuid: &tokens.TokenUUID{ + Uuid: token.UUID, + }, + TokenMetadata: &tokens.TokenMetadata{ + Name: token.Name, + ExpiresAt: timestamppb.New(token.ExpiresAt), + LastUsedAt: timestamppb.New(token.LastUsedAt), + GeneratedAt: timestamppb.New(token.GeneratedAt), + CreatedAt: timestamppb.New(token.CreatedAt), + RevokedAt: timestamppb.New(token.RevokedAt), + }, + TokenPermissions: &tokens.TokenPermissions{}, + }, nil } // ListTokens implements [v1.TokensServiceServer]. @@ -92,31 +136,86 @@ func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreami if errors.Is(err, controllers.ErrServerError) { return status.Error(codes.Internal, "Something is broken on our side") } - return status.Error(codes.Aborted, "Couldn't create a token") + return status.Error(codes.Aborted, "Couldn't list tokens") } for _, tokenRes := range tokensRes { - stream.Send(&tokens.ListTokensResponse{ + if err := stream.Send(&tokens.ListTokensResponse{ TokenUuid: &tokens.TokenUUID{ Uuid: tokenRes.UUID, }, TokenMetadata: &tokens.TokenMetadata{ - Name: tokenRes.Name, - ExpiresAt: timestamppb.New(tokenRes.ExpiresAt), + Name: tokenRes.Name, + ExpiresAt: timestamppb.New(tokenRes.ExpiresAt), + LastUsedAt: timestamppb.New(tokenRes.LastUsedAt), + GeneratedAt: timestamppb.New(tokenRes.GeneratedAt), + CreatedAt: timestamppb.New(tokenRes.CreatedAt), + RevokedAt: timestamppb.New(tokenRes.RevokedAt), }, - }) + }); err != nil { + return status.Error(codes.Aborted, "Couldn't send data") + } } - return status.Error(codes.Unimplemented, "Method is not implemented") + return nil } // RegenerateToken implements [v1.TokensServiceServer]. -func (t *TokensServer) RegenerateToken(context.Context, *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid()) + if err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + return &tokens.RegenerateTokenResponse{ + TokenValue: &tokens.TokenValue{ + Token: tokenVal, + }, + }, nil } // UpdateToken implements [v1.TokensServiceServer]. -func (t *TokensServer) UpdateToken(context.Context, *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) { - return nil, status.Error(codes.Unimplemented, "Method is not implemented") +func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) { + claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx) + if err != nil { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + if claims.UserID == "" { + return nil, status.Error(codes.Aborted, "Context is invalid") + } + + if in.TokenPermissions == nil { + return nil, status.Error(codes.InvalidArgument, "Permissions must be set") + } + + permissions := map[string][]string{} + for service, methods := range in.TokenPermissions.Permissions { + permissions[service] = methods.GetMethods() + } + tokenData := &controllers.TokenData{ + Name: in.TokenMetadata.Name, + Scopes: permissions, + } + if err := srv.tokenCtrl.Update(ctx, tokenData); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + return &tokens.UpdateTokenResponse{ + TokenUuid: &tokens.TokenUUID{}, + TokenMetadata: &tokens.TokenMetadata{}, + TokenPermissions: &tokens.TokenPermissions{}, + }, nil } // ListPermissions implements [v1.TokensServiceServer]. diff --git a/go.mod b/go.mod index b4b23a1..07ed484 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/mattn/go-colorable v0.1.14 - github.com/opentracing/opentracing-go v1.1.0 github.com/redis/go-redis/v9 v9.18.0 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 @@ -43,7 +42,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 507b266..de6260e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f h1:zNKrOmQPnn+TPV/Zd6vMTYLb2GySSEyt2VawvjP7wb4= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514115132-f577d4d9c77f/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd h1:Qzvr5tAI909J38KNlx3KknzBAgXy5ZJS+0qzvRh/FGI= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -117,7 +117,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index e191c0f..9f5b969 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -35,7 +35,7 @@ type TokenData struct { Name string UserID string CreatedAt time.Time - LastUserAt time.Time + LastUsedAt time.Time RevokedAt time.Time ExpiresAt time.Time GeneratedAt time.Time @@ -83,12 +83,13 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin } query := ` - INSERT INTO tokens (uuid, token_hash, user_id, scopes, created_at, generated_at, expires_at) - VALUES ($1, $2, $3, $4, $5, $6, $7)` + INSERT INTO tokens (uuid, description, token_hash, user_id, scopes, created_at, generated_at, expires_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8)` if _, err := ctrl.DB.Query( query, id, + data.Name, tokenHash, data.UserID, scopesJson, @@ -114,12 +115,14 @@ func (ctrl *TokenController) Update(ctx context.Context, data *TokenData) error return ErrServerError } - query := "UPDATE tokens SET name = $1, scopes = $2 WHERE uuid = $3;" + query := "UPDATE tokens SET description = $1, scopes = $2 WHERE uuid = $3;" if _, err := ctrl.DB.Query(query, data.Name, scopesJson, data.UUID); err != nil { log.Error(err, "Couldn't update a token in the database") return ErrServerError } + data, err := ctrl.Get(ctx, , id string, userID string) + return nil } @@ -171,8 +174,42 @@ func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string, } // Get an existing token data -func (ctrl *TokenController) Get(ctx context.Context, uuid string) error { - return nil +func (ctrl *TokenController) Get(ctx context.Context, id, userID string) (*TokenData, error) { + log := logger.FromContext(ctx).WithValues("uuid", id, "user_id", userID) + log.V(2).Info("Regenerating a token") + query := ` + SELECT uuid, description, generated_at, expires_at, last_used_at, revoked_at, created_at + FROM tokens + WHERE uuid = $1 AND user_id = $2` + token := &TokenData{} + var generatedAt sql.NullTime + var expiresAt sql.NullTime + var revokedAt sql.NullTime + var lastUsedAt sql.NullTime + var createdAt sql.NullTime + + if err := ctrl.DB.QueryRowContext(ctx, query, id, userID).Scan( + &token.UUID, + &token.Name, + &generatedAt, + &expiresAt, + &lastUsedAt, + &revokedAt, + &createdAt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, err + } + log.Error(err, "Couldn't find a token") + return nil, ErrServerError + } + token.GeneratedAt = generatedAt.Time + token.ExpiresAt = expiresAt.Time + token.RevokedAt = revokedAt.Time + token.LastUsedAt = lastUsedAt.Time + token.CreatedAt = createdAt.Time + + return token, nil } // List all available token @@ -183,7 +220,7 @@ func (ctrl *TokenController) List(ctx context.Context, userID string) ([]TokenDa result := []TokenData{} query := ` - SELECT uuid, name, generated_at, expires_at + SELECT uuid, description, generated_at, expires_at, revoked_at, last_used_at, created_at FROM tokens WHERE user_id = $1` @@ -203,13 +240,30 @@ func (ctrl *TokenController) List(ctx context.Context, userID string) ([]TokenDa for rows.Next() { var t TokenData + var generatedAt sql.NullTime + var expiresAt sql.NullTime + var revokedAt sql.NullTime + var lastUsedAt sql.NullTime + var createdAt sql.NullTime + err := rows.Scan( &t.UUID, &t.Name, - &t.GeneratedAt, - &t.ExpiresAt, + &generatedAt, + &expiresAt, + &revokedAt, + &lastUsedAt, + &createdAt, ) + + t.GeneratedAt = generatedAt.Time + t.ExpiresAt = expiresAt.Time + t.RevokedAt = revokedAt.Time + t.LastUsedAt = lastUsedAt.Time + t.CreatedAt = createdAt.Time + if err != nil { + log.Error(err, "Couldn't write token into a struct") return nil, err } diff --git a/migrations/20260510174348_tokens_init.up.sql b/migrations/20260510174348_tokens_init.up.sql index 0765384..eb4f1e6 100644 --- a/migrations/20260510174348_tokens_init.up.sql +++ b/migrations/20260510174348_tokens_init.up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS tokens ( uuid UUID PRIMARY KEY, - name TEXT NOT NULL, + description TEXT NOT NULL, token_hash TEXT NOT NULL, user_id UUID NOT NULL, scopes JSONB NOT NULL DEFAULT '[]', -- 2.49.1 From efe9042bdcbd1f3a1e8714255cee5a5694761efe Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Thu, 14 May 2026 19:54:55 +0200 Subject: [PATCH 7/8] Start adding token auth Signed-off-by: Nikolai Rodionov --- api/v1/tokens.go | 41 +++++++++++ cmd/server.go | 6 ++ go.mod | 5 +- go.sum | 4 +- internal/controllers/tokens.go | 110 +++++++++++++++++++++++++---- internal/helpers/hash/hash_test.go | 6 +- 6 files changed, 149 insertions(+), 23 deletions(-) diff --git a/api/v1/tokens.go b/api/v1/tokens.go index 01519f3..937e482 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -3,6 +3,7 @@ package v1 import ( "context" "errors" + "fmt" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" @@ -78,6 +79,13 @@ func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.Fo return nil, status.Error(codes.Aborted, "Context is invalid") } + if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") + } + if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil { if errors.Is(err, controllers.ErrServerError) { return nil, status.Error(codes.Internal, "Something is broken on our side") @@ -96,6 +104,12 @@ func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenReques if claims.UserID == "" { return nil, status.Error(codes.Aborted, "Context is invalid") } + if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") + } token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID) if err != nil { @@ -168,6 +182,12 @@ func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.Regener if claims.UserID == "" { return nil, status.Error(codes.Aborted, "Context is invalid") } + if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") + } tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid()) if err != nil { @@ -193,6 +213,12 @@ func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateToken return nil, status.Error(codes.Aborted, "Context is invalid") } + if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") + } if in.TokenPermissions == nil { return nil, status.Error(codes.InvalidArgument, "Permissions must be set") } @@ -235,3 +261,18 @@ func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerSt } return nil } + +func (srv *TokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) { + scopes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token) + if err != nil { + if errors.Is(err, controllers.ErrBadToken) { + return nil, status.Error(codes.Unauthenticated, "Token is not valid") + } + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + fmt.Println(scopes) + return nil, nil +} diff --git a/cmd/server.go b/cmd/server.go index a4eb2b1..ed97fc4 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -99,6 +99,7 @@ func (cmd *Server) Run(ctx context.Context) error { tokenCtrl := &controllers.TokenController{ DB: db, HashCost: cmd.HashCost, + Redis: rdb, } accountCtrl := &controllers.AccountController{ @@ -136,6 +137,7 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo return false } serviceName := serviceParts[len(serviceParts)-1] + fmt.Println(serviceName) if strings.HasPrefix(serviceName, "Public") { return false @@ -145,5 +147,9 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo return false } + if strings.Contains(callMeta.Method, "AuthenticateWithToken") { + return false + } + return true } diff --git a/go.mod b/go.mod index 07ed484..ec03edb 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module gitea.badhouseplants.net/softplayer/softplayer-backend go 1.25.9 require ( - github.com/alecthomas/assert/v2 v2.11.0 github.com/alecthomas/kong v1.15.0 github.com/db-operator/can-haz-password v0.1.1 github.com/go-logr/logr v1.4.3 @@ -21,12 +20,10 @@ require ( ) require ( - github.com/alecthomas/repr v0.5.2 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/geozelot/intree v1.0.0 // indirect - github.com/hexops/gotextdiff v1.0.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -42,7 +39,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514173933-48bddcf5c686 github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index de6260e..ed898b9 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd h1:Qzvr5tAI909J38KNlx3KknzBAgXy5ZJS+0qzvRh/FGI= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514152254-9c02001a83fd/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514173933-48bddcf5c686 h1:tOSfg7VeD0Xq2NhVQblSiWGICvSH8RWfaaPH7mCvw0Y= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514173933-48bddcf5c686/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index 9f5b969..c673871 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -2,16 +2,21 @@ package controllers import ( "context" + "crypto/sha256" "database/sql" + "encoding/hex" "encoding/json" "errors" + "fmt" "regexp" + "testing" "time" - "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/token" "github.com/google/uuid" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/assert" "google.golang.org/grpc" ) @@ -20,6 +25,7 @@ type TokenController struct { HashCost int16 ServiceInfo map[string]grpc.ServiceInfo rules []rule + Redis *redis.Client } // Services that are not available for tokens @@ -27,7 +33,9 @@ var DisabledServicesRegex = []string{".*Accounts.*", ".*Tokens.*"} // Errors var ( - ErrServerError = errors.New("internal server error") + ErrServerError = errors.New("internal server error") + ErrUserTokenMismatch = errors.New("user doesn't match the token") + ErrBadToken = errors.New("bad token") ) type TokenData struct { @@ -58,6 +66,36 @@ func (ctrl *TokenController) SetRules() { ctrl.rules = rules } +// Each token operation must first verify that the current user +// is allowed to manipulate the token. +func (ctrl *TokenController) VerifyTokenOwner(ctx context.Context, userID, tokenID string) error { + log := logger.FromContext(ctx).WithValues("uuid", tokenID, "user_id", userID) + log.V(2).Info("Verifying the token owner") + + // First try to get from the redis + redisKey := fmt.Sprintf("token:%s", tokenID) + realUserID := ctrl.Redis.Get(ctx, redisKey).Val() + // If not found in cache, get from postgres + if realUserID == "" { + query := "SELECT user_id FROM tokens WHERE uuid = $1;" + if err := ctrl.DB.QueryRowContext(ctx, query, tokenID).Scan( + &realUserID, + ); err != nil { + log.Error(err, "Couldn't get user_id for a token") + return ErrServerError + } + } + + if realUserID != userID { + return ErrUserTokenMismatch + } + err := ctrl.Redis.Set(ctx, redisKey, realUserID, time.Hour) + if err != nil { + log.Info("Couldn't write to cache", "error", err) + } + return nil +} + // Create a new token, store its hash in the database and return the token value func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (string, error) { id := uuid.NewString() @@ -70,11 +108,7 @@ func (ctrl *TokenController) Create(ctx context.Context, data *TokenData) (strin return "", ErrServerError } - tokenHash, err := hash.HashPassword(tokenValue, int(ctrl.HashCost)) - if err != nil { - log.Error(err, "Couldn't calculate token hash") - return "", ErrServerError - } + tokenHash := hashSHA256(tokenValue) scopesJson, err := json.Marshal(data.Scopes) if err != nil { @@ -121,8 +155,6 @@ func (ctrl *TokenController) Update(ctx context.Context, data *TokenData) error return ErrServerError } - data, err := ctrl.Get(ctx, , id string, userID string) - return nil } @@ -151,11 +183,7 @@ func (ctrl *TokenController) Regenerate(ctx context.Context, id string) (string, return "", ErrServerError } - tokenHash, err := hash.HashPassword(tokenValue, int(ctrl.HashCost)) - if err != nil { - log.Error(err, "Couldn't calculate token hash") - return "", ErrServerError - } + tokenHash := hashSHA256(tokenValue) query := ` UPDATE tokens @@ -301,3 +329,57 @@ func shouldSkip(s string, rules []rule) bool { } return false } + +func (ctrl *TokenController) AuthenticateWithToken(ctx context.Context, token string) (map[string][]string, error) { + log := logger.FromContext(ctx) + log.V(2).Info("Authenticating with a token") + + query := ` + SELECT scopes, expires_at, revoked_at + FROM tokens + WHERE token_hash = $1` + + var expiresAt sql.NullTime + var revokedAt sql.NullTime + var scopes string + fmt.Println(hashSHA256(token)) + fmt.Println(hashSHA256(token)) + if err := ctrl.DB.QueryRowContext(ctx, query, hashSHA256(token)).Scan( + &scopes, + &expiresAt, + &revokedAt, + ); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, err + } + log.Error(err, "Couldn't find a token") + return nil, ErrServerError + } + + if !revokedAt.Valid { + return nil, ErrBadToken + } + + if expiresAt.Time.After(time.Now()) { + return nil, ErrBadToken + } + + scopesMap := map[string][]string{} + + if err := json.Unmarshal([]byte(scopes), scopesMap); err != nil { + return nil, ErrServerError + } + return scopesMap, nil +} + +func hashSHA256(s string) string { + hash := sha256.Sum256([]byte(s)) + return hex.EncodeToString(hash[:]) +} + +func TestUnitHashPersistence(t *testing.T) { + password := "qwertyu9" + hash1 := hashSHA256(password) + hash2 := hashSHA256(password) + assert.Equal(t, hash1, hash2) +} diff --git a/internal/helpers/hash/hash_test.go b/internal/helpers/hash/hash_test.go index 2714721..5130f34 100644 --- a/internal/helpers/hash/hash_test.go +++ b/internal/helpers/hash/hash_test.go @@ -4,17 +4,17 @@ import ( "testing" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" - "github.com/alecthomas/assert/v2" + "github.com/stretchr/testify/assert" ) -func TestHashValid(t *testing.T) { +func TestUnitHashValid(t *testing.T) { password := "qwertyu9" hpass, err := hash.HashPassword(password, 10) assert.NoError(t, err) assert.NoError(t, hash.CheckPasswordHash(password, hpass)) } -func TestHashInvalid(t *testing.T) { +func TestUnitHashInvalid(t *testing.T) { password := "qwertyu9" invhash := "qwertyu9" assert.Error(t, hash.CheckPasswordHash(password, invhash)) -- 2.49.1 From d954491893b12c38baedc27bef0f2e4ba83fbc40 Mon Sep 17 00:00:00 2001 From: Nikolai Rodionov Date: Fri, 15 May 2026 14:51:21 +0200 Subject: [PATCH 8/8] Token authorization is ready for MVP Signed-off-by: Nikolai Rodionov --- api/v1/accounts.go | 13 ++++- api/v1/public_accounts.go | 24 ++++++-- api/v1/public_tokens.go | 64 ++++++++++++++++++++++ api/v1/tokens.go | 16 ------ cmd/server.go | 13 +++-- go.mod | 2 +- go.sum | 4 +- internal/controllers/authorization.go | 59 ++++++++++++++++---- internal/controllers/authorization_test.go | 20 ++++++- internal/controllers/tokens.go | 31 ++++++----- 10 files changed, 188 insertions(+), 58 deletions(-) create mode 100644 api/v1/public_tokens.go diff --git a/api/v1/accounts.go b/api/v1/accounts.go index e268f3a..95de164 100644 --- a/api/v1/accounts.go +++ b/api/v1/accounts.go @@ -48,16 +48,23 @@ func (a *AccountsServer) RefreshToken(ctx context.Context, in *empty.Empty) (*em return nil, status.Error(codes.Unauthenticated, "Invalid session") } - accessToken, _, err := a.authorizationCtrl.GenerateToken(session.UserID, controllers.TokenTypeAccess) + accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ + UserID: claims.UserID, + TokenType: controllers.TokenTypeAccess, + TokenAud: controllers.TokenAudWeb, + }) if err != nil { return nil, status.Error(codes.Aborted, "Couldn't generate an access token") } - refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(session.UserID, controllers.TokenTypeRefresh) + refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ + UserID: claims.UserID, + TokenType: controllers.TokenTypeRefresh, + TokenAud: controllers.TokenAudWeb, + }) if err != nil { return nil, status.Error(codes.Aborted, "Couldn't generate an access token") } - newSession := &controllers.Session{UserID: session.UserID} if err := a.authorizationCtrl.SaveSession(ctx, tokenID, newSession); err != nil { diff --git a/api/v1/public_accounts.go b/api/v1/public_accounts.go index 9afd2dd..8ec563e 100644 --- a/api/v1/public_accounts.go +++ b/api/v1/public_accounts.go @@ -34,12 +34,20 @@ func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRe if err != nil { return nil, status.Error(codes.Aborted, "Couldn't create a user") } - accessToken, _, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeAccess) + accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ + UserID: id, + TokenType: controllers.TokenTypeAccess, + TokenAud: controllers.TokenAudWeb, + }) if err != nil { return nil, status.Error(codes.Aborted, "Couldn't generate an access token") } - refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeRefresh) + refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ + UserID: id, + TokenType: controllers.TokenTypeRefresh, + TokenAud: controllers.TokenAudWeb, + }) if err != nil { return nil, status.Error(codes.Aborted, "Couldn't generate an access token") } @@ -70,12 +78,20 @@ func (a *PublicAccountService) SignUp(ctx context.Context, in *accounts.SignUpRe return nil, status.Error(codes.Aborted, "Couldn't create a user") } - accessToken, _, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeAccess) + accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ + UserID: id, + TokenType: controllers.TokenTypeAccess, + TokenAud: controllers.TokenAudWeb, + }) if err != nil { return nil, status.Error(codes.Aborted, "Couldn't generate an access token") } - refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeRefresh) + refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ + UserID: id, + TokenType: controllers.TokenTypeRefresh, + TokenAud: controllers.TokenAudWeb, + }) if err != nil { return nil, status.Error(codes.Aborted, "Couldn't generate an access token") } diff --git a/api/v1/public_tokens.go b/api/v1/public_tokens.go new file mode 100644 index 0000000..91e6075 --- /dev/null +++ b/api/v1/public_tokens.go @@ -0,0 +1,64 @@ +package v1 + +import ( + "context" + "errors" + + "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" + tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +// var _ tokens.PublicTokensServiceServer = (*PublicTokensServer)(nil) + +type PublicTokensServer struct { + tokens.UnimplementedPublicTokensServiceServer + tokenCtrl *controllers.TokenController + authorizationCtrl *controllers.AuthController +} + +func NewPublicTokensServer( + tokenCtrl *controllers.TokenController, + authorizationCtrl *controllers.AuthController, +) *PublicTokensServer { + return &PublicTokensServer{ + tokenCtrl: tokenCtrl, + authorizationCtrl: authorizationCtrl, + } +} + +func (srv *PublicTokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) { + tokenAuthRes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token) + if err != nil { + if errors.Is(err, controllers.ErrBadToken) { + return nil, status.Error(codes.Unauthenticated, "Token is not valid") + } + if errors.Is(err, controllers.ErrServerError) { + return nil, status.Error(codes.Internal, "Something is broken on our side") + } + return nil, status.Error(codes.Aborted, "Couldn't list tokens") + } + + jwtData := &controllers.JWTData{ + UserID: tokenAuthRes.UserID, + TokenType: controllers.TokenTypeAccess, + TokenAud: controllers.TokenAudToken, + Scope: tokenAuthRes.Scope, + } + accessToken, _, err := srv.authorizationCtrl.GenerateToken(jwtData) + if err != nil { + return nil, status.Error(codes.Aborted, "Couldn't generate an access token") + } + + header := metadata.New(map[string]string{ + "X-Access-Token": accessToken, + }) + if err := grpc.SetHeader(ctx, header); err != nil { + return nil, status.Error(codes.Aborted, "Couldn't set metadata") + } + return &emptypb.Empty{}, nil +} diff --git a/api/v1/tokens.go b/api/v1/tokens.go index 937e482..f6aec00 100644 --- a/api/v1/tokens.go +++ b/api/v1/tokens.go @@ -3,7 +3,6 @@ package v1 import ( "context" "errors" - "fmt" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" @@ -261,18 +260,3 @@ func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerSt } return nil } - -func (srv *TokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) { - scopes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token) - if err != nil { - if errors.Is(err, controllers.ErrBadToken) { - return nil, status.Error(codes.Unauthenticated, "Token is not valid") - } - if errors.Is(err, controllers.ErrServerError) { - return nil, status.Error(codes.Internal, "Something is broken on our side") - } - return nil, status.Error(codes.Aborted, "Couldn't list tokens") - } - fmt.Println(scopes) - return nil, nil -} diff --git a/cmd/server.go b/cmd/server.go index ed97fc4..a158f07 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -67,7 +67,7 @@ func (cmd *Server) Run(ctx context.Context) error { Addr: cmd.RedisHost, }) - authInterceptor := controllers.NewAuthController( + authController := controllers.NewAuthController( []byte(cmd.JWTSecret), cmd.AccessTokenTTL, cmd.RefrestTokenTTL, @@ -79,14 +79,14 @@ func (cmd *Server) Run(ctx context.Context) error { grpc_zap.UnaryServerInterceptor(logger.SetupLogger("info")), // jwtVerifier.JWTAuthInterceptor, selector.UnaryServerInterceptor( - auth.UnaryServerInterceptor(authInterceptor.AuthInterceptorFN), + auth.UnaryServerInterceptor(authController.AuthInterceptorFN), selector.MatchFunc(selectorRequireAuth), ), ), grpc.ChainStreamInterceptor( grpc_zap.StreamServerInterceptor(logger.SetupLogger("info")), selector.StreamServerInterceptor( - auth.StreamServerInterceptor(authInterceptor.AuthInterceptorFN), + auth.StreamServerInterceptor(authController.AuthInterceptorFN), selector.MatchFunc(selectorRequireAuth), ), ), @@ -113,11 +113,12 @@ func (cmd *Server) Run(ctx context.Context) error { } // Services that should be accessible for tokens should go here - accounts.RegisterAccountsServiceServer(grpcServer, v1.NewAccountServer(accountCtrl, authInterceptor)) + accounts.RegisterAccountsServiceServer(grpcServer, v1.NewAccountServer(accountCtrl, authController)) test.RegisterTestServiceServer(grpcServer, v1.NewTestServer()) test.RegisterPublicTestServiceServer(grpcServer, v1.NewPublicTestServer()) - tokens.RegisterTokensServiceServer(grpcServer, v1.NewTokensServer(tokenCtrl, authInterceptor)) - accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authInterceptor)) + tokens.RegisterTokensServiceServer(grpcServer, v1.NewTokensServer(tokenCtrl, authController)) + tokens.RegisterPublicTokensServiceServer(grpcServer, v1.NewPublicTokensServer(tokenCtrl, authController)) + accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authController)) info := grpcServer.GetServiceInfo() tokenCtrl.SetGRPCInfo(info) diff --git a/go.mod b/go.mod index ec03edb..60f6fdf 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( ) require ( - gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514173933-48bddcf5c686 + gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260515083721-50411957979f github.com/golang/protobuf v1.5.4 golang.org/x/net v0.49.0 // indirect golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index ed898b9..cb2407e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514173933-48bddcf5c686 h1:tOSfg7VeD0Xq2NhVQblSiWGICvSH8RWfaaPH7mCvw0Y= -gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260514173933-48bddcf5c686/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260515083721-50411957979f h1:o+GpCFtuw59LrWw9ZkWQOXhQxJjLaJYM+uZm0gdtrRI= +gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260515083721-50411957979f/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/internal/controllers/authorization.go b/internal/controllers/authorization.go index 7a6be33..07f14a3 100644 --- a/internal/controllers/authorization.go +++ b/internal/controllers/authorization.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "strings" "time" @@ -22,6 +23,8 @@ type TokenType string const ( TokenTypeAccess TokenType = "access" TokenTypeRefresh TokenType = "refresh" + TokenAudToken string = "token" + TokenAudWeb string = "web" ) var ( @@ -33,6 +36,7 @@ type Claims struct { UserID string `json:"user_id"` TokenID string `json:"token_id"` TokenType TokenType `json:"token_type"` + Scope string `json:"scope,omitempty"` jwt.RegisteredClaims } @@ -56,6 +60,13 @@ func NewAuthController(jwtSecret []byte, accessTTL, refreshTTL time.Duration, re } } +type JWTData struct { + UserID string + TokenType TokenType + TokenAud string + Scope string +} + // Write claims into context func (a *AuthController) WithClaims(ctx context.Context, claims *Claims) context.Context { return context.WithValue(ctx, claimsContextKey, claims) @@ -87,15 +98,43 @@ func (a *AuthController) AuthInterceptorFN(ctx context.Context) (context.Context } } + // If it's a cli token, we need to check the scope + if slices.Contains(claims.Audience, TokenAudToken) { + currentMethod, ok := grpc.Method(ctx) + if !ok { + return nil, errors.New("unknown method") + } + + scopeMap := map[string][]string{} + if err := json.Unmarshal([]byte(claims.Scope), &scopeMap); err != nil { + return nil, ErrServerError + } + allowed := isAllowed(scopeMap, currentMethod) + if !allowed { + return nil, errors.New("not authorized") + } + } + ctx = a.WithClaims(ctx, claims) return ctx, nil } +func isAllowed(scope map[string][]string, currentMethod string) bool { + for service, methods := range scope { + for _, method := range methods { + if fmt.Sprintf("/%s/%s", service, method) == currentMethod { + return true + } + } + } + return false +} + // Generate JWT token -func (a *AuthController) GenerateToken(userID string, tokenType TokenType) (token, tokenID string, err error) { +func (a *AuthController) GenerateToken(data *JWTData) (token, tokenID string, err error) { var expiresAt time.Time notBefore := time.Now() - switch tokenType { + switch data.TokenType { case TokenTypeAccess: expiresAt = time.Now().Add(a.accessTTL) case TokenTypeRefresh: @@ -103,25 +142,25 @@ func (a *AuthController) GenerateToken(userID string, tokenType TokenType) (toke default: return "", "", ErrUnknownTokenType } - if tokenType != TokenTypeAccess && tokenType != TokenTypeRefresh { - return "", "", ErrUnknownTokenType - } tokenID = uuid.New().String() + claims := Claims{ - UserID: userID, + UserID: data.UserID, TokenID: tokenID, - TokenType: tokenType, + TokenType: data.TokenType, + Scope: data.Scope, RegisteredClaims: jwt.RegisteredClaims{ Issuer: "", - Subject: "", - Audience: jwt.ClaimStrings{}, + Subject: data.UserID, + Audience: jwt.ClaimStrings{data.TokenAud}, ExpiresAt: jwt.NewNumericDate(expiresAt), NotBefore: jwt.NewNumericDate(notBefore), IssuedAt: jwt.NewNumericDate(time.Now()), - ID: userID, + ID: tokenID, }, } + tokenJwt := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token, err = tokenJwt.SignedString(a.jwtSecret) if err != nil { diff --git a/internal/controllers/authorization_test.go b/internal/controllers/authorization_test.go index face84a..567014a 100644 --- a/internal/controllers/authorization_test.go +++ b/internal/controllers/authorization_test.go @@ -16,16 +16,26 @@ var ( ) func TestGenerateInvalidTokenType(t *testing.T) { + data := &controllers.JWTData{ + UserID: testUserID, + TokenType: "invalid_type", + } + authCtrl := controllers.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil) - token, _, err := authCtrl.GenerateToken(testUserID, "invalid_type") + + token, _, err := authCtrl.GenerateToken(data) assert.Equal(t, "", token) assert.ErrorIs(t, controllers.ErrUnknownTokenType, err) } func TestGenerateValidateAccessToken(t *testing.T) { + data := &controllers.JWTData{ + UserID: testUserID, + TokenType: controllers.TokenTypeAccess, + } authCtrl := controllers.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil) now := time.Now() - token, _, err := authCtrl.GenerateToken(testUserID, controllers.TokenTypeAccess) + token, _, err := authCtrl.GenerateToken(data) assert.NoError(t, err) assert.NotEmpty(t, token) @@ -40,9 +50,13 @@ func TestGenerateValidateAccessToken(t *testing.T) { } func TestGenerateValidateRefreshToken(t *testing.T) { + data := &controllers.JWTData{ + UserID: testUserID, + TokenType: controllers.TokenTypeRefresh, + } authCtrl := controllers.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil) now := time.Now() - token, _, err := authCtrl.GenerateToken(testUserID, controllers.TokenTypeRefresh) + token, _, err := authCtrl.GenerateToken(data) assert.NoError(t, err) assert.NotEmpty(t, token) diff --git a/internal/controllers/tokens.go b/internal/controllers/tokens.go index c673871..7a4d772 100644 --- a/internal/controllers/tokens.go +++ b/internal/controllers/tokens.go @@ -330,22 +330,27 @@ func shouldSkip(s string, rules []rule) bool { return false } -func (ctrl *TokenController) AuthenticateWithToken(ctx context.Context, token string) (map[string][]string, error) { +type TokenAuthResult struct { + UserID string + Scope string +} + +func (ctrl *TokenController) AuthenticateWithToken(ctx context.Context, token string) (*TokenAuthResult, error) { log := logger.FromContext(ctx) log.V(2).Info("Authenticating with a token") query := ` - SELECT scopes, expires_at, revoked_at + SELECT user_id, scopes, expires_at, revoked_at FROM tokens WHERE token_hash = $1` + var userID string var expiresAt sql.NullTime var revokedAt sql.NullTime - var scopes string - fmt.Println(hashSHA256(token)) - fmt.Println(hashSHA256(token)) + var scope string if err := ctrl.DB.QueryRowContext(ctx, query, hashSHA256(token)).Scan( - &scopes, + &userID, + &scope, &expiresAt, &revokedAt, ); err != nil { @@ -356,20 +361,20 @@ func (ctrl *TokenController) AuthenticateWithToken(ctx context.Context, token st return nil, ErrServerError } - if !revokedAt.Valid { + if revokedAt.Valid { return nil, ErrBadToken } - if expiresAt.Time.After(time.Now()) { + if expiresAt.Time.Before(time.Now()) { return nil, ErrBadToken } - scopesMap := map[string][]string{} - - if err := json.Unmarshal([]byte(scopes), scopesMap); err != nil { - return nil, ErrServerError + result := &TokenAuthResult{ + UserID: userID, + Scope: scope, } - return scopesMap, nil + + return result, nil } func hashSHA256(s string) string { -- 2.49.1