Merge branch 'develop' of github.com:thecodingmachine/workadventure

This commit is contained in:
_Bastler
2021-11-16 10:56:33 +01:00
49 changed files with 755 additions and 489 deletions
+3
View File
@@ -6,6 +6,7 @@ import { PrometheusController } from "./Controller/PrometheusController";
import { DebugController } from "./Controller/DebugController";
import { App as uwsApp } from "./Server/sifrr.server";
import { AdminController } from "./Controller/AdminController";
import { OpenIdProfileController } from "./Controller/OpenIdProfileController";
class App {
public app: uwsApp;
@@ -15,6 +16,7 @@ class App {
public prometheusController: PrometheusController;
private debugController: DebugController;
private adminController: AdminController;
private openIdProfileController: OpenIdProfileController;
constructor() {
this.app = new uwsApp();
@@ -26,6 +28,7 @@ class App {
this.prometheusController = new PrometheusController(this.app);
this.debugController = new DebugController(this.app);
this.adminController = new AdminController(this.app);
this.openIdProfileController = new OpenIdProfileController(this.app);
}
}
+41 -13
View File
@@ -1,11 +1,11 @@
import { v4 } from "uuid";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"
import { DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
export interface TokenInterface {
userUuid: string;
@@ -56,19 +56,20 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { code, nonce, token } = parse(req.getQuery());
const IPAddress = req.getHeader("x-forwarded-for");
const { code, nonce, token, playUri } = parse(req.getQuery());
try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
const resCheckTokenAuth = await openIDClient.checkTokenAuth(authTokenData.accessToken);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken: token, username: authTokenData.username, userUuid : authTokenData.identifier }));
return res.end(JSON.stringify({ ...resCheckTokenAuth, username: authTokenData.username, authToken: token }));
} catch (err) {
console.info("User was not connected", err);
}
@@ -81,9 +82,14 @@ export class AuthenticateController extends BaseController {
throw new Error("No sub in the response");
}
const authToken = jwtTokenManager.createAuthToken(sub, userInfo.access_token, userInfo.username);
//Get user data from Admin Back Office
//This is very important to create User Local in LocalStorage in WorkAdventure
const data = await this.getUserByUserIdentifier(sub, playUri as string, IPAddress);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken: authToken, username: userInfo.username, userUuid : sub }));
return res.end(JSON.stringify({ ...data, authToken, username: userInfo.username, userUuid : sub }));
} catch (e) {
console.error("openIDCallback => ERROR", e);
return this.errorToResponse(e, res);
@@ -100,10 +106,10 @@ export class AuthenticateController extends BaseController {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be logout on Hydra");
}
await openIDClient.logoutUser(authTokenData.hydraAccessToken);
await openIDClient.logoutUser(authTokenData.accessToken);
} catch (error) {
console.error("openIDCallback => logout-callback", error);
} finally {
@@ -202,20 +208,20 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { userIdentify, token } = parse(req.getQuery());
const { token } = parse(req.getQuery());
try {
//verify connected by token
if (token != undefined) {
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
if (authTokenData.accessToken == undefined) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
await openIDClient.checkTokenAuth(authTokenData.accessToken);
//get login profile
res.writeStatus("302");
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.hydraAccessToken));
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.accessToken));
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
@@ -229,4 +235,26 @@ export class AuthenticateController extends BaseController {
}
});
}
/**
*
* @param email
* @param playUri
* @param IPAddress
* @return FetchMemberDataByUuidResponse|object
* @private
*/
private async getUserByUserIdentifier(
email: string,
playUri: string,
IPAddress: string
): Promise<FetchMemberDataByUuidResponse | object> {
let data: FetchMemberDataByUuidResponse | object = {};
try {
data = await adminApi.fetchMemberDataByUuid(email, playUri, IPAddress);
} catch (err) {
console.error("openIDCallback => fetchMemberDataByUuid", err);
}
return data;
}
}
+11 -10
View File
@@ -26,10 +26,9 @@ import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenMana
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable";
import { ADMIN_SOCKETS_TOKEN, ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { Zone } from "_Model/Zone";
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import { v4 } from "uuid";
import { CharacterTexture } from "../Services/AdminApi/CharacterTexture";
export class IoSocketController {
@@ -48,15 +47,19 @@ export class IoSocketController {
const websocketProtocol = req.getHeader("sec-websocket-protocol");
const websocketExtensions = req.getHeader("sec-websocket-extensions");
const token = query.token;
if (token !== ADMIN_API_TOKEN) {
console.log("Admin access refused for token: " + token);
let authorizedRoomIds: string[];
try {
const data = jwtTokenManager.verifyAdminSocketToken(token as string);
authorizedRoomIds = data.authorizedRoomIds;
} catch (e) {
console.error("Admin access refused for token: " + token);
res.writeStatus("401 Unauthorized").end("Incorrect token");
return;
}
const roomId = query.roomId;
if (typeof roomId !== "string") {
console.error("Received");
res.writeStatus("400 Bad Request").end("Missing room id");
if (typeof roomId !== "string" || !authorizedRoomIds.includes(roomId)) {
console.error("Invalid room id");
res.writeStatus("403 Bad Request").end("Invalid room id");
return;
}
@@ -70,8 +73,6 @@ export class IoSocketController {
},
message: (ws, arrayBuffer, isBinary): void => {
try {
const roomId = ws.roomId as string;
//TODO refactor message type and data
const message: { event: string; message: { type: string; message: unknown; userUuid: string } } =
JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer)));
@@ -250,7 +251,7 @@ export class IoSocketController {
roomId
);
console.error(e);
throw new Error("User cannot access this room");
throw new Error("User cannot access this world");
}
}
+3 -3
View File
@@ -52,7 +52,7 @@ export class MapController extends BaseController {
return;
}
const mapUrl = roomUrl.protocol + "//" + match[ 1 ];
const mapUrl = roomUrl.protocol + "//" + match[1];
res.writeStatus("200 OK");
this.addCorsHeaders(res);
@@ -65,7 +65,7 @@ export class MapController extends BaseController {
tags: [],
textures: [],
contactPage: undefined,
authenticationMandatory : DISABLE_ANONYMOUS,
authenticationMandatory: DISABLE_ANONYMOUS,
} as MapDetailsData)
);
@@ -90,7 +90,7 @@ export class MapController extends BaseController {
const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId);
if (isMapDetailsData(mapDetails) && DISABLE_ANONYMOUS) {
(mapDetails as MapDetailsData).authenticationMandatory = true;
mapDetails.authenticationMandatory = true;
}
res.writeStatus("200 OK");
@@ -0,0 +1,80 @@
import { BaseController } from "./BaseController";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { adminApi } from "../Services/AdminApi";
import { OPID_CLIENT_ISSUER } from "../Enum/EnvironmentVariable";
import { IntrospectionResponse } from "openid-client";
export class OpenIdProfileController extends BaseController {
constructor(private App: TemplatedApp) {
super();
this.profileOpenId();
}
profileOpenId() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/profile", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { accessToken } = parse(req.getQuery());
if (!accessToken) {
throw Error("Access token expected cannot to be check on Hydra");
}
try {
const resCheckTokenAuth = await openIDClient.checkTokenAuth(accessToken as string);
if (!resCheckTokenAuth.email) {
throw "Email was not found";
}
res.end(
this.buildHtml(
OPID_CLIENT_ISSUER,
resCheckTokenAuth.email as string,
resCheckTokenAuth.picture as string | undefined
)
);
} catch (error) {
console.error("profileCallback => ERROR", error);
this.errorToResponse(error, res);
}
});
}
buildHtml(domain: string, email: string, pictureUrl?: string) {
return `
<!DOCTYPE>
<html>
<head>
<style>
*{
font-family: PixelFont-7, monospace;
}
body{
text-align: center;
color: white;
}
section{
margin: 20px;
}
</style>
</head>
<body>
<div class="container">
<section>
<img src="${pictureUrl ? pictureUrl : "/images/profile"}">
</section>
<section>
Profile validated by domain: <span style="font-weight: bold">${domain}</span>
</section>
<section>
Your email: <span style="font-weight: bold">${email}</span>
</section>
</div>
</body>
</html>
`;
}
}
+7 -4
View File
@@ -4,6 +4,7 @@ const API_URL = process.env.API_URL || "";
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_URL = process.env.ADMIN_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
export const ADMIN_SOCKETS_TOKEN = process.env.ADMIN_SOCKETS_TOKEN || "myapitoken";
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || "";
@@ -12,11 +13,13 @@ 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 FRONT_URL = process.env.FRONT_URL || "http://localhost";
export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS ? process.env.DISABLE_ANONYMOUS == "true" : false;
export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || "";
export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
export const OPID_CLIENT_ISSUER = process.env.OPID_CLIENT_ISSUER || "";
export const OPID_CLIENT_REDIRECT_URL = process.env.OPID_CLIENT_REDIRECT_URL || FRONT_URL + "/jwt";
export const OPID_PROFILE_SCREEN_PROVIDER = process.env.OPID_PROFILE_SCREEN_PROVIDER || ADMIN_URL + "/profile";
export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS || 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_SECRET = process.env.OIDC_CLIENT_SECRET || "";
export const OIDC_CLIENT_ISSUER = process.env.OIDC_CLIENT_ISSUER || "";
export {
SECRET_KEY,
+10 -4
View File
@@ -1,4 +1,4 @@
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL } from "../Enum/EnvironmentVariable";
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL, OPID_PROFILE_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import Axios from "axios";
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
import { CharacterTexture } from "./AdminApi/CharacterTexture";
@@ -142,13 +142,19 @@ class AdminApi {
});
}
/*TODO add constant to use profile companny*/
/**
*
* @param accessToken
*/
getProfileUrl(accessToken: string): string {
if (!ADMIN_URL) {
if (!OPID_PROFILE_SCREEN_PROVIDER) {
throw new Error("No admin backoffice set!");
}
return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`;
}
return ADMIN_URL + `/profile?token=${accessToken}`;
async logoutOauth(token: string) {
await Axios.get(ADMIN_API_URL + `/oauth/logout?token=${token}`);
}
}
+11 -4
View File
@@ -1,4 +1,4 @@
import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { uuid } from "uuidv4";
import Jwt, { verify } from "jsonwebtoken";
import { TokenInterface } from "../Controller/AuthenticateController";
@@ -6,14 +6,21 @@ import { adminApi, AdminBannedData } from "../Services/AdminApi";
export interface AuthTokenData {
identifier: string; //will be a sub (id) if logged in or an uuid if anonymous
hydraAccessToken?: string;
accessToken?: string;
username?: string;
}
export interface AdminSocketTokenData {
authorizedRoomIds: string[]; //the list of rooms the client is authorized to read from.
}
export const tokenInvalidException = "tokenInvalid";
class JWTTokenManager {
public createAuthToken(identifier: string, hydraAccessToken?: string, username?: string) {
return Jwt.sign({ identifier, hydraAccessToken, username }, SECRET_KEY, { expiresIn: "30d" });
public verifyAdminSocketToken(token: string): AdminSocketTokenData {
return Jwt.verify(token, ADMIN_SOCKETS_TOKEN) as AdminSocketTokenData;
}
public createAuthToken(identifier: string, accessToken?: string, username?: string) {
return Jwt.sign({ identifier, accessToken, username }, SECRET_KEY, { expiresIn: "30d" });
}
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {
+11 -8
View File
@@ -1,18 +1,21 @@
import { Issuer, Client, IntrospectionResponse } from "openid-client";
import { OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
const opidRedirectUri = FRONT_URL + "/jwt";
import {
OPID_CLIENT_ID,
OPID_CLIENT_SECRET,
OPID_CLIENT_ISSUER,
OPID_CLIENT_REDIRECT_URL,
} from "../Enum/EnvironmentVariable";
class OpenIDClient {
private issuerPromise: Promise<Client> | null = null;
private initClient(): Promise<Client> {
if (!this.issuerPromise) {
this.issuerPromise = Issuer.discover(OIDC_CLIENT_ISSUER).then((issuer) => {
this.issuerPromise = Issuer.discover(OPID_CLIENT_ISSUER).then((issuer) => {
return new issuer.Client({
client_id: OIDC_CLIENT_ID,
client_secret: OIDC_CLIENT_SECRET,
redirect_uris: [opidRedirectUri],
client_id: OPID_CLIENT_ID,
client_secret: OPID_CLIENT_SECRET,
redirect_uris: [OPID_CLIENT_REDIRECT_URL],
response_types: ["code"],
});
});
@@ -35,7 +38,7 @@ class OpenIDClient {
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string; access_token: string; username: string }> {
return this.initClient().then((client) => {
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
return client.callback(OPID_CLIENT_REDIRECT_URL, { code }, { nonce }).then((tokenSet) => {
return client.userinfo(tokenSet).then((res) => {
return {
...res,
+3 -3
View File
@@ -54,10 +54,10 @@ import { CharacterTexture } from "./AdminApi/CharacterTexture";
const debug = Debug("socket");
interface AdminSocketRoomsList {
[ index: string ]: number;
[index: string]: number;
}
interface AdminSocketUsersList {
[ index: string ]: boolean;
[index: string]: boolean;
}
export interface AdminSocketData {
@@ -635,7 +635,7 @@ export class SocketManager implements ZoneEventListener {
if (playGlobalMessageEvent.getBroadcasttoworld()) {
tabUrlRooms = await adminApi.getUrlRoomsFromSameWorld(clientRoomUrl);
} else {
tabUrlRooms = [ clientRoomUrl ];
tabUrlRooms = [clientRoomUrl];
}
const roomMessage = new AdminRoomMessage();