secure DISABLE_ANONYMOUS

This commit is contained in:
_Bastler 2021-10-21 16:23:42 +02:00
parent e3470d3474
commit f984897e80
13 changed files with 55 additions and 77 deletions

View File

@ -14,7 +14,6 @@ export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const REDIS_HOST = process.env.REDIS_HOST || undefined; export const REDIS_HOST = process.env.REDIS_HOST || undefined;
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379; export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined; export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
export const DEBUG_IGNORE_SSL = process.env.DEBUG_IGNORE_SSL ? process.env.DEBUG_IGNORE_SSL == "true" : false;
export const DEBUG_BACK_IGNORE_LOCAL = process.env.DEBUG_BACK_IGNORE_LOCAL ? process.env.DEBUG_BACK_IGNORE_LOCAL == "true" : false; export const DEBUG_BACK_IGNORE_LOCAL = process.env.DEBUG_BACK_IGNORE_LOCAL ? process.env.DEBUG_BACK_IGNORE_LOCAL == "true" : false;
export { export {

View File

@ -1,13 +1,11 @@
import Axios from "axios"; import Axios from "axios";
import { AxiosRequestConfig } from "axios";
import https from "https";
import ipaddr from "ipaddr.js"; import ipaddr from "ipaddr.js";
import { Resolver } from "dns"; import { Resolver } from "dns";
import { promisify } from "util"; import { promisify } from "util";
import { LocalUrlError } from "./LocalUrlError"; import { LocalUrlError } from "./LocalUrlError";
import { ITiledMap } from "@workadventure/tiled-map-type-guard"; import { ITiledMap } from "@workadventure/tiled-map-type-guard";
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist"; import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
import { DEBUG_IGNORE_SSL, DEBUG_BACK_IGNORE_LOCAL } from "../Enum/EnvironmentVariable"; import { DEBUG_BACK_IGNORE_LOCAL } from "../Enum/EnvironmentVariable";
class MapFetcher { class MapFetcher {
async fetchMap(mapUrl: string): Promise<ITiledMap> { async fetchMap(mapUrl: string): Promise<ITiledMap> {
@ -17,18 +15,6 @@ class MapFetcher {
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map'); throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
} }
const axiosConfig: AxiosRequestConfig = {
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
timeout: 10000, // Timeout after 10 seconds
};
if (DEBUG_IGNORE_SSL) {
const agent = new https.Agent({
rejectUnauthorized: false,
});
axiosConfig.httpsAgent = agent;
}
// Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that // Note: mapUrl is provided by the client. A possible attack vector would be to use a rogue DNS server that
// returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially // returns local URLs. Alas, Axios cannot pin a URL to a given IP. So "isLocalUrl" and Axios.get could potentially
// target to different servers (and one could trick Axios.get into loading resources on the internal network // target to different servers (and one could trick Axios.get into loading resources on the internal network
@ -36,18 +22,16 @@ class MapFetcher {
// We can deem this problem not that important because: // We can deem this problem not that important because:
// - We make sure we are only passing "GET" requests // - We make sure we are only passing "GET" requests
// - The result of the query is never displayed to the end user // - The result of the query is never displayed to the end user
const res = await Axios.get(mapUrl, axiosConfig); const res = await Axios.get(mapUrl, {
maxContentLength: 50 * 1024 * 1024, // Max content length: 50MB. Maps should not be bigger
timeout: 10000, // Timeout after 10 seconds
});
try {
if (!isTiledMap(res.data)) { if (!isTiledMap(res.data)) {
//TODO fixme //TODO fixme
//throw new Error("Invalid map format for map " + mapUrl); //throw new Error("Invalid map format for map " + mapUrl);
console.error("Invalid map format for map " + mapUrl); console.error("Invalid map format for map " + mapUrl);
} }
} catch (e) {
console.error("Invalid map format for map " + mapUrl);
}
return res.data; return res.data;
} }

View File

@ -44,13 +44,22 @@ class ConnectionManager {
//TODO fix me to redirect this URL by pusher //TODO fix me to redirect this URL by pusher
if (!this._currentRoom) { if (!this._currentRoom) {
console.error("cannot get currentRoom!");
loginSceneVisibleIframeStore.set(false); loginSceneVisibleIframeStore.set(false);
return null; return null;
} }
const redirectUrl = `${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}&playUri=${this._currentRoom.key}`; // also allow OIDC login without admin API by using pusher
window.location.assign(redirectUrl); let redirectUrl : URL;
if (this._currentRoom.iframeAuthentication) {
redirectUrl = new URL(`${this._currentRoom.iframeAuthentication}`);
} else {
// need origin if PUSHER_URL is relative (in Single-Domain-Deployment)
redirectUrl = new URL(`${PUSHER_URL}/login-screen`, (!PUSHER_URL.startsWith('http:') || !PUSHER_URL.startsWith('https:')) ? window.location.origin : undefined);
}
redirectUrl.searchParams.append("state", state);
redirectUrl.searchParams.append("nonce", nonce);
redirectUrl.searchParams.append("playUri", this._currentRoom.key);
window.location.assign(redirectUrl.toString());
return redirectUrl; return redirectUrl;
} }

View File

@ -18,7 +18,7 @@ export class Room {
private _iframeAuthentication?: string; private _iframeAuthentication?: string;
private _mapUrl: string | undefined; private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined; private _textures: CharacterTexture[] | undefined;
// private instance: string | undefined; private instance: string | undefined;
private readonly _search: URLSearchParams; private readonly _search: URLSearchParams;
private _contactPage: string | undefined; private _contactPage: string | undefined;
private _group: string | null = null; private _group: string | null = null;
@ -75,13 +75,12 @@ export class Room {
const baseUrl = new URL(currentRoomUrl); const baseUrl = new URL(currentRoomUrl);
const currentRoom = new Room(baseUrl); const currentRoom = new Room(baseUrl);
// let instance: string = "global"; let instance: string = "global";
// if (currentRoom.isPublic) { if (currentRoom.isPublic) {
// instance = currentRoom.instance as string; instance = currentRoom.instance as string;
//} }
// baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname; baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
baseUrl.pathname = "/_/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
if (absoluteExitSceneUrl.hash) { if (absoluteExitSceneUrl.hash) {
baseUrl.hash = absoluteExitSceneUrl.hash; baseUrl.hash = absoluteExitSceneUrl.hash;
} }
@ -119,8 +118,6 @@ export class Room {
* - In a private URL: [organizationId/worldId] * - In a private URL: [organizationId/worldId]
*/ */
public getInstance(): string { public getInstance(): string {
return "";
/*
if (this.instance !== undefined) { if (this.instance !== undefined) {
return this.instance; return this.instance;
} }
@ -131,12 +128,12 @@ export class Room {
this.instance = match[1]; this.instance = match[1];
return this.instance; return this.instance;
} else { } else {
const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id); //const match = /@\/([^/]+)\/([^/]+)\/.+/.exec(this.id);
if (!match) throw new Error('Could not extract instance from "' + this.id + '"'); //if (!match) throw new Error('Could not extract instance from "' + this.id + '"');
this.instance = match[1] + "/" + match[2]; //this.instance = match[1] + "/" + match[2];
this.instance = ""
return this.instance; return this.instance;
} }
*/
} }
public isDisconnected(): boolean { public isDisconnected(): boolean {

View File

@ -37,7 +37,7 @@ export class GameManager {
//If player name was not set show login scene with player name //If player name was not set show login scene with player name
//If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous) //If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous)
if (!this.playerName || !localUserStore.getAuthToken()) { if (!this.playerName || (this.startRoom.authenticationMandatory && !localUserStore.getAuthToken())) {
return LoginSceneName; return LoginSceneName;
} else if (!this.characterLayers || !this.characterLayers.length) { } else if (!this.characterLayers || !this.characterLayers.length) {
return SelectCharacterSceneName; return SelectCharacterSceneName;

View File

@ -303,17 +303,12 @@ export class GameScene extends DirtyScene {
//remove loader in progress //remove loader in progress
removeLoader(this); removeLoader(this);
if (this.roomUrl == localUserStore.getLastRoomUrl()) {
localUserStore.setLastRoomUrl(null);
}
//display an error scene //display an error scene
this.scene.start(ErrorSceneName, { this.scene.start(ErrorSceneName, {
title: "Network error", title: "Network error",
subTitle: "An error occurred while loading resource:", subTitle: "An error occurred while loading resource:",
message: this.originalMapUrl ?? file.src, message: this.originalMapUrl ?? file.src,
}); });
return;
} }
}); });
this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles"); this.load.scenePlugin("AnimatedTiles", AnimatedTiles, "animatedTiles", "animatedTiles");
@ -460,12 +455,6 @@ export class GameScene extends DirtyScene {
//initialise map //initialise map
this.Map = this.add.tilemap(this.MapUrlFile); this.Map = this.add.tilemap(this.MapUrlFile);
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/")); const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
if (!this.mapFile) {
localUserStore.setLastRoomUrl(null);
throw new Error("invalid map");
}
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => { this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
this.Terrains.push( this.Terrains.push(
this.Map.addTilesetImage( this.Map.addTilesetImage(

View File

@ -23,7 +23,9 @@ export class LoginScene extends ResizableScene {
loginSceneVisibleIframeStore.set(false); loginSceneVisibleIframeStore.set(false);
//If authentication is mandatory, push authentication iframe //If authentication is mandatory, push authentication iframe
if ( if (
localUserStore.getAuthToken() == undefined localUserStore.getAuthToken() == undefined &&
gameManager.currentStartedRoom &&
gameManager.currentStartedRoom.authenticationMandatory
) { ) {
connectionManager.loadOpenIDScreen(); connectionManager.loadOpenIDScreen();
loginSceneVisibleIframeStore.set(true); loginSceneVisibleIframeStore.set(true);

View File

@ -38,7 +38,7 @@ class UrlManager {
} }
public pushRoomIdToUrl(room: Room): void { public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id || room.isPublic) return; if (window.location.pathname === room.id) return;
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API) //Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
localUserStore.setLastRoomUrl(room.key); localUserStore.setLastRoomUrl(room.key);
const hash = window.location.hash; const hash = window.location.hash;

View File

@ -5,9 +5,7 @@ import { adminApi } from "../Services/AdminApi";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string"; import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient"; import { openIDClient } from "../Services/OpenIDClient";
import { DEBUG_IGNORE_SSL, PUSHER_DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable" import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"
import { AxiosRequestConfig } from "axios";
import https from "https";
export interface TokenInterface { export interface TokenInterface {
userUuid: string; userUuid: string;
@ -173,26 +171,16 @@ export class AuthenticateController extends BaseController {
res.end(); res.end();
}); });
this.App.post("/anonymLogin", async (res: HttpResponse, req: HttpRequest) => { this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => { res.onAborted(() => {
console.warn("Login request was aborted"); console.warn("Login request was aborted");
}); });
if (PUSHER_DISABLE_ANONYMOUS) { if (DISABLE_ANONYMOUS) {
res.writeStatus("403 FORBIDDEN"); res.writeStatus("403 FORBIDDEN");
res.end(); res.end();
} else { } else {
let userUuid = v4(); const userUuid = v4();
const axiosConfig: AxiosRequestConfig = {};
if (DEBUG_IGNORE_SSL) {
const agent = new https.Agent({
rejectUnauthorized: false,
});
axiosConfig.httpsAgent = agent;
}
const authToken = jwtTokenManager.createAuthToken(userUuid); const authToken = jwtTokenManager.createAuthToken(userUuid);
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);

View File

@ -26,7 +26,7 @@ import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenMana
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager"; import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers"; import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { Zone } from "_Model/Zone"; import { Zone } from "_Model/Zone";
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import { v4 } from "uuid"; import { v4 } from "uuid";
@ -175,6 +175,11 @@ export class IoSocketController {
const tokenData = const tokenData =
token && typeof token === "string" ? jwtTokenManager.verifyJWTToken(token) : null; token && typeof token === "string" ? jwtTokenManager.verifyJWTToken(token) : null;
if (DISABLE_ANONYMOUS && !tokenData) {
throw new Error("Expecting token");
}
const userIdentifier = tokenData ? tokenData.identifier : ""; const userIdentifier = tokenData ? tokenData.identifier : "";
let memberTags: string[] = []; let memberTags: string[] = [];

View File

@ -2,9 +2,9 @@ import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController"; import { BaseController } from "./BaseController";
import { parse } from "query-string"; import { parse } from "query-string";
import { adminApi } from "../Services/AdminApi"; import { adminApi } from "../Services/AdminApi";
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { PusherRoom, GameRoomPolicyTypes } from "../Model/PusherRoom"; import { GameRoomPolicyTypes } from "../Model/PusherRoom";
import { MapDetailsData } from "../Services/AdminApi/MapDetailsData"; import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData";
import { socketManager } from "../Services/SocketManager"; import { socketManager } from "../Services/SocketManager";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { v4 } from "uuid"; import { v4 } from "uuid";
@ -65,6 +65,7 @@ export class MapController extends BaseController {
tags: [], tags: [],
textures: [], textures: [],
contactPage: undefined, contactPage: undefined,
authenticationMandatory : DISABLE_ANONYMOUS,
} as MapDetailsData) } as MapDetailsData)
); );
@ -88,6 +89,10 @@ export class MapController extends BaseController {
} }
const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId); const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId);
if (isMapDetailsData(mapDetails) && DISABLE_ANONYMOUS) {
(mapDetails as MapDetailsData).authenticationMandatory = true;
}
res.writeStatus("200 OK"); res.writeStatus("200 OK");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(JSON.stringify(mapDetails)); res.end(JSON.stringify(mapDetails));

View File

@ -12,12 +12,11 @@ const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 808
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 120; // maximum time (in second) without activity before a socket is closed. Should be greater than 60 seconds in order to cope for Chrome intensive throttling (https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling) export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 120; // maximum time (in second) without activity before a socket is closed. Should be greater than 60 seconds in order to cope for Chrome intensive throttling (https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling)
export const FRONT_URL = process.env.FRONT_URL || "http://localhost"; export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const PUSHER_DISABLE_ANONYMOUS = process.env.PUSHER_DISABLE_ANONYMOUS ? process.env.PUSHER_DISABLE_ANONYMOUS == "true" : false; export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS ? process.env.DISABLE_ANONYMOUS == "true" : false;
export const PUSHER_FORCE_ROOM_UPDATE = process.env.PUSHER_FORCE_ROOM_UPDATE ? process.env.PUSHER_FORCE_ROOM_UPDATE == "true" : false; export const PUSHER_FORCE_ROOM_UPDATE = process.env.PUSHER_FORCE_ROOM_UPDATE ? process.env.PUSHER_FORCE_ROOM_UPDATE == "true" : false;
export const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || ""; export const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
export const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || ""; export const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET || "";
export const OIDC_CLIENT_ISSUER = process.env.OIDC_CLIENT_ISSUER || ""; export const OIDC_CLIENT_ISSUER = process.env.OIDC_CLIENT_ISSUER || "";
export const DEBUG_IGNORE_SSL = process.env.DEBUG_IGNORE_SSL ? process.env.DEBUG_IGNORE_SSL == "true" : false;
export { export {
SECRET_KEY, SECRET_KEY,

View File

@ -16,6 +16,7 @@ export const isMapDetailsData = new tg.IsInterface()
tags: tg.isArray(tg.isString), tags: tg.isArray(tg.isString),
textures: tg.isArray(isCharacterTexture), textures: tg.isArray(isCharacterTexture),
contactPage: tg.isUnion(tg.isString, tg.isUndefined), contactPage: tg.isUnion(tg.isString, tg.isUndefined),
authenticationMandatory: tg.isUnion(tg.isBoolean, tg.isUndefined),
}) })
.get(); .get();