Starting Point
In Your First Spaceship Multiplayer Portal, we built a working /game portal with:
- built-in
handshake,join-game, andleave-game - custom gameplay gates
- real player entities stored through
IGameSessionService - sync patches for visible state changes
Now we are going to add the next missing piece: authentication.
The goal is simple:
- players log in over HTTP
- the server returns JWT tokens
- protected endpoints and portals only accept authenticated users
1. Enable JWT Mode
Add security config to config.yml:
altruist:
server:
http:
host: "0.0.0.0"
port: 8080
path: "/"
transport:
codec:
provider: json
websocket:
enabled: true
path: /game
security:
mode: "jwt"
key: "your-base64-secret-key"
persistence:
cache:
provider: inmemory
That turns on the built-in JWT services:
JwtAuthJwtTokenValidatorJwtTokenIssuer
2. Add an Auth Controller
Create Http/AuthController.cs:
using Altruist;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace SpaceGame.Http;
[ApiController]
[Route("/auth")]
public class AuthController : JwtAuthController
{
public AuthController(
IJwtTokenValidator validator,
ILoginService loginService,
TokenSessionSyncService syncService,
IJwtTokenIssuer issuer,
IAuthService authService,
ILoggerFactory loggerFactory)
: base(
validator,
loginService,
syncService,
issuer,
authService,
loggerFactory)
{
}
}
That immediately gives you these endpoints:
POST /auth/signupPOST /auth/login/emailpwdPOST /auth/login/unamepwdPOST /auth/refreshPOST /auth/upgrade
3. Protect the Portal
Take the portal from the first post and add JwtShield:
using Altruist;
using Altruist.Gaming;
namespace SpaceGame.Portals;
[Portal("/game")]
[JwtShield]
public class GamePortal : AltruistGameSessionPortal
{
public GamePortal(
IGameSessionService gameSessionService,
IAltruistRouter router,
ILoggerFactory loggerFactory)
: base(gameSessionService, router)
{
}
[Gate("chat")]
public async Task OnChat(ChatPacket packet, string clientId)
{
// Only authenticated users reach this point
}
}
If you only want to protect a few actions, keep the portal public and shield just the sensitive gates:
[Portal("/game")]
public class GamePortal : AltruistGameSessionPortal
{
[Gate("public-ping")]
public Task Ping(TextPacket packet, string clientId) => Task.CompletedTask;
[JwtShield]
[Gate("private-chat")]
public Task PrivateChat(ChatPacket packet, string clientId) => Task.CompletedTask;
}
4. Test the Login Flow
Create an account:
curl -X POST http://localhost:8080/auth/signup \
-H "Content-Type: application/json" \
-d '{"email":"pilot@example.com","username":"pilot","password":"secret123"}'
Log in:
curl -X POST http://localhost:8080/auth/login/unamepwd \
-H "Content-Type: application/json" \
-d '{"username":"pilot","password":"secret123"}'
Response:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
Once the client has a valid access token, protected HTTP controllers and protected portals can trust that identity.
Why This Post Matters
The first post gave you a playable multiplayer shell.
This post makes that shell real:
- named players can now become authenticated users
- admin and player features can diverge cleanly
- game actions can be gated without rewriting the portal architecture
Next Up
The next post gives those authenticated users a durable profile in the database:
Continue the Series