Skip to content

Commit

Permalink
fix(satori): Add 'IssuedAt' to Nakama token and reuse it for Satori
Browse files Browse the repository at this point in the history
  • Loading branch information
flaviofernandes004 committed Oct 25, 2024
1 parent 0930eb8 commit 545e8ed
Show file tree
Hide file tree
Showing 12 changed files with 142 additions and 83 deletions.
9 changes: 9 additions & 0 deletions internal/ctx_env/ctx_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ctx_env

// 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 struct{}
type CtxTokenIssuedAtKey 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/ctx_env"
"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(ctx_env.CtxTokenIDKey{}).(string)
tIssuedAt, _ := ctx.Value(ctx_env.CtxTokenIssuedAtKey{}).(int64)
tExpirySec, _ := ctx.Value(ctx_env.CtxExpiryKey{}).(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 {
// 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/ctx_env"
"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 = ctx_env.CtxUserIDKey
type ctxUsernameKey = ctx_env.CtxUsernameKey
type ctxVarsKey = ctx_env.CtxVarsKey
type ctxExpiryKey = ctx_env.CtxExpiryKey
type ctxTokenIDKey = ctx_env.CtxTokenIDKey
type ctxTokenIssuedAtKey = ctx_env.CtxTokenIssuedAtKey

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
6 changes: 3 additions & 3 deletions server/api_rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ func (s *ApiServer) RpcFuncHttp(w http.ResponseWriter, r *http.Request) {
return
}
} else {
var token string
userID, username, vars, expiry, token, isTokenAuth = parseBearerAuth([]byte(s.config.GetSession().EncryptionKey), auth[0])
if !isTokenAuth || !s.sessionCache.IsValidSession(userID, expiry, token) {
var tokenId string
userID, username, vars, expiry, tokenId, _, isTokenAuth = parseBearerAuth([]byte(s.config.GetSession().EncryptionKey), auth[0])
if !isTokenAuth || !s.sessionCache.IsValidSession(userID, expiry, tokenId) {
// Auth token not valid or expired.
w.Header().Set("content-type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
Expand Down
Loading

0 comments on commit 545e8ed

Please sign in to comment.