Back to blog
Your FirstPart 2 of 9

Friday, April 17, 2026

Your First Authentication

cover

Starting Point

In Your First Spaceship Multiplayer Portal, we built a working /game portal with:

  • built-in handshake, join-game, and leave-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:

  • JwtAuth
  • JwtTokenValidator
  • JwtTokenIssuer

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/signup
  • POST /auth/login/emailpwd
  • POST /auth/login/unamepwd
  • POST /auth/refresh
  • POST /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: