Signed-off-by: Nikolai Rodionov <allanger@badhouseplants.net>
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
|
||||
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"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"
|
||||
)
|
||||
|
||||
func NewAccountAuthRPCImpl(ctrl *controllers.AccountController) *AccountsAuthServer {
|
||||
@@ -15,3 +24,36 @@ type AccountsAuthServer struct {
|
||||
accounts.UnimplementedAccountsAuthServiceServer
|
||||
ctrl *controllers.AccountController
|
||||
}
|
||||
|
||||
func (a *AccountsAuthServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.Empty, error) {
|
||||
tokenID := ctx.Value("token_id").(string)
|
||||
userID := ctx.Value("user_id").(string)
|
||||
log := logger.FromContext(ctx)
|
||||
uuid, err := a.ctrl.ValidateRefreshToken(ctx, tokenID, userID)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "refresh token is invalid")
|
||||
}
|
||||
accessToken, err := a.ctrl.GenerateAccessToken(uuid)
|
||||
if err != nil {
|
||||
log.Error(err, "Couldn't generate an access token")
|
||||
return nil, status.Error(codes.Aborted, "Couldn't generate Access Token")
|
||||
}
|
||||
|
||||
refreshToken, err := a.ctrl.GenerateRefreshToken(ctx, uuid)
|
||||
if err != nil {
|
||||
log.Error(err, "Couldn't generate a refresh token")
|
||||
return nil, status.Error(codes.Aborted, "Couldn't generate Access Token")
|
||||
}
|
||||
|
||||
header := metadata.Pairs(
|
||||
"access-token", accessToken,
|
||||
"refreshToken", refreshToken,
|
||||
)
|
||||
|
||||
if err := grpc.SetHeader(ctx, header); err != nil {
|
||||
log.Error(err, "Couldn't set headers")
|
||||
return nil, status.Error(codes.Unknown, "Couldn't set headers")
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
@@ -76,41 +76,3 @@ func (a *AccountsNoAuthServer) ResetPassword(ctx context.Context, in *accounts.R
|
||||
func (acc *AccountsNoAuthServer) NewPassword(ctx context.Context, in *accounts.NewPasswordRequest) (*empty.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "Endpoint is not implemented")
|
||||
}
|
||||
|
||||
func (a *AccountsNoAuthServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.Empty, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Unauthenticated, "User is not authorized")
|
||||
}
|
||||
|
||||
tokenString := md.Get("token")[0]
|
||||
uuid, err := a.ctrl.ValidateRefreshToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "refresh token is invalid")
|
||||
}
|
||||
accessToken, err := a.ctrl.GenerateAccessToken(uuid)
|
||||
if err != nil {
|
||||
log.Error(err, "Couldn't generate an access token")
|
||||
return nil, status.Error(codes.Aborted, "Couldn't generate Access Token")
|
||||
}
|
||||
|
||||
refreshToken, err := a.ctrl.GenerateRefreshToken(ctx, uuid)
|
||||
if err != nil {
|
||||
log.Error(err, "Couldn't generate a refresh token")
|
||||
return nil, status.Error(codes.Aborted, "Couldn't generate Access Token")
|
||||
}
|
||||
|
||||
header := metadata.Pairs(
|
||||
"access-token", accessToken,
|
||||
"refreshToken", refreshToken,
|
||||
)
|
||||
|
||||
if err := grpc.SetHeader(ctx, header); err != nil {
|
||||
log.Error(err, "Couldn't set headers")
|
||||
return nil, status.Error(codes.Unknown, "Couldn't set headers")
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
@@ -137,7 +138,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260428111006-efa5c57e6a14
|
||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260428191411-8c93fd05025a
|
||||
github.com/golang/protobuf v1.5.4
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
|
||||
9
go.sum
9
go.sum
@@ -1,10 +1,13 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
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=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260428111006-efa5c57e6a14 h1:PwOWag8dum67a1w/QIP7NlSGPL/Z7rZDHAwjRJjyk3U=
|
||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260428111006-efa5c57e6a14/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg=
|
||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260428191411-8c93fd05025a h1:8Mo14FqMkUcUNyyVfrTGZOe/BHn412olquEDqL+Fg6c=
|
||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260428191411-8c93fd05025a/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
@@ -182,6 +185,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash"
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -64,6 +65,10 @@ func (c *AccountController) GenerateAccessToken(userID string) (string, error) {
|
||||
return token.SignedString(c.JWTSecret)
|
||||
}
|
||||
|
||||
func redisKey(id string) string {
|
||||
return fmt.Sprintf("refresh:%s", id)
|
||||
}
|
||||
|
||||
func (c *AccountController) GenerateRefreshToken(ctx context.Context, userID string) (string, error) {
|
||||
tokenID := uuid.New().String()
|
||||
claims := jwt.MapClaims{
|
||||
@@ -74,8 +79,7 @@ func (c *AccountController) GenerateRefreshToken(ctx context.Context, userID str
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
redisKey := fmt.Sprintf("refresh_token:%s", tokenID)
|
||||
if err := c.Redis.Set(ctx, redisKey, userID, c.RefreshTokenTTL).Err(); err != nil {
|
||||
if err := c.Redis.Set(ctx, redisKey(tokenID), userID, c.RefreshTokenTTL).Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token.SignedString(c.JWTSecret)
|
||||
@@ -84,28 +88,15 @@ func (c *AccountController) GenerateRefreshToken(ctx context.Context, userID str
|
||||
// It must validate the refresh token
|
||||
// Get it's id from the content
|
||||
// Find a corresponding token in redis, and if it's found, remove it and create a new one
|
||||
func (c *AccountController) ValidateRefreshToken(ctx context.Context, tokenString string) (string, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
|
||||
return c.JWTSecret, nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var tokenID string
|
||||
var userID string
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
tokenID = claims["token_id"].(string)
|
||||
userID = claims["user_id"].(string)
|
||||
} else {
|
||||
return "", errors.New("token id is not set")
|
||||
}
|
||||
|
||||
userIDRedis := c.Redis.Get(ctx, tokenID).String()
|
||||
if c.Redis.Del(ctx, tokenID).Err() != nil {
|
||||
func (c *AccountController) ValidateRefreshToken(ctx context.Context, tokenID, userID string) (string, error) {
|
||||
log := logger.FromContext(ctx)
|
||||
userIDRedis := c.Redis.Get(ctx, redisKey(tokenID)).Val()
|
||||
if err := c.Redis.Del(ctx, redisKey(tokenID)).Err(); err != nil {
|
||||
log.Error(err, "Couldn't delete redis entry")
|
||||
return "", err
|
||||
}
|
||||
log.Info(userIDRedis)
|
||||
log.Info(userID)
|
||||
if userID != userIDRedis {
|
||||
return "", errors.New("user id doesn't match")
|
||||
}
|
||||
|
||||
37
main.go
37
main.go
@@ -6,21 +6,27 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "gitea.badhouseplants.net/softplayer/softplayer-backend/api/v1"
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/interceptors"
|
||||
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
|
||||
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
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"
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector"
|
||||
_ "github.com/lib/pq"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
@@ -154,12 +160,37 @@ func server(ctx context.Context, params Serve) error {
|
||||
return err
|
||||
}
|
||||
|
||||
jwtVerifier := interceptors.NewJWTVerifier(ctx, []byte(params.JWTSecret))
|
||||
// jwtVerifier := interceptors.NewJWTVerifier(ctx, []byte(params.JWTSecret))
|
||||
|
||||
authFn := func(ctx context.Context) (context.Context, error) {
|
||||
tokenString, err := auth.AuthFromMD(ctx, "bearer")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
|
||||
return []byte(params.JWTSecret), nil
|
||||
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "User is not authorized")
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(jwt.MapClaims); ok {
|
||||
ctx = context.WithValue(ctx, "token_id", claims["token_id"].(string))
|
||||
ctx = context.WithValue(ctx, "user_id", claims["user_id"].(string))
|
||||
} else {
|
||||
return ctx, errors.New("claims are missing int the token")
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
authReqServices := func(ctx context.Context, callMeta interceptors.CallMeta) bool {
|
||||
return !strings.Contains(callMeta.Service, "NoAuth")
|
||||
}
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(
|
||||
grpc_zap.UnaryServerInterceptor(logger.SetupLogger("info")),
|
||||
jwtVerifier.JWTAuthInterceptor,
|
||||
// jwtVerifier.JWTAuthInterceptor,
|
||||
selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(authFn), selector.MatchFunc(authReqServices)),
|
||||
),
|
||||
grpc.StreamInterceptor(grpc_zap.StreamServerInterceptor(logger.SetupLogger("info"))),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user