create a websocket game server
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// Package auth implements parsing and validation of the JWT tokens used by
|
||||
// the game server. Both the game token and the user token are HS256-signed
|
||||
// with the same shared secret (the main server's secret_key_base).
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// GameClaims is the payload of the game token issued by the main server
|
||||
// when a match is created.
|
||||
type GameClaims struct {
|
||||
MatchID string `json:"match_id"`
|
||||
Player1ID int64 `json:"player1_id"`
|
||||
Player2ID int64 `json:"player2_id"`
|
||||
GameServer string `json:"game_server"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// UserClaims is the payload of the personal token of a player.
|
||||
type UserClaims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func keyFunc(secret []byte) jwt.Keyfunc {
|
||||
return func(t *jwt.Token) (any, error) {
|
||||
if t.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||
return nil, fmt.Errorf("unexpected signing method: %s", t.Method.Alg())
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ParseGameToken verifies the signature and the expiration time of a game
|
||||
// token and returns its claims.
|
||||
func ParseGameToken(secret []byte, tokenString string) (*GameClaims, error) {
|
||||
claims := &GameClaims{}
|
||||
_, err := jwt.ParseWithClaims(tokenString, claims, keyFunc(secret),
|
||||
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
|
||||
jwt.WithExpirationRequired(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid game token: %w", err)
|
||||
}
|
||||
if claims.MatchID == "" {
|
||||
return nil, fmt.Errorf("invalid game token: empty match_id")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// ParseUserToken verifies the signature and the expiration time of a user
|
||||
// token and returns its claims.
|
||||
func ParseUserToken(secret []byte, tokenString string) (*UserClaims, error) {
|
||||
claims := &UserClaims{}
|
||||
_, err := jwt.ParseWithClaims(tokenString, claims, keyFunc(secret),
|
||||
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
|
||||
jwt.WithExpirationRequired(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid user token: %w", err)
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
var secret = []byte("test-secret")
|
||||
|
||||
func sign(t *testing.T, claims jwt.Claims, key []byte) string {
|
||||
t.Helper()
|
||||
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(key)
|
||||
if err != nil {
|
||||
t.Fatalf("sign: %v", err)
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func gameToken(t *testing.T, key []byte, exp time.Time) string {
|
||||
t.Helper()
|
||||
return sign(t, &GameClaims{
|
||||
MatchID: "m1",
|
||||
Player1ID: 1,
|
||||
Player2ID: 2,
|
||||
GameServer: "ws://game.example.com/ws",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(exp),
|
||||
},
|
||||
}, key)
|
||||
}
|
||||
|
||||
func TestParseGameToken(t *testing.T) {
|
||||
token := gameToken(t, secret, time.Now().Add(time.Hour))
|
||||
claims, err := ParseGameToken(secret, token)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if claims.MatchID != "m1" || claims.Player1ID != 1 || claims.Player2ID != 2 {
|
||||
t.Fatalf("unexpected claims: %+v", claims)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGameTokenExpired(t *testing.T) {
|
||||
token := gameToken(t, secret, time.Now().Add(-time.Hour))
|
||||
if _, err := ParseGameToken(secret, token); err == nil {
|
||||
t.Fatal("expected error for expired token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGameTokenWrongSecret(t *testing.T) {
|
||||
token := gameToken(t, []byte("other-secret"), time.Now().Add(time.Hour))
|
||||
if _, err := ParseGameToken(secret, token); err == nil {
|
||||
t.Fatal("expected error for wrong secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGameTokenMissingExp(t *testing.T) {
|
||||
token := sign(t, &GameClaims{MatchID: "m1", Player1ID: 1, Player2ID: 2}, secret)
|
||||
if _, err := ParseGameToken(secret, token); err == nil {
|
||||
t.Fatal("expected error for token without exp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUserToken(t *testing.T) {
|
||||
token := sign(t, &UserClaims{
|
||||
UserID: 42,
|
||||
RegisteredClaims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour))},
|
||||
}, secret)
|
||||
claims, err := ParseUserToken(secret, token)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
if claims.UserID != 42 {
|
||||
t.Fatalf("user_id = %d, want 42", claims.UserID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user