FEATURE: users can now login via an openID client
This commit is contained in:
parent
74975ac9d8
commit
9c803a69ff
@ -19,3 +19,6 @@ ACME_EMAIL=
|
|||||||
MAX_PER_GROUP=4
|
MAX_PER_GROUP=4
|
||||||
MAX_USERNAME_LENGTH=8
|
MAX_USERNAME_LENGTH=8
|
||||||
|
|
||||||
|
OPID_CLIENT_ID=
|
||||||
|
OPID_CLIENT_SECRET=
|
||||||
|
OPID_CLIENT_ISSUER=
|
||||||
|
2
.github/workflows/continuous_integration.yml
vendored
2
.github/workflows/continuous_integration.yml
vendored
@ -82,7 +82,7 @@ jobs:
|
|||||||
- name: "Setup NodeJS"
|
- name: "Setup NodeJS"
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: '12.x'
|
node-version: '14.x'
|
||||||
|
|
||||||
- name: Install Protoc
|
- name: Install Protoc
|
||||||
uses: arduino/setup-protoc@v1
|
uses: arduino/setup-protoc@v1
|
||||||
|
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,4 +1,12 @@
|
|||||||
## Version 1.4.x-dev
|
## Version develop
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
- Rewrote the way authentification works: the auth jwt token can now contains an email instead of an uuid
|
||||||
|
- Added an OpenId login flow than can be plugged to any OIDC provider.
|
||||||
|
-
|
||||||
|
|
||||||
|
## Version 1.4.10
|
||||||
|
|
||||||
### Updates
|
### Updates
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ RoomConnection.setWebsocketFactory((url: string) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function startOneUser(): Promise<void> {
|
async function startOneUser(): Promise<void> {
|
||||||
await connectionManager.anonymousLogin(true);
|
|
||||||
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
const onConnect = await connectionManager.connectToRoomSocket(process.env.ROOM_ID ? process.env.ROOM_ID : '_/global/maps.workadventure.localhost/Floor0/floor0.json', 'TEST', ['male3'],
|
||||||
{
|
{
|
||||||
x: 783,
|
x: 783,
|
||||||
@ -23,7 +22,7 @@ async function startOneUser(): Promise<void> {
|
|||||||
bottom: 200,
|
bottom: 200,
|
||||||
left: 500,
|
left: 500,
|
||||||
right: 800
|
right: 800
|
||||||
});
|
}, null);
|
||||||
|
|
||||||
const connection = onConnect.connection;
|
const connection = onConnect.connection;
|
||||||
|
|
||||||
|
@ -66,6 +66,10 @@ services:
|
|||||||
API_URL: back:50051
|
API_URL: back:50051
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
|
FRONT_URL: http://localhost
|
||||||
|
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
||||||
|
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
||||||
|
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
@ -66,6 +66,10 @@ services:
|
|||||||
API_URL: back:50051
|
API_URL: back:50051
|
||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
|
FRONT_URL: http://play.workadventure.localhost
|
||||||
|
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
||||||
|
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
||||||
|
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
3
front/dist/resources/html/gameMenu.html
vendored
3
front/dist/resources/html/gameMenu.html
vendored
@ -60,6 +60,9 @@
|
|||||||
<section>
|
<section>
|
||||||
<button id="enableNotification">Enable notifications</button>
|
<button id="enableNotification">Enable notifications</button>
|
||||||
</section>
|
</section>
|
||||||
|
<section hidden>
|
||||||
|
<button id="oidcLogin">Oauth Login</button>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<button id="sparkButton">Create map</button>
|
<button id="sparkButton">Create map</button>
|
||||||
</section>
|
</section>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"@types/mini-css-extract-plugin": "^1.4.3",
|
"@types/mini-css-extract-plugin": "^1.4.3",
|
||||||
"@types/node": "^15.3.0",
|
"@types/node": "^15.3.0",
|
||||||
"@types/quill": "^1.3.7",
|
"@types/quill": "^1.3.7",
|
||||||
|
"@types/uuidv4": "^5.0.0",
|
||||||
"@types/webpack-dev-server": "^3.11.4",
|
"@types/webpack-dev-server": "^3.11.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
"@typescript-eslint/eslint-plugin": "^4.23.0",
|
||||||
"@typescript-eslint/parser": "^4.23.0",
|
"@typescript-eslint/parser": "^4.23.0",
|
||||||
@ -53,7 +54,8 @@
|
|||||||
"rxjs": "^6.6.3",
|
"rxjs": "^6.6.3",
|
||||||
"simple-peer": "^9.11.0",
|
"simple-peer": "^9.11.0",
|
||||||
"socket.io-client": "^2.3.0",
|
"socket.io-client": "^2.3.0",
|
||||||
"standardized-audio-context": "^25.2.4"
|
"standardized-audio-context": "^25.2.4",
|
||||||
|
"uuidv4": "^6.2.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "run-p templater serve svelte-check-watch",
|
"start": "run-p templater serve svelte-check-watch",
|
||||||
|
@ -14,6 +14,7 @@ class ConnectionManager {
|
|||||||
private connexionType?: GameConnexionTypes;
|
private connexionType?: GameConnexionTypes;
|
||||||
private reconnectingTimeout: NodeJS.Timeout | null = null;
|
private reconnectingTimeout: NodeJS.Timeout | null = null;
|
||||||
private _unloading: boolean = false;
|
private _unloading: boolean = false;
|
||||||
|
private authToken: string | null = null;
|
||||||
|
|
||||||
private serviceWorker?: _ServiceWorker;
|
private serviceWorker?: _ServiceWorker;
|
||||||
|
|
||||||
@ -27,21 +28,57 @@ class ConnectionManager {
|
|||||||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
|
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public loadOpenIDScreen() {
|
||||||
|
localUserStore.setAuthToken(null);
|
||||||
|
const state = localUserStore.generateState();
|
||||||
|
const nonce = localUserStore.generateNonce();
|
||||||
|
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public logout() {
|
||||||
|
localUserStore.setAuthToken(null);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to login to the node server and return the starting map url to be loaded
|
* Tries to login to the node server and return the starting map url to be loaded
|
||||||
*/
|
*/
|
||||||
public async initGameConnexion(): Promise<Room> {
|
public async initGameConnexion(): Promise<Room> {
|
||||||
const connexionType = urlManager.getGameConnexionType();
|
const connexionType = urlManager.getGameConnexionType();
|
||||||
this.connexionType = connexionType;
|
this.connexionType = connexionType;
|
||||||
|
|
||||||
let room: Room | null = null;
|
let room: Room | null = null;
|
||||||
if (connexionType === GameConnexionTypes.register) {
|
if (connexionType === GameConnexionTypes.jwt) {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get("code");
|
||||||
|
const state = urlParams.get("state");
|
||||||
|
if (!state || !localUserStore.verifyState(state)) {
|
||||||
|
throw "Could not validate state!";
|
||||||
|
}
|
||||||
|
if (!code) {
|
||||||
|
throw "No Auth code provided";
|
||||||
|
}
|
||||||
|
const nonce = localUserStore.getNonce();
|
||||||
|
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce } }).then(
|
||||||
|
(res) => res.data
|
||||||
|
);
|
||||||
|
localUserStore.setAuthToken(authToken);
|
||||||
|
this.authToken = authToken;
|
||||||
|
room = await Room.createRoom(
|
||||||
|
new URL(localUserStore.getLastRoomUrl())
|
||||||
|
);
|
||||||
|
urlManager.pushRoomIdToUrl(room);
|
||||||
|
|
||||||
|
} else if (connexionType === GameConnexionTypes.register) {
|
||||||
|
//@deprecated
|
||||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||||
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
|
||||||
(res) => res.data
|
(res) => res.data
|
||||||
);
|
);
|
||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
|
this.localUser = new LocalUser(data.userUuid, data.textures);
|
||||||
|
this.authToken = data.authToken;
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
|
localUserStore.setAuthToken(this.authToken);
|
||||||
|
|
||||||
const roomUrl = data.roomUrl;
|
const roomUrl = data.roomUrl;
|
||||||
|
|
||||||
@ -61,24 +98,12 @@ class ConnectionManager {
|
|||||||
connexionType === GameConnexionTypes.anonymous ||
|
connexionType === GameConnexionTypes.anonymous ||
|
||||||
connexionType === GameConnexionTypes.empty
|
connexionType === GameConnexionTypes.empty
|
||||||
) {
|
) {
|
||||||
let localUser = localUserStore.getLocalUser();
|
this.authToken = localUserStore.getAuthToken();
|
||||||
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
//todo: add here some kind of warning if authToken has expired.
|
||||||
this.localUser = localUser;
|
if (!this.authToken) {
|
||||||
try {
|
|
||||||
await this.verifyToken(localUser.jwtToken);
|
|
||||||
} catch (e) {
|
|
||||||
// If the token is invalid, let's generate an anonymous one.
|
|
||||||
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
|
|
||||||
await this.anonymousLogin();
|
await this.anonymousLogin();
|
||||||
}
|
}
|
||||||
} else {
|
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
|
||||||
await this.anonymousLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
localUser = localUserStore.getLocalUser();
|
|
||||||
if (!localUser) {
|
|
||||||
throw "Error to store local user data";
|
|
||||||
}
|
|
||||||
|
|
||||||
let roomPath: string;
|
let roomPath: string;
|
||||||
if (connexionType === GameConnexionTypes.empty) {
|
if (connexionType === GameConnexionTypes.empty) {
|
||||||
@ -97,19 +122,18 @@ class ConnectionManager {
|
|||||||
room = await Room.createRoom(new URL(roomPath));
|
room = await Room.createRoom(new URL(roomPath));
|
||||||
if (room.textures != undefined && room.textures.length > 0) {
|
if (room.textures != undefined && room.textures.length > 0) {
|
||||||
//check if texture was changed
|
//check if texture was changed
|
||||||
if (localUser.textures.length === 0) {
|
if (this.localUser.textures.length === 0) {
|
||||||
localUser.textures = room.textures;
|
this.localUser.textures = room.textures;
|
||||||
} else {
|
} else {
|
||||||
room.textures.forEach((newTexture) => {
|
room.textures.forEach((newTexture) => {
|
||||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
|
||||||
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
localUser?.textures.push(newTexture);
|
this.localUser.textures.push(newTexture);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.localUser = localUser;
|
localUserStore.saveUser(this.localUser);
|
||||||
localUserStore.saveUser(localUser);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (room == undefined) {
|
if (room == undefined) {
|
||||||
@ -120,21 +144,19 @@ class ConnectionManager {
|
|||||||
return Promise.resolve(room);
|
return Promise.resolve(room);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyToken(token: string): Promise<void> {
|
|
||||||
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
|
||||||
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||||
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
|
this.localUser = new LocalUser(data.userUuid, []);
|
||||||
|
this.authToken = data.authToken;
|
||||||
if (!isBenchmark) {
|
if (!isBenchmark) {
|
||||||
// In benchmark, we don't have a local storage.
|
// In benchmark, we don't have a local storage.
|
||||||
localUserStore.saveUser(this.localUser);
|
localUserStore.saveUser(this.localUser);
|
||||||
|
localUserStore.setAuthToken(this.authToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public initBenchmark(): void {
|
public initBenchmark(): void {
|
||||||
this.localUser = new LocalUser("", "test", []);
|
this.localUser = new LocalUser("", []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public connectToRoomSocket(
|
public connectToRoomSocket(
|
||||||
@ -147,7 +169,7 @@ class ConnectionManager {
|
|||||||
): Promise<OnConnectInterface> {
|
): Promise<OnConnectInterface> {
|
||||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||||
const connection = new RoomConnection(
|
const connection = new RoomConnection(
|
||||||
this.localUser.jwtToken,
|
this.authToken,
|
||||||
roomUrl,
|
roomUrl,
|
||||||
name,
|
name,
|
||||||
characterLayers,
|
characterLayers,
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
|
import { MAX_USERNAME_LENGTH } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
export interface CharacterTexture {
|
export interface CharacterTexture {
|
||||||
id: number,
|
id: number;
|
||||||
level: number,
|
level: number;
|
||||||
url: string,
|
url: string;
|
||||||
rights: string
|
rights: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
||||||
@ -24,6 +24,5 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LocalUser {
|
export class LocalUser {
|
||||||
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
|
constructor(public readonly uuid: string, public textures: CharacterTexture[]) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
|
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
const playerNameKey = "playerName";
|
const playerNameKey = "playerName";
|
||||||
const selectedPlayerKey = "selectedPlayer";
|
const selectedPlayerKey = "selectedPlayer";
|
||||||
@ -12,6 +13,9 @@ const audioPlayerMuteKey = "audioMute";
|
|||||||
const helpCameraSettingsShown = "helpCameraSettingsShown";
|
const helpCameraSettingsShown = "helpCameraSettingsShown";
|
||||||
const fullscreenKey = "fullscreen";
|
const fullscreenKey = "fullscreen";
|
||||||
const lastRoomUrl = "lastRoomUrl";
|
const lastRoomUrl = "lastRoomUrl";
|
||||||
|
const authToken = "authToken";
|
||||||
|
const state = "state";
|
||||||
|
const nonce = "nonce";
|
||||||
|
|
||||||
class LocalUserStore {
|
class LocalUserStore {
|
||||||
saveUser(localUser: LocalUser) {
|
saveUser(localUser: LocalUser) {
|
||||||
@ -116,6 +120,36 @@ class LocalUserStore {
|
|||||||
getLastRoomUrl(): string {
|
getLastRoomUrl(): string {
|
||||||
return localStorage.getItem(lastRoomUrl) ?? "";
|
return localStorage.getItem(lastRoomUrl) ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setAuthToken(value: string | null) {
|
||||||
|
value ? localStorage.setItem(authToken, value) : localStorage.removeItem(authToken);
|
||||||
|
}
|
||||||
|
getAuthToken(): string | null {
|
||||||
|
return localStorage.getItem(authToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateState(): string {
|
||||||
|
const newState = uuidv4();
|
||||||
|
localStorage.setItem(state, newState);
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyState(value: string): boolean {
|
||||||
|
const oldValue = localStorage.getItem(state);
|
||||||
|
localStorage.removeItem(state);
|
||||||
|
return oldValue === value;
|
||||||
|
}
|
||||||
|
generateNonce(): string {
|
||||||
|
const newNonce = uuidv4();
|
||||||
|
localStorage.setItem(nonce, newNonce);
|
||||||
|
return newNonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNonce(): string | null {
|
||||||
|
const oldValue = localStorage.getItem(nonce);
|
||||||
|
localStorage.removeItem(nonce);
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const localUserStore = new LocalUserStore();
|
export const localUserStore = new LocalUserStore();
|
||||||
|
@ -76,7 +76,7 @@ export class RoomConnection implements RoomConnection {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param token A JWT token containing the UUID of the user
|
* @param token A JWT token containing the email of the user
|
||||||
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
|
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -217,6 +217,9 @@ export class RoomConnection implements RoomConnection {
|
|||||||
} else if (message.hasWorldfullmessage()) {
|
} else if (message.hasWorldfullmessage()) {
|
||||||
worldFullMessageStream.onMessage();
|
worldFullMessageStream.onMessage();
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
|
} else if (message.hasTokenexpiredmessage()) {
|
||||||
|
connectionManager.loadOpenIDScreen();
|
||||||
|
this.closed = true; //technically, this isn't needed since loadOpenIDScreen() will do window.location.assign() but I prefer to leave it for consistency
|
||||||
} else if (message.hasWorldconnexionmessage()) {
|
} else if (message.hasWorldconnexionmessage()) {
|
||||||
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
|
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
||||||
const START_ROOM_URL: string =
|
const START_ROOM_URL: string =
|
||||||
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor0/floor0.json";
|
process.env.START_ROOM_URL || "/_/global/maps.workadventure.localhost/Floor1/floor1.json";
|
||||||
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
|
const PUSHER_URL = process.env.PUSHER_URL || "//pusher.workadventure.localhost";
|
||||||
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
|
export const ADMIN_URL = process.env.ADMIN_URL || "//workadventu.re";
|
||||||
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
|
const UPLOADER_URL = process.env.UPLOADER_URL || "//uploader.workadventure.localhost";
|
||||||
|
@ -344,6 +344,9 @@ export class MenuScene extends Phaser.Scene {
|
|||||||
case "editGameSettingsButton":
|
case "editGameSettingsButton":
|
||||||
this.openGameSettingsMenu();
|
this.openGameSettingsMenu();
|
||||||
break;
|
break;
|
||||||
|
case "oidcLogin":
|
||||||
|
connectionManager.loadOpenIDScreen();
|
||||||
|
break;
|
||||||
case "toggleFullscreen":
|
case "toggleFullscreen":
|
||||||
this.toggleFullscreen();
|
this.toggleFullscreen();
|
||||||
break;
|
break;
|
||||||
|
@ -1,45 +1,46 @@
|
|||||||
import type {Room} from "../Connexion/Room";
|
import type { Room } from "../Connexion/Room";
|
||||||
|
|
||||||
export enum GameConnexionTypes {
|
export enum GameConnexionTypes {
|
||||||
anonymous=1,
|
anonymous = 1,
|
||||||
organization,
|
organization,
|
||||||
register,
|
register,
|
||||||
empty,
|
empty,
|
||||||
unknown,
|
unknown,
|
||||||
|
jwt,
|
||||||
}
|
}
|
||||||
|
|
||||||
//this class is responsible with analysing and editing the game's url
|
//this class is responsible with analysing and editing the game's url
|
||||||
class UrlManager {
|
class UrlManager {
|
||||||
|
|
||||||
//todo: use that to detect if we can find a token in localstorage
|
|
||||||
public getGameConnexionType(): GameConnexionTypes {
|
public getGameConnexionType(): GameConnexionTypes {
|
||||||
const url = window.location.pathname.toString();
|
const url = window.location.pathname.toString();
|
||||||
if (url.includes('_/')) {
|
if (url === "/jwt") {
|
||||||
|
return GameConnexionTypes.jwt;
|
||||||
|
} else if (url.includes("_/")) {
|
||||||
return GameConnexionTypes.anonymous;
|
return GameConnexionTypes.anonymous;
|
||||||
} else if (url.includes('@/')) {
|
} else if (url.includes("@/")) {
|
||||||
return GameConnexionTypes.organization;
|
return GameConnexionTypes.organization;
|
||||||
} else if(url.includes('register/')) {
|
} else if (url.includes("register/")) {
|
||||||
return GameConnexionTypes.register;
|
return GameConnexionTypes.register;
|
||||||
} else if(url === '/') {
|
} else if (url === "/") {
|
||||||
return GameConnexionTypes.empty;
|
return GameConnexionTypes.empty;
|
||||||
} else {
|
} else {
|
||||||
return GameConnexionTypes.unknown;
|
return GameConnexionTypes.unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOrganizationToken(): string|null {
|
public getOrganizationToken(): string | null {
|
||||||
const match = /\/register\/(.+)/.exec(window.location.pathname.toString());
|
const match = /\/register\/(.+)/.exec(window.location.pathname.toString());
|
||||||
return match ? match [1] : null;
|
return match ? match[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public pushRoomIdToUrl(room:Room): void {
|
public pushRoomIdToUrl(room: Room): void {
|
||||||
if (window.location.pathname === room.id) return;
|
if (window.location.pathname === room.id) return;
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
const search = room.search.toString();
|
const search = room.search.toString();
|
||||||
history.pushState({}, 'WorkAdventure', room.id+(search?'?'+search:'')+hash);
|
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStartLayerNameFromUrl(): string|null {
|
public getStartLayerNameFromUrl(): string | null {
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
return hash.length > 1 ? hash.substring(1) : null;
|
return hash.length > 1 ? hash.substring(1) : null;
|
||||||
}
|
}
|
||||||
|
@ -291,6 +291,18 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
|
"@types/uuid@8.3.0":
|
||||||
|
version "8.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
||||||
|
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
|
||||||
|
|
||||||
|
"@types/uuidv4@^5.0.0":
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuidv4/-/uuidv4-5.0.0.tgz#2c94e67b0c06d5adb28fb7ced1a1b5f0866ecd50"
|
||||||
|
integrity sha512-xUrhYSJnkTq9CP79cU3svoKTLPCIbMMnu9Twf/tMpHATYSHCAAeDNeb2a/29YORhk5p4atHhCTMsIBU/tvdh6A==
|
||||||
|
dependencies:
|
||||||
|
uuidv4 "*"
|
||||||
|
|
||||||
"@types/webpack-dev-server@^3.11.4":
|
"@types/webpack-dev-server@^3.11.4":
|
||||||
version "3.11.4"
|
version "3.11.4"
|
||||||
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07"
|
resolved "https://registry.yarnpkg.com/@types/webpack-dev-server/-/webpack-dev-server-3.11.4.tgz#90d47dd660b696d409431ab8c1e9fa3615103a07"
|
||||||
@ -5775,11 +5787,24 @@ utils-merge@1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||||
|
|
||||||
|
uuid@8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
uuid@^3.3.2, uuid@^3.4.0:
|
uuid@^3.3.2, uuid@^3.4.0:
|
||||||
version "3.4.0"
|
version "3.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
|
uuidv4@*, uuidv4@^6.2.10:
|
||||||
|
version "6.2.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.10.tgz#42fc1c12b6f85ad536c2c5c1e836079d1e15003c"
|
||||||
|
integrity sha512-FMo1exd9l5UvoUPHRR6NrtJ/OJRePh0ca7IhPwBuMNuYRqjtuh8lE3WDxAUvZ4Yss5FbCOsPFjyWJf9lVTEmnw==
|
||||||
|
dependencies:
|
||||||
|
"@types/uuid" "8.3.0"
|
||||||
|
uuid "8.3.2"
|
||||||
|
|
||||||
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
|
v8-compile-cache@^2.0.3, v8-compile-cache@^2.2.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||||
|
@ -247,6 +247,8 @@ message RefreshRoomMessage{
|
|||||||
|
|
||||||
message WorldFullMessage{
|
message WorldFullMessage{
|
||||||
}
|
}
|
||||||
|
message TokenExpiredMessage{
|
||||||
|
}
|
||||||
|
|
||||||
message WorldConnexionMessage{
|
message WorldConnexionMessage{
|
||||||
string message = 2;
|
string message = 2;
|
||||||
@ -278,6 +280,7 @@ message ServerToClientMessage {
|
|||||||
RefreshRoomMessage refreshRoomMessage = 17;
|
RefreshRoomMessage refreshRoomMessage = 17;
|
||||||
WorldConnexionMessage worldConnexionMessage = 18;
|
WorldConnexionMessage worldConnexionMessage = 18;
|
||||||
EmoteEventMessage emoteEventMessage = 19;
|
EmoteEventMessage emoteEventMessage = 19;
|
||||||
|
TokenExpiredMessage tokenExpiredMessage = 20;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
"grpc": "^1.24.4",
|
"grpc": "^1.24.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
|
"openid-client": "^4.7.4",
|
||||||
"prom-client": "^12.0.0",
|
"prom-client": "^12.0.0",
|
||||||
"query-string": "^6.13.3",
|
"query-string": "^6.13.3",
|
||||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||||
|
@ -4,6 +4,7 @@ import { BaseController } from "./BaseController";
|
|||||||
import { adminApi } from "../Services/AdminApi";
|
import { adminApi } from "../Services/AdminApi";
|
||||||
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
|
import { openIDClient } from "../Services/OpenIDClient";
|
||||||
|
|
||||||
export interface TokenInterface {
|
export interface TokenInterface {
|
||||||
userUuid: string;
|
userUuid: string;
|
||||||
@ -12,11 +13,58 @@ export interface TokenInterface {
|
|||||||
export class AuthenticateController extends BaseController {
|
export class AuthenticateController extends BaseController {
|
||||||
constructor(private App: TemplatedApp) {
|
constructor(private App: TemplatedApp) {
|
||||||
super();
|
super();
|
||||||
|
this.openIDLogin();
|
||||||
|
this.openIDCallback();
|
||||||
this.register();
|
this.register();
|
||||||
this.verify();
|
|
||||||
this.anonymLogin();
|
this.anonymLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openIDLogin() {
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
this.App.get("/login-screen", async (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn("/message request was aborted");
|
||||||
|
});
|
||||||
|
|
||||||
|
const { nonce, state } = parse(req.getQuery());
|
||||||
|
if (!state || !nonce) {
|
||||||
|
res.writeStatus("400 Unauthorized").end("missing state and nonce URL parameters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const loginUri = await openIDClient.authorizationUrl(state as string, nonce as string);
|
||||||
|
res.writeStatus("302");
|
||||||
|
res.writeHeader("Location", loginUri);
|
||||||
|
return res.end();
|
||||||
|
} catch (e) {
|
||||||
|
return this.errorToResponse(e, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openIDCallback() {
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
this.App.get("/login-callback", async (res: HttpResponse, req: HttpRequest) => {
|
||||||
|
res.onAborted(() => {
|
||||||
|
console.warn("/message request was aborted");
|
||||||
|
});
|
||||||
|
const { code, nonce } = parse(req.getQuery());
|
||||||
|
try {
|
||||||
|
const userInfo = await openIDClient.getUserInfo(code as string, nonce as string);
|
||||||
|
const email = userInfo.email || userInfo.sub;
|
||||||
|
if (!email) {
|
||||||
|
throw new Error("No email in the response");
|
||||||
|
}
|
||||||
|
const authToken = jwtTokenManager.createAuthToken(email);
|
||||||
|
res.writeStatus("200");
|
||||||
|
this.addCorsHeaders(res);
|
||||||
|
return res.end(JSON.stringify({ authToken }));
|
||||||
|
} catch (e) {
|
||||||
|
return this.errorToResponse(e, res);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//Try to login with an admin token
|
//Try to login with an admin token
|
||||||
private register() {
|
private register() {
|
||||||
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
|
||||||
@ -39,11 +87,12 @@ export class AuthenticateController extends BaseController {
|
|||||||
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
|
||||||
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
|
||||||
const userUuid = data.userUuid;
|
const userUuid = data.userUuid;
|
||||||
|
const email = data.email;
|
||||||
const roomUrl = data.roomUrl;
|
const roomUrl = data.roomUrl;
|
||||||
const mapUrlStart = data.mapUrlStart;
|
const mapUrlStart = data.mapUrlStart;
|
||||||
const textures = data.textures;
|
const textures = data.textures;
|
||||||
|
|
||||||
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
const authToken = jwtTokenManager.createAuthToken(email || userUuid);
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end(
|
res.end(
|
||||||
@ -63,45 +112,6 @@ export class AuthenticateController extends BaseController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private verify() {
|
|
||||||
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => {
|
|
||||||
(async () => {
|
|
||||||
const query = parse(req.getQuery());
|
|
||||||
|
|
||||||
res.onAborted(() => {
|
|
||||||
console.warn("verify request was aborted");
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
await jwtTokenManager.getUserUuidFromToken(query.token as string);
|
|
||||||
} catch (e) {
|
|
||||||
res.writeStatus("400 Bad Request");
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: false,
|
|
||||||
message: "Invalid JWT token",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.writeStatus("200 OK");
|
|
||||||
this.addCorsHeaders(res);
|
|
||||||
res.end(
|
|
||||||
JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//permit to login on application. Return token to connect on Websocket IO.
|
//permit to login on application. Return token to connect on Websocket IO.
|
||||||
private anonymLogin() {
|
private anonymLogin() {
|
||||||
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
|
||||||
@ -115,7 +125,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const userUuid = v4();
|
const userUuid = v4();
|
||||||
const authToken = jwtTokenManager.createJWTToken(userUuid);
|
const authToken = jwtTokenManager.createAuthToken(userUuid);
|
||||||
res.writeStatus("200 OK");
|
res.writeStatus("200 OK");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
res.end(
|
res.end(
|
||||||
|
@ -22,7 +22,7 @@ import {
|
|||||||
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
import { UserMovesMessage } from "../Messages/generated/messages_pb";
|
||||||
import { TemplatedApp } from "uWebSockets.js";
|
import { TemplatedApp } from "uWebSockets.js";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { jwtTokenManager } from "../Services/JWTTokenManager";
|
import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
|
||||||
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";
|
||||||
@ -173,31 +173,34 @@ export class IoSocketController {
|
|||||||
characterLayers = [characterLayers];
|
characterLayers = [characterLayers];
|
||||||
}
|
}
|
||||||
|
|
||||||
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId);
|
const tokenData =
|
||||||
|
token && typeof token === "string" ? jwtTokenManager.decodeJWTToken(token) : null;
|
||||||
|
const userIdentifier = tokenData ? tokenData.identifier : "";
|
||||||
|
|
||||||
let memberTags: string[] = [];
|
let memberTags: string[] = [];
|
||||||
let memberVisitCardUrl: string | null = null;
|
let memberVisitCardUrl: string | null = null;
|
||||||
let memberMessages: unknown;
|
let memberMessages: unknown;
|
||||||
let memberTextures: CharacterTexture[] = [];
|
let memberTextures: CharacterTexture[] = [];
|
||||||
const room = await socketManager.getOrCreateRoom(roomId);
|
const room = await socketManager.getOrCreateRoom(roomId);
|
||||||
if (ADMIN_API_URL) {
|
|
||||||
try {
|
|
||||||
let userData: FetchMemberDataByUuidResponse = {
|
let userData: FetchMemberDataByUuidResponse = {
|
||||||
uuid: v4(),
|
userUuid: userIdentifier,
|
||||||
tags: [],
|
tags: [],
|
||||||
visitCardUrl: null,
|
visitCardUrl: null,
|
||||||
textures: [],
|
textures: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
anonymous: true,
|
anonymous: true,
|
||||||
};
|
};
|
||||||
|
if (ADMIN_API_URL) {
|
||||||
try {
|
try {
|
||||||
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId);
|
try {
|
||||||
|
userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err?.response?.status == 404) {
|
if (err?.response?.status == 404) {
|
||||||
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
|
||||||
|
|
||||||
console.warn(
|
console.warn(
|
||||||
'Cannot find user with uuid "' +
|
'Cannot find user with email "' +
|
||||||
userUuid +
|
(userIdentifier || "anonymous") +
|
||||||
'". Performing an anonymous login instead.'
|
'". Performing an anonymous login instead.'
|
||||||
);
|
);
|
||||||
} else if (err?.response?.status == 403) {
|
} else if (err?.response?.status == 403) {
|
||||||
@ -235,7 +238,12 @@ export class IoSocketController {
|
|||||||
throw new Error("Use the login URL to connect");
|
throw new Error("Use the login URL to connect");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("access not granted for user " + userUuid + " and room " + roomId);
|
console.log(
|
||||||
|
"access not granted for user " +
|
||||||
|
(userIdentifier || "anonymous") +
|
||||||
|
" and room " +
|
||||||
|
roomId
|
||||||
|
);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new Error("User cannot access this world");
|
throw new Error("User cannot access this world");
|
||||||
}
|
}
|
||||||
@ -257,7 +265,7 @@ export class IoSocketController {
|
|||||||
// Data passed here is accessible on the "websocket" socket object.
|
// Data passed here is accessible on the "websocket" socket object.
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
userUuid,
|
userUuid: userData.userUuid,
|
||||||
IPAddress,
|
IPAddress,
|
||||||
roomId,
|
roomId,
|
||||||
name,
|
name,
|
||||||
@ -287,15 +295,10 @@ export class IoSocketController {
|
|||||||
context
|
context
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/*if (e instanceof Error) {
|
res.upgrade(
|
||||||
console.log(e.message);
|
|
||||||
res.writeStatus("401 Unauthorized").end(e.message);
|
|
||||||
} else {
|
|
||||||
res.writeStatus("500 Internal Server Error").end('An error occurred');
|
|
||||||
}*/
|
|
||||||
return res.upgrade(
|
|
||||||
{
|
{
|
||||||
rejected: true,
|
rejected: true,
|
||||||
|
reason: e.reason || null,
|
||||||
message: e.message ? e.message : "500 Internal Server Error",
|
message: e.message ? e.message : "500 Internal Server Error",
|
||||||
},
|
},
|
||||||
websocketKey,
|
websocketKey,
|
||||||
@ -310,12 +313,14 @@ export class IoSocketController {
|
|||||||
open: (ws) => {
|
open: (ws) => {
|
||||||
if (ws.rejected === true) {
|
if (ws.rejected === true) {
|
||||||
//FIX ME to use status code
|
//FIX ME to use status code
|
||||||
if (ws.message === "World is full") {
|
if (ws.reason === tokenInvalidException) {
|
||||||
|
socketManager.emitTokenExpiredMessage(ws);
|
||||||
|
} else if (ws.message === "World is full") {
|
||||||
socketManager.emitWorldFullMessage(ws);
|
socketManager.emitWorldFullMessage(ws);
|
||||||
} else {
|
} else {
|
||||||
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
|
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
|
||||||
}
|
}
|
||||||
ws.close();
|
setTimeout(() => ws.close(), 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
|
||||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
|
||||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
|
||||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
|
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
|
||||||
const API_URL = process.env.API_URL || "";
|
const API_URL = process.env.API_URL || "";
|
||||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
|
||||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
|
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
|
||||||
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || "") || 600;
|
|
||||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
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_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL;
|
||||||
const JITSI_ISS = process.env.JITSI_ISS || "";
|
const JITSI_ISS = process.env.JITSI_ISS || "";
|
||||||
@ -13,14 +10,16 @@ const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || "";
|
|||||||
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080;
|
const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080;
|
||||||
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
|
||||||
|
|
||||||
|
export const FRONT_URL = process.env.FRONT_URL || "http://localhost";
|
||||||
|
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 {
|
export {
|
||||||
SECRET_KEY,
|
SECRET_KEY,
|
||||||
MINIMUM_DISTANCE,
|
|
||||||
API_URL,
|
API_URL,
|
||||||
ADMIN_API_URL,
|
ADMIN_API_URL,
|
||||||
ADMIN_API_TOKEN,
|
ADMIN_API_TOKEN,
|
||||||
MAX_USERS_PER_ROOM,
|
|
||||||
GROUP_RADIUS,
|
|
||||||
ALLOW_ARTILLERY,
|
ALLOW_ARTILLERY,
|
||||||
CPU_OVERHEAT_THRESHOLD,
|
CPU_OVERHEAT_THRESHOLD,
|
||||||
JITSI_URL,
|
JITSI_URL,
|
||||||
|
@ -7,6 +7,7 @@ import { RoomRedirect } from "./AdminApi/RoomRedirect";
|
|||||||
|
|
||||||
export interface AdminApiData {
|
export interface AdminApiData {
|
||||||
roomUrl: string;
|
roomUrl: string;
|
||||||
|
email: string | null;
|
||||||
mapUrlStart: string;
|
mapUrlStart: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
policy_type: number;
|
policy_type: number;
|
||||||
@ -21,7 +22,7 @@ export interface AdminBannedData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface FetchMemberDataByUuidResponse {
|
export interface FetchMemberDataByUuidResponse {
|
||||||
uuid: string;
|
userUuid: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
visitCardUrl: string | null;
|
visitCardUrl: string | null;
|
||||||
textures: CharacterTexture[];
|
textures: CharacterTexture[];
|
||||||
@ -46,12 +47,16 @@ class AdminApi {
|
|||||||
return res.data;
|
return res.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMemberDataByUuid(uuid: string, roomId: string): Promise<FetchMemberDataByUuidResponse> {
|
async fetchMemberDataByUuid(
|
||||||
|
userIdentifier: string | null,
|
||||||
|
roomId: string,
|
||||||
|
ipAddress: string
|
||||||
|
): Promise<FetchMemberDataByUuidResponse> {
|
||||||
if (!ADMIN_API_URL) {
|
if (!ADMIN_API_URL) {
|
||||||
return Promise.reject(new Error("No admin backoffice set!"));
|
return Promise.reject(new Error("No admin backoffice set!"));
|
||||||
}
|
}
|
||||||
const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
|
const res = await Axios.get(ADMIN_API_URL + "/api/room/access", {
|
||||||
params: { uuid, roomId },
|
params: { userIdentifier, roomId, ipAddress },
|
||||||
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
headers: { Authorization: `${ADMIN_API_TOKEN}` },
|
||||||
});
|
});
|
||||||
return res.data;
|
return res.data;
|
||||||
|
@ -1,100 +1,25 @@
|
|||||||
import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
|
import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable";
|
||||||
import { uuid } from "uuidv4";
|
import { uuid } from "uuidv4";
|
||||||
import Jwt from "jsonwebtoken";
|
import Jwt, { verify } from "jsonwebtoken";
|
||||||
import { TokenInterface } from "../Controller/AuthenticateController";
|
import { TokenInterface } from "../Controller/AuthenticateController";
|
||||||
import { adminApi, AdminBannedData } from "../Services/AdminApi";
|
import { adminApi, AdminBannedData } from "../Services/AdminApi";
|
||||||
|
|
||||||
|
export interface AuthTokenData {
|
||||||
|
identifier: string; //will be a email if logged in or an uuid if anonymous
|
||||||
|
}
|
||||||
|
export const tokenInvalidException = "tokenInvalid";
|
||||||
|
|
||||||
class JWTTokenManager {
|
class JWTTokenManager {
|
||||||
public createJWTToken(userUuid: string) {
|
public createAuthToken(identifier: string) {
|
||||||
return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token
|
return Jwt.sign({ identifier }, SECRET_KEY, { expiresIn: "3d" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserUuidFromToken(token: unknown, ipAddress?: string, roomUrl?: string): Promise<string> {
|
public decodeJWTToken(token: string): AuthTokenData {
|
||||||
if (!token) {
|
try {
|
||||||
throw new Error("An authentication error happened, a user tried to connect without a token.");
|
return Jwt.verify(token, SECRET_KEY, { ignoreExpiration: false }) as AuthTokenData;
|
||||||
|
} catch (e) {
|
||||||
|
throw { reason: tokenInvalidException, message: e.message };
|
||||||
}
|
}
|
||||||
if (typeof token !== "string") {
|
|
||||||
throw new Error("Token is expected to be a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token === "test") {
|
|
||||||
if (ALLOW_ARTILLERY) {
|
|
||||||
return uuid();
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => {
|
|
||||||
const tokenInterface = tokenDecoded as TokenInterface;
|
|
||||||
if (err) {
|
|
||||||
console.error("An authentication error happened, invalid JsonWebToken.", err);
|
|
||||||
reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (tokenDecoded === undefined) {
|
|
||||||
console.error("Empty token found.");
|
|
||||||
reject(new Error("Empty token found."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//verify token
|
|
||||||
if (!this.isValidToken(tokenInterface)) {
|
|
||||||
reject(new Error("Authentication error, invalid token structure."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ADMIN_API_URL) {
|
|
||||||
//verify user in admin
|
|
||||||
let promise = new Promise((resolve) => resolve());
|
|
||||||
if (ipAddress && roomUrl) {
|
|
||||||
promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, roomUrl);
|
|
||||||
}
|
|
||||||
promise
|
|
||||||
.then(() => {
|
|
||||||
adminApi
|
|
||||||
.fetchCheckUserByToken(tokenInterface.userUuid)
|
|
||||||
.then(() => {
|
|
||||||
resolve(tokenInterface.userUuid);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
//anonymous user
|
|
||||||
if (err.response && err.response.status && err.response.status === 404) {
|
|
||||||
resolve(tokenInterface.userUuid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(tokenInterface.userUuid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private verifyBanUser(userUuid: string, ipAddress: string, roomUrl: string): Promise<AdminBannedData> {
|
|
||||||
return adminApi
|
|
||||||
.verifyBanUser(userUuid, ipAddress, roomUrl)
|
|
||||||
.then((data: AdminBannedData) => {
|
|
||||||
if (data && data.is_banned) {
|
|
||||||
throw new Error("User was banned");
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private isValidToken(token: object): token is TokenInterface {
|
|
||||||
return !(typeof (token as TokenInterface).userUuid !== "string");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
43
pusher/src/Services/OpenIDClient.ts
Normal file
43
pusher/src/Services/OpenIDClient.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Issuer, Client } from "openid-client";
|
||||||
|
import { OPID_CLIENT_ID, OPID_CLIENT_SECRET, OPID_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
|
const opidRedirectUri = FRONT_URL + "/jwt";
|
||||||
|
|
||||||
|
class OpenIDClient {
|
||||||
|
private issuerPromise: Promise<Client> | null = null;
|
||||||
|
|
||||||
|
private initClient(): Promise<Client> {
|
||||||
|
if (!this.issuerPromise) {
|
||||||
|
this.issuerPromise = Issuer.discover(OPID_CLIENT_ISSUER).then((issuer) => {
|
||||||
|
return new issuer.Client({
|
||||||
|
client_id: OPID_CLIENT_ID,
|
||||||
|
client_secret: OPID_CLIENT_SECRET,
|
||||||
|
redirect_uris: [opidRedirectUri],
|
||||||
|
response_types: ["code"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.issuerPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
public authorizationUrl(state: string, nonce: string) {
|
||||||
|
return this.initClient().then((client) => {
|
||||||
|
return client.authorizationUrl({
|
||||||
|
scope: "openid email",
|
||||||
|
prompt: "login",
|
||||||
|
state: state,
|
||||||
|
nonce: nonce,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string }> {
|
||||||
|
return this.initClient().then((client) => {
|
||||||
|
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
|
||||||
|
return client.userinfo(tokenSet);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openIDClient = new OpenIDClient();
|
@ -28,6 +28,7 @@ import {
|
|||||||
UserLeftRoomMessage,
|
UserLeftRoomMessage,
|
||||||
AdminMessage,
|
AdminMessage,
|
||||||
BanMessage,
|
BanMessage,
|
||||||
|
TokenExpiredMessage,
|
||||||
RefreshRoomMessage,
|
RefreshRoomMessage,
|
||||||
EmotePromptMessage,
|
EmotePromptMessage,
|
||||||
VariableMessage,
|
VariableMessage,
|
||||||
@ -117,7 +118,7 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
console.warn("Admin connection lost to back server");
|
console.warn("Admin connection lost to back server");
|
||||||
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
|
// Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start.
|
||||||
if (!client.disconnecting) {
|
if (!client.disconnecting) {
|
||||||
this.closeWebsocketConnection(client, 1011, "Connection lost to back server");
|
this.closeWebsocketConnection(client, 1011, "Admin Connection lost to back server");
|
||||||
}
|
}
|
||||||
console.log("A user left");
|
console.log("A user left");
|
||||||
})
|
})
|
||||||
@ -140,24 +141,6 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getAdminSocketDataFor(roomId: string): AdminSocketData {
|
|
||||||
throw new Error("Not reimplemented yet");
|
|
||||||
/*const data:AdminSocketData = {
|
|
||||||
rooms: {},
|
|
||||||
users: {},
|
|
||||||
}
|
|
||||||
const room = this.Worlds.get(roomId);
|
|
||||||
if (room === undefined) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
const users = room.getUsers();
|
|
||||||
data.rooms[roomId] = users.size;
|
|
||||||
users.forEach(user => {
|
|
||||||
data.users[user.uuid] = true
|
|
||||||
})
|
|
||||||
return data;*/
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleJoinRoom(client: ExSocketInterface): Promise<void> {
|
async handleJoinRoom(client: ExSocketInterface): Promise<void> {
|
||||||
const viewport = client.viewport;
|
const viewport = client.viewport;
|
||||||
try {
|
try {
|
||||||
@ -598,8 +581,20 @@ export class SocketManager implements ZoneEventListener {
|
|||||||
const serverToClientMessage = new ServerToClientMessage();
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
serverToClientMessage.setWorldfullmessage(errorMessage);
|
serverToClientMessage.setWorldfullmessage(errorMessage);
|
||||||
|
|
||||||
|
if (!client.disconnecting) {
|
||||||
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
public emitTokenExpiredMessage(client: WebSocket) {
|
||||||
|
const errorMessage = new TokenExpiredMessage();
|
||||||
|
|
||||||
|
const serverToClientMessage = new ServerToClientMessage();
|
||||||
|
serverToClientMessage.setTokenexpiredmessage(errorMessage);
|
||||||
|
|
||||||
|
if (!client.disconnecting) {
|
||||||
|
client.send(serverToClientMessage.serializeBinary().buffer, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public emitConnexionErrorMessage(client: WebSocket, message: string) {
|
public emitConnexionErrorMessage(client: WebSocket, message: string) {
|
||||||
const errorMessage = new WorldConnexionMessage();
|
const errorMessage = new WorldConnexionMessage();
|
||||||
|
1820
pusher/yarn.lock
1820
pusher/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user