Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(satori): Add 'IssuedAt' to Nakama token and reuse it for Satori #1278

Merged
merged 4 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-compose-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ services:
interval: 5s

test:
image: "golang:1.21"
image: "golang:1.23.2"
command: /bin/sh -c "mkdir -p /nakama/internal/gopher-lua/_lua5.1-tests/libs/P1; go test -v -race ./..."

working_dir: "/nakama"
Expand Down
9 changes: 9 additions & 0 deletions internal/ctxkeys/ctxkeys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ctxkeys

// Keys used for storing/retrieving user information in the context of a request after authentication.
type UserIDKey struct{}
type UsernameKey struct{}
type VarsKey struct{}
type ExpiryKey struct{}
type TokenIDKey struct{}
type TokenIssuedAtKey struct{}
57 changes: 35 additions & 22 deletions internal/satori/satori.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,37 +30,38 @@ import (

"github.com/golang-jwt/jwt/v4"
"github.com/heroiclabs/nakama-common/runtime"
"github.com/heroiclabs/nakama/v3/internal/ctxkeys"
"go.uber.org/zap"
)

var _ runtime.Satori = &SatoriClient{}

type CtxTokenIDKey struct{}

type SatoriClient struct {
logger *zap.Logger
httpc *http.Client
url *url.URL
urlString string
apiKeyName string
apiKey string
signingKey string
tokenExpirySec int
invalidConfig bool
logger *zap.Logger
httpc *http.Client
url *url.URL
urlString string
apiKeyName string
apiKey string
signingKey string
tokenExpirySec int
nakamaTokenExpirySec int64
invalidConfig bool
}

func NewSatoriClient(logger *zap.Logger, satoriUrl, apiKeyName, apiKey, signingKey string) *SatoriClient {
func NewSatoriClient(logger *zap.Logger, satoriUrl, apiKeyName, apiKey, signingKey string, nakamaTokenExpirySec int64) *SatoriClient {
parsedUrl, _ := url.Parse(satoriUrl)

sc := &SatoriClient{
logger: logger,
urlString: satoriUrl,
httpc: &http.Client{Timeout: 2 * time.Second},
url: parsedUrl,
apiKeyName: strings.TrimSpace(apiKeyName),
apiKey: strings.TrimSpace(apiKey),
signingKey: strings.TrimSpace(signingKey),
tokenExpirySec: 3600,
logger: logger,
urlString: satoriUrl,
httpc: &http.Client{Timeout: 2 * time.Second},
url: parsedUrl,
apiKeyName: strings.TrimSpace(apiKeyName),
apiKey: strings.TrimSpace(apiKey),
signingKey: strings.TrimSpace(signingKey),
tokenExpirySec: 3600,
nakamaTokenExpirySec: nakamaTokenExpirySec,
}

if sc.urlString == "" && sc.apiKeyName == "" && sc.apiKey == "" && sc.signingKey == "" {
Expand Down Expand Up @@ -121,13 +122,25 @@ func (stc *sessionTokenClaims) Valid() error {
}

func (s *SatoriClient) generateToken(ctx context.Context, id string) (string, error) {
tid, _ := ctx.Value(CtxTokenIDKey{}).(string)
tid, _ := ctx.Value(ctxkeys.TokenIDKey{}).(string)
tIssuedAt, _ := ctx.Value(ctxkeys.TokenIssuedAtKey{}).(int64)
tExpirySec, _ := ctx.Value(ctxkeys.ExpiryKey{}).(int64)

timestamp := time.Now().UTC()
if tIssuedAt == 0 && tExpirySec > s.nakamaTokenExpirySec {
// Token was issued before 'IssuedAt' had been added to the session token.
// Thus Nakama will make a guess of that value.
tIssuedAt = tExpirySec - s.nakamaTokenExpirySec
} else if tIssuedAt == 0 {
// Unable to determine the token's issued at.
tIssuedAt = timestamp.Unix()
}

claims := sessionTokenClaims{
SessionID: tid,
IdentityId: id,
ExpiresAt: timestamp.Add(time.Duration(s.tokenExpirySec) * time.Second).Unix(),
IssuedAt: timestamp.Unix(),
IssuedAt: tIssuedAt,
ApiKeyName: s.apiKeyName,
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, &claims).SignedString([]byte(s.signingKey))
Expand Down
2 changes: 1 addition & 1 deletion internal/satori/satori_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestSatoriClient_EventsPublish(t *testing.T) {
identityID := uuid.Must(uuid.NewV4()).String()

logger := NewConsoleLogger(os.Stdout, true)
client := NewSatoriClient(logger, "<URL>", "<API KEY NAME>", "<API KEY>", "<SIGNING KEY>")
client := NewSatoriClient(logger, "<URL>", "<API KEY NAME>", "<API KEY>", "<SIGNING KEY>", 0)

ctx, ctxCancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer ctxCancelFn()
Expand Down
27 changes: 14 additions & 13 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import (
grpcgw "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/heroiclabs/nakama-common/api"
"github.com/heroiclabs/nakama/v3/apigrpc"
"github.com/heroiclabs/nakama/v3/internal/satori"
"github.com/heroiclabs/nakama/v3/internal/ctxkeys"
"github.com/heroiclabs/nakama/v3/social"
"go.uber.org/zap"
"google.golang.org/grpc"
Expand All @@ -61,11 +61,12 @@ var once sync.Once
const byteBracket byte = '{'

// Keys used for storing/retrieving user information in the context of a request after authentication.
type ctxUserIDKey struct{}
type ctxUsernameKey struct{}
type ctxVarsKey struct{}
type ctxExpiryKey struct{}
type ctxTokenIDKey = satori.CtxTokenIDKey
type ctxUserIDKey = ctxkeys.UserIDKey
type ctxUsernameKey = ctxkeys.UsernameKey
type ctxVarsKey = ctxkeys.VarsKey
type ctxExpiryKey = ctxkeys.ExpiryKey
type ctxTokenIDKey = ctxkeys.TokenIDKey
type ctxTokenIssuedAtKey = ctxkeys.TokenIssuedAtKey

type ctxFullMethodKey struct{}

Expand Down Expand Up @@ -430,15 +431,15 @@ func securityInterceptorFunc(logger *zap.Logger, config Config, sessionCache Ses
// Value of "authorization" or "grpc-authorization" was empty or repeated.
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
}
userID, username, vars, exp, tokenId, ok := parseBearerAuth([]byte(config.GetSession().EncryptionKey), auth[0])
userID, username, vars, exp, tokenId, tokenIssuedAt, ok := parseBearerAuth([]byte(config.GetSession().EncryptionKey), auth[0])
if !ok {
// Value of "authorization" or "grpc-authorization" was malformed or expired.
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
}
if !sessionCache.IsValidSession(userID, exp, tokenId) {
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
}
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp), ctxTokenIDKey{}, tokenId)
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp), ctxTokenIDKey{}, tokenId), ctxTokenIssuedAtKey{}, tokenIssuedAt)
default:
// Unless explicitly defined above, handlers require full user authentication.
md, ok := metadata.FromIncomingContext(ctx)
Expand All @@ -458,15 +459,15 @@ func securityInterceptorFunc(logger *zap.Logger, config Config, sessionCache Ses
// Value of "authorization" or "grpc-authorization" was empty or repeated.
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
}
userID, username, vars, exp, tokenId, ok := parseBearerAuth([]byte(config.GetSession().EncryptionKey), auth[0])
userID, username, vars, exp, tokenId, tokenIssuedAt, ok := parseBearerAuth([]byte(config.GetSession().EncryptionKey), auth[0])
if !ok {
// Value of "authorization" or "grpc-authorization" was malformed or expired.
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
}
if !sessionCache.IsValidSession(userID, exp, tokenId) {
return nil, status.Error(codes.Unauthenticated, "Auth token invalid")
}
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp), ctxTokenIDKey{}, tokenId)
ctx = context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(context.WithValue(ctx, ctxUserIDKey{}, userID), ctxUsernameKey{}, username), ctxVarsKey{}, vars), ctxExpiryKey{}, exp), ctxTokenIDKey{}, tokenId), ctxTokenIssuedAtKey{}, tokenIssuedAt)
}
return context.WithValue(ctx, ctxFullMethodKey{}, info.FullMethod), nil
}
Expand All @@ -491,7 +492,7 @@ func parseBasicAuth(auth string) (username, password string, ok bool) {
return cs[:s], cs[s+1:], true
}

func parseBearerAuth(hmacSecretByte []byte, auth string) (userID uuid.UUID, username string, vars map[string]string, exp int64, tokenId string, ok bool) {
func parseBearerAuth(hmacSecretByte []byte, auth string) (userID uuid.UUID, username string, vars map[string]string, exp int64, tokenId string, issuedAt int64, ok bool) {
if auth == "" {
return
}
Expand All @@ -502,7 +503,7 @@ func parseBearerAuth(hmacSecretByte []byte, auth string) (userID uuid.UUID, user
return parseToken(hmacSecretByte, auth[len(prefix):])
}

func parseToken(hmacSecretByte []byte, tokenString string) (userID uuid.UUID, username string, vars map[string]string, exp int64, tokenId string, ok bool) {
func parseToken(hmacSecretByte []byte, tokenString string) (userID uuid.UUID, username string, vars map[string]string, exp int64, tokenId string, issuedAt int64, ok bool) {
jwtToken, err := jwt.ParseWithClaims(tokenString, &SessionTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
if s, ok := token.Method.(*jwt.SigningMethodHMAC); !ok || s.Hash != crypto.SHA256 {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
Expand All @@ -520,7 +521,7 @@ func parseToken(hmacSecretByte []byte, tokenString string) (userID uuid.UUID, us
if err != nil {
return
}
return userID, claims.Username, claims.Vars, claims.ExpiresAt, claims.TokenId, true
return userID, claims.Username, claims.Vars, claims.ExpiresAt, claims.TokenId, claims.IssuedAt, true
}

func decompressHandler(logger *zap.Logger, h http.Handler) http.HandlerFunc {
Expand Down
57 changes: 34 additions & 23 deletions server/api_authenticate.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type SessionTokenClaims struct {
Username string `json:"usn,omitempty"`
Vars map[string]string `json:"vrs,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
}

func (stc *SessionTokenClaims) Valid() error {
Expand Down Expand Up @@ -107,9 +108,10 @@ func (s *ApiServer) AuthenticateApple(ctx context.Context, in *api.AuthenticateA
s.sessionCache.RemoveAll(uuid.Must(uuid.FromString(dbUserID)))
}

tokenIssuedAt := time.Now().Unix()
tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -179,8 +181,9 @@ func (s *ApiServer) AuthenticateCustom(ctx context.Context, in *api.Authenticate
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -250,8 +253,9 @@ func (s *ApiServer) AuthenticateDevice(ctx context.Context, in *api.Authenticate
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -351,8 +355,9 @@ func (s *ApiServer) AuthenticateEmail(ctx context.Context, in *api.AuthenticateE
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, username, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, username, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, username, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, username, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -423,8 +428,9 @@ func (s *ApiServer) AuthenticateFacebook(ctx context.Context, in *api.Authentica
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -490,8 +496,9 @@ func (s *ApiServer) AuthenticateFacebookInstantGame(ctx context.Context, in *api
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -569,8 +576,9 @@ func (s *ApiServer) AuthenticateGameCenter(ctx context.Context, in *api.Authenti
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -636,8 +644,9 @@ func (s *ApiServer) AuthenticateGoogle(ctx context.Context, in *api.Authenticate
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand Down Expand Up @@ -712,8 +721,9 @@ func (s *ApiServer) AuthenticateSteam(ctx context.Context, in *api.AuthenticateS
}

tokenID := uuid.Must(uuid.NewV4()).String()
token, exp := generateToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, dbUserID, dbUsername, in.Account.Vars)
tokenIssuedAt := time.Now().Unix()
token, exp := generateToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
refreshToken, refreshExp := generateRefreshToken(s.config, tokenID, tokenIssuedAt, dbUserID, dbUsername, in.Account.Vars)
s.sessionCache.Add(uuid.FromStringOrNil(dbUserID), exp, tokenID, refreshExp, tokenID)
session := &api.Session{Created: created, Token: token, RefreshToken: refreshToken}

Expand All @@ -730,23 +740,24 @@ func (s *ApiServer) AuthenticateSteam(ctx context.Context, in *api.AuthenticateS
return session, nil
}

func generateToken(config Config, tokenID, userID, username string, vars map[string]string) (string, int64) {
func generateToken(config Config, tokenID string, tokenIssuedAt int64, userID, username string, vars map[string]string) (string, int64) {
exp := time.Now().UTC().Add(time.Duration(config.GetSession().TokenExpirySec) * time.Second).Unix()
return generateTokenWithExpiry(config.GetSession().EncryptionKey, tokenID, userID, username, vars, exp)
return generateTokenWithExpiry(config.GetSession().EncryptionKey, tokenID, tokenIssuedAt, userID, username, vars, exp)
}

func generateRefreshToken(config Config, tokenID, userID string, username string, vars map[string]string) (string, int64) {
func generateRefreshToken(config Config, tokenID string, tokenIssuedAt int64, userID string, username string, vars map[string]string) (string, int64) {
exp := time.Now().UTC().Add(time.Duration(config.GetSession().RefreshTokenExpirySec) * time.Second).Unix()
return generateTokenWithExpiry(config.GetSession().RefreshEncryptionKey, tokenID, userID, username, vars, exp)
return generateTokenWithExpiry(config.GetSession().RefreshEncryptionKey, tokenID, tokenIssuedAt, userID, username, vars, exp)
}

func generateTokenWithExpiry(signingKey, tokenID, userID, username string, vars map[string]string, exp int64) (string, int64) {
func generateTokenWithExpiry(signingKey, tokenID string, tokenIssuedAt int64, userID, username string, vars map[string]string, exp int64) (string, int64) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, &SessionTokenClaims{
TokenId: tokenID,
UserId: userID,
Username: username,
Vars: vars,
ExpiresAt: exp,
IssuedAt: tokenIssuedAt,
})
signedToken, _ := token.SignedString([]byte(signingKey))
return signedToken, exp
Expand Down
Loading
Loading