Skip to main content

Custom Authentication

Typically, all PlaySafe requests made by the Unity client only contain one level of authentication (app keys). However, in some cases, this exposes the possibility of hackers being able to spam PlaySafe with all player user ids while saying offensive statements. As a result, a malicious actor, with significant effort, could get players other than themselves banned.

To solve this, we provide you with the option of custom authentication.

How it works

There are two main concepts you need to understand:

  1. Generating a token - This generates a JWT token that can be used to authenticate all future PlaySafe requests
  2. Verifying a token - This verifies that a token received is valid before letting PlaySafe process the request.

Steps to implement

  1. Setup an auth url that will be implemented on the PlaySafe dashboard
  2. Implement your generate token endpoint: In your PlaySafe integration: call playsafeManager.SetCustomAuth()
  • Internally, this will send a POST request to your authUrl with the request body contain the following information as JSON { playerUserId: string }. It is expected to return a JWT as a string.
  1. Implement your verify token endpoint: This is a GET request endpoint with the same endpoint as your authUrl. It accepts a token as a query parameter and is expected to return an object with the signature: { succes: boolean, error?:string }

Example Backend Implementations

We provide NodeJS implementation examples that you can mostly drop-in to your backend and have the custom authentication working once you have set up PlaySafe auth urls in the dashboard.

Hono Example

/**
* Custom Authentication Examples
*
* This file contains example Hono endpoints that game developers can implement
* on their own servers to integrate with PlaySafe's authentication system.
*
* These examples show how to:
* 1. Verify player authentication tokens (used by PlaySafe to validate players)
* 2. Generate player auth tokens (used by PlaySafe to get JWT tokens for players)
*/

import { Hono } from "hono";
import { sign, verify } from "hono/jwt";

const app = new Hono();

// Your JWT secret - should be stored securely in environment variables
const JWT_SECRET = "your-super-secret-jwt-key";

/**
* Verify Player Auth Endpoint (GET)
*
* This endpoint is called by PlaySafe to verify if a player's token is valid.
* PlaySafe will send a GET request with a token query parameter.
*
* Expected Request: GET /verify-player-auth?token=<player-token>
* Expected Response: { success: boolean, expiryUnixTimestampMs?: number, error?: string }
*
* This corresponds to the `sendProductAuthRequest` function in PlaySafe's auth service.
*/
app.get("/playsafe/auth", async (c) => {
try {
const token = c.req.query("token");

if (!token) {
return c.json({
success: false,
error: "Token is required",
});
}

// Verify the token using your JWT secret
try {
const decoded = await verify(token, JWT_SECRET);

// Check if token has required fields (customize based on your needs)
if (!decoded.playerUserId || !decoded.exp) {
return c.json({
success: false,
error: "Invalid token structure",
});
}

// Check if token is expired
const currentTime = Date.now();
if (decoded.exp < currentTime) {
return c.json({
success: false,
error: "Token has expired",
});
}

// Token is valid - return success with expiry time in milliseconds
return c.json({
success: true,
expiryUnixTimestampMs: decoded.exp, // Convert to milliseconds
});
} catch (error) {
return c.json({
success: false,
error: "Invalid or expired token",
});
}
} catch (error) {
console.error("[Custom Auth Example] Error verifying player auth", error);
return c.json({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
}
});

/**
* Generate Player Auth Token Endpoint (POST)
*
* This endpoint is called by PlaySafe to generate a JWT token for a player.
* PlaySafe will send a POST request with playerUserId and userId in the body.
*
* Expected Request Body: { playerUserId: string, exp: number }
* Expected Response: JWT string (plain text, not JSON)
*
* This corresponds to the `generatePlayerAuthToken` function in PlaySafe's auth service.
*/
app.post("/playsafe/auth", async (c) => {
try {
const { playerUserId, exp } = await c.req.json();

if (!playerUserId) {
return c.text("playerUserId is required", 400);
}

if (!exp) {
return c.text(
"exp is required (expiry time of the token in milliseconds since epoch)",
400
);
}

// Generate JWT token with player information
const token = await sign(
{
playerUserId,
exp: exp,
iat: Date.now(),
},
JWT_SECRET
);

// Return the JWT token as plain text (not JSON)
return c.text(token);
} catch (error) {
return c.text(
error instanceof Error ? error.message : "Failed to generate token",
500
);
}
});

ExpressJS Example

/**
* Custom Authentication Examples - Express.js
*
* This file contains example Express endpoints that game developers can implement
* on their own servers to integrate with PlaySafe's authentication system.
*
* These examples show how to:
* 1. Verify player authentication tokens (used by PlaySafe to validate players)
* 2. Generate player auth tokens (used by PlaySafe to get JWT tokens for players)
*/

import express, { Request, Response } from "express";
import jwt from "jsonwebtoken";

const app = express();

// Middleware to parse JSON bodies
app.use(express.json());

// Your JWT secret - should be stored securely in environment variables
const JWT_SECRET = "your-super-secret-jwt-key";

/**
* Verify Player Auth Endpoint (GET)
*
* This endpoint is called by PlaySafe to verify if a player's token is valid.
* PlaySafe will send a GET request with a token query parameter.
*
* Expected Request: GET /playsafe/auth?token=<player-token>
* Expected Response: { success: boolean, expiryUnixTimestampMs?: number, error?: string }
*
* This corresponds to the `sendProductAuthRequest` function in PlaySafe's auth service.
*/
app.get("/playsafe/auth", async (req: Request, res: Response) => {
try {
const token = req.query.token as string;

if (!token) {
return res.json({
success: false,
error: "Token is required",
});
}

// Verify the token using your JWT secret
try {
const decoded = jwt.verify(token, JWT_SECRET) as any;

// Check if token has required fields (customize based on your needs)
if (!decoded.playerUserId || !decoded.exp) {
return res.json({
success: false,
error: "Invalid token structure",
});
}

// Check if token is expired
const currentTime = Date.now();
if (decoded.exp < currentTime) {
return res.json({
success: false,
error: "Token has expired",
});
}

// Token is valid - return success with expiry time in milliseconds
return res.json({
success: true,
expiryUnixTimestampMs: decoded.exp,
});
} catch (error) {
return res.json({
success: false,
error: "Invalid or expired token",
});
}
} catch (error) {
console.error("[Custom Auth Example] Error verifying player auth", error);
return res.json({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
}
});

/**
* Generate Player Auth Token Endpoint (POST)
*
* This endpoint is called by PlaySafe to generate a JWT token for a player.
* PlaySafe will send a POST request with playerUserId and userId in the body.
*
* Expected Request Body: { playerUserId: string, exp: number }
* Expected Response: JWT string (plain text, not JSON)
*
* This corresponds to the `generatePlayerAuthToken` function in PlaySafe's auth service.
*/
app.post("/playsafe/auth", async (req: Request, res: Response) => {
try {
const { playerUserId, exp } = req.body;

if (!playerUserId) {
return res.status(400).send("playerUserId is required");
}

if (!exp) {
return res
.status(400)
.send(
"exp is required (expiry time of the token in milliseconds since epoch)"
);
}

// Generate JWT token with player information
const token = jwt.sign(
{
playerUserId,
exp: exp,
iat: Date.now(),
},
JWT_SECRET
);

// Return the JWT token as plain text (not JSON)
return res.send(token);
} catch (error) {
return res
.status(500)
.send(
error instanceof Error ? error.message : "Failed to generate token"
);
}
});

// Start the server (if running this file directly)
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Express server running on port ${PORT}`);
});

export default app;

Fastify Example

/**
* Custom Authentication Examples - Fastify
*
* This file contains example Fastify endpoints that game developers can implement
* on their own servers to integrate with PlaySafe's authentication system.
*
* These examples show how to:
* 1. Verify player authentication tokens (used by PlaySafe to validate players)
* 2. Generate player auth tokens (used by PlaySafe to get JWT tokens for players)
*/

import Fastify, { FastifyRequest, FastifyReply } from "fastify";
import jwt from "jsonwebtoken";

const fastify = Fastify({
logger: true,
});

// Your JWT secret - should be stored securely in environment variables
const JWT_SECRET = "your-super-secret-jwt-key";

// Type definitions for requests
interface VerifyAuthQuery {
token?: string;
}

interface GenerateTokenBody {
playerUserId?: string;
userId?: string;
exp?: number;
}

/**
* Verify Player Auth Endpoint (GET)
*
* This endpoint is called by PlaySafe to verify if a player's token is valid.
* PlaySafe will send a GET request with a token query parameter.
*
* Expected Request: GET /playsafe/auth?token=<player-token>
* Expected Response: { success: boolean, expiryUnixTimestampMs?: number, error?: string }
*
* This corresponds to the `sendProductAuthRequest` function in PlaySafe's auth service.
*/
fastify.get<{ Querystring: VerifyAuthQuery }>(
"/playsafe/auth",
async (
request: FastifyRequest<{ Querystring: VerifyAuthQuery }>,
reply: FastifyReply
) => {
try {
const { token } = request.query;

if (!token) {
return reply.send({
success: false,
error: "Token is required",
});
}

// Verify the token using your JWT secret
try {
const decoded = jwt.verify(token, JWT_SECRET) as any;

// Check if token has required fields (customize based on your needs)
if (!decoded.playerUserId || !decoded.exp) {
return reply.send({
success: false,
error: "Invalid token structure",
});
}

// Check if token is expired
const currentTime = Date.now();
if (decoded.exp < currentTime) {
return reply.send({
success: false,
error: "Token has expired",
});
}

// Token is valid - return success with expiry time in milliseconds
return reply.send({
success: true,
expiryUnixTimestampMs: decoded.exp,
});
} catch (error) {
return reply.send({
success: false,
error: "Invalid or expired token",
});
}
} catch (error) {
console.error("[Custom Auth Example] Error verifying player auth", error);
return reply.send({
success: false,
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
);

/**
* Generate Player Auth Token Endpoint (POST)
*
* This endpoint is called by PlaySafe to generate a JWT token for a player.
* PlaySafe will send a POST request with playerUserId and userId in the body.
*
* Expected Request Body: { playerUserId: string, exp: number }
* Expected Response: JWT string (plain text, not JSON)
*
* This corresponds to the `generatePlayerAuthToken` function in PlaySafe's auth service.
*/
fastify.post<{ Body: GenerateTokenBody }>(
"/playsafe/auth",
async (
request: FastifyRequest<{ Body: GenerateTokenBody }>,
reply: FastifyReply
) => {
try {
const { playerUserId, exp } = request.body;

if (!playerUserId) {
return reply.code(400).send("playerUserId is required");
}

if (!exp) {
return reply
.code(400)
.send(
"exp is required (expiry time of the token in milliseconds since epoch)"
);
}

// Generate JWT token with player information
const token = jwt.sign(
{
playerUserId,
exp: exp,
iat: Date.now(),
},
JWT_SECRET
);

// Return the JWT token as plain text (not JSON)
return reply.type("text/plain").send(token);
} catch (error) {
return reply
.code(500)
.send(
error instanceof Error ? error.message : "Failed to generate token"
);
}
}
);

// Start the server (if running this file directly)
const start = async () => {
try {
const PORT = process.env.PORT || 3000;
await fastify.listen({ port: Number(PORT), host: "0.0.0.0" });
console.log(`Fastify server running on port ${PORT}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};

start();

export default fastify;