diff --git a/.env.template b/.env.template index 715ebeec..5328fe08 100644 --- a/.env.template +++ b/.env.template @@ -22,6 +22,10 @@ MAX_USERNAME_LENGTH=8 OPID_CLIENT_ID= OPID_CLIENT_SECRET= OPID_CLIENT_ISSUER= +OPID_CLIENT_REDIREC_URL= +OPID_LOGIN_SCREEN_PROVIDER=http://pusher.workadventure.localhost/login-screen +OPID_PROFILE_SCREEN_PROVIDER= +DISABLE_ANONYMOUS= # If you want to have a contact page in your menu, you MUST set CONTACT_URL to the URL of the page that you want CONTACT_URL= \ No newline at end of file diff --git a/docker-compose.single-domain.yaml b/docker-compose.single-domain.yaml index 4e85d702..e241c108 100644 --- a/docker-compose.single-domain.yaml +++ b/docker-compose.single-domain.yaml @@ -40,6 +40,7 @@ services: TURN_USER: "" TURN_PASSWORD: "" START_ROOM_URL: "$START_ROOM_URL" + DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS" command: yarn run start volumes: - ./front:/usr/src/app @@ -70,6 +71,9 @@ services: OPID_CLIENT_ID: $OPID_CLIENT_ID OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER + OPID_CLIENT_REDIREC_URL: $OPID_CLIENT_REDIREC_URL + OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER + DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS volumes: - ./pusher:/usr/src/app labels: diff --git a/docker-compose.yaml b/docker-compose.yaml index f68ed6a0..03395f22 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,6 +43,8 @@ services: START_ROOM_URL: "$START_ROOM_URL" MAX_PER_GROUP: "$MAX_PER_GROUP" MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" + DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS" + OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER" command: yarn run start volumes: - ./front:/usr/src/app @@ -71,6 +73,9 @@ services: OPID_CLIENT_ID: $OPID_CLIENT_ID OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER + OPID_CLIENT_REDIREC_URL: $OPID_CLIENT_REDIREC_URL + OPID_PROFILE_SCREEN_PROVIDER: $OPID_PROFILE_SCREEN_PROVIDER + DISABLE_ANONYMOUS: $DISABLE_ANONYMOUS volumes: - ./pusher:/usr/src/app labels: diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 1f844bf2..793831bf 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -212,6 +212,8 @@ class ConnectionManager { analyticsClient.identifyUser(this.localUser.uuid, this.localUser.email); } + //clean history with new URL + window.history.pushState({}, document.title, window.location.pathname); this.serviceWorker = new _ServiceWorker(); return Promise.resolve(this._currentRoom); } diff --git a/front/src/Connexion/Room.ts b/front/src/Connexion/Room.ts index 860223c6..535d2f8d 100644 --- a/front/src/Connexion/Room.ts +++ b/front/src/Connexion/Room.ts @@ -1,5 +1,5 @@ import Axios from "axios"; -import { CONTACT_URL, PUSHER_URL } from "../Enum/EnvironmentVariable"; +import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import type { CharacterTexture } from "./LocalUser"; import { localUserStore } from "./LocalUserStore"; @@ -14,8 +14,8 @@ export interface RoomRedirect { export class Room { public readonly id: string; public readonly isPublic: boolean; - private _authenticationMandatory: boolean = false; - private _iframeAuthentication?: string; + private _authenticationMandatory: boolean = DISABLE_ANONYMOUS as boolean; + private _iframeAuthentication?: string = OPID_LOGIN_SCREEN_PROVIDER; private _mapUrl: string | undefined; private _textures: CharacterTexture[] | undefined; private instance: string | undefined; @@ -106,8 +106,8 @@ export class Room { this._mapUrl = data.mapUrl; this._textures = data.textures; this._group = data.group; - this._authenticationMandatory = data.authenticationMandatory || false; - this._iframeAuthentication = data.iframeAuthentication; + this._authenticationMandatory = data.authenticationMandatory || (DISABLE_ANONYMOUS as boolean); + this._iframeAuthentication = data.iframeAuthentication || OPID_LOGIN_SCREEN_PROVIDER; this._contactPage = data.contactPage || CONTACT_URL; return new MapDetail(data.mapUrl, data.textures); } diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index cf76a87d..644b7a77 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -23,6 +23,8 @@ export const CONTACT_URL = process.env.CONTACT_URL || undefined; export const PROFILE_URL = process.env.PROFILE_URL || undefined; export const POSTHOG_API_KEY: string = (process.env.POSTHOG_API_KEY as string) || ""; export const POSTHOG_URL = process.env.POSTHOG_URL || undefined; +export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS || false; +export const OPID_LOGIN_SCREEN_PROVIDER = process.env.OPID_LOGIN_SCREEN_PROVIDER; export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600; diff --git a/front/webpack.config.ts b/front/webpack.config.ts index 003db1fc..9d18d51f 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -7,7 +7,6 @@ import MiniCssExtractPlugin from "mini-css-extract-plugin"; import sveltePreprocess from "svelte-preprocess"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; -import { POSTHOG_API_KEY, PROFILE_URL } from "./src/Enum/EnvironmentVariable"; const mode = process.env.NODE_ENV ?? "development"; const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS; @@ -208,6 +207,8 @@ module.exports = { POSTHOG_API_KEY: null, POSTHOG_URL: null, NODE_ENV: mode, + DISABLE_ANONYMOUS: false, + OPID_LOGIN_SCREEN_PROVIDER: null, }), ], } as Configuration & WebpackDevServer.Configuration; diff --git a/pusher/src/App.ts b/pusher/src/App.ts index 81aed045..327d493c 100644 --- a/pusher/src/App.ts +++ b/pusher/src/App.ts @@ -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); } } diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 972cc102..0cef24bb 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -5,6 +5,7 @@ import { adminApi } 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"; export interface TokenInterface { userUuid: string; @@ -61,10 +62,11 @@ export class AuthenticateController extends BaseController { 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); + console.log("resCheckTokenAuth", resCheckTokenAuth); res.writeStatus("200"); this.addCorsHeaders(res); return res.end(JSON.stringify({ authToken: token })); @@ -99,10 +101,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 { @@ -175,16 +177,21 @@ export class AuthenticateController extends BaseController { console.warn("Login request was aborted"); }); - const userUuid = v4(); - const authToken = jwtTokenManager.createAuthToken(userUuid); - res.writeStatus("200 OK"); - this.addCorsHeaders(res); - res.end( - JSON.stringify({ - authToken, - userUuid, - }) - ); + if (DISABLE_ANONYMOUS) { + res.writeStatus("403 FORBIDDEN"); + res.end(); + } else { + const userUuid = v4(); + const authToken = jwtTokenManager.createAuthToken(userUuid); + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end( + JSON.stringify({ + authToken, + userUuid, + }) + ); + } }); } @@ -196,20 +203,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(); diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index 0466100c..f73e44fd 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -26,7 +26,7 @@ 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 } from "../Enum/EnvironmentVariable"; +import { ADMIN_API_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"; @@ -175,6 +175,11 @@ export class IoSocketController { const tokenData = token && typeof token === "string" ? jwtTokenManager.verifyJWTToken(token) : null; + + if (DISABLE_ANONYMOUS && !tokenData) { + throw new Error("Expecting token"); + } + const userIdentifier = tokenData ? tokenData.identifier : ""; let memberTags: string[] = []; diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index f775b50c..18748d9e 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -2,9 +2,9 @@ import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; import { BaseController } from "./BaseController"; import { parse } from "query-string"; import { adminApi } from "../Services/AdminApi"; -import { ADMIN_API_URL } from "../Enum/EnvironmentVariable"; +import { ADMIN_API_URL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable"; import { GameRoomPolicyTypes } from "../Model/PusherRoom"; -import { MapDetailsData } from "../Services/AdminApi/MapDetailsData"; +import { isMapDetailsData, MapDetailsData } from "../Services/AdminApi/MapDetailsData"; import { socketManager } from "../Services/SocketManager"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { v4 } from "uuid"; @@ -64,6 +64,7 @@ export class MapController extends BaseController { tags: [], textures: [], contactPage: undefined, + authenticationMandatory: DISABLE_ANONYMOUS, } as MapDetailsData) ); @@ -87,6 +88,10 @@ export class MapController extends BaseController { } const mapDetails = await adminApi.fetchMapDetails(query.playUri as string, userId); + if (isMapDetailsData(mapDetails) && DISABLE_ANONYMOUS) { + mapDetails.authenticationMandatory = true; + } + res.writeStatus("200 OK"); this.addCorsHeaders(res); res.end(JSON.stringify(mapDetails)); diff --git a/pusher/src/Controller/OpenIdProfileController.ts b/pusher/src/Controller/OpenIdProfileController.ts new file mode 100644 index 00000000..372b603b --- /dev/null +++ b/pusher/src/Controller/OpenIdProfileController.ts @@ -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 ( + "" + + ` +
+ +
+ +
+
+ +
+
+ Profile validated by domain: ${domain} +
+
+ Your email: ${email} +
+
+ + ` + ); + } +} diff --git a/pusher/src/Enum/EnvironmentVariable.ts b/pusher/src/Enum/EnvironmentVariable.ts index 22c4db4f..52382266 100644 --- a/pusher/src/Enum/EnvironmentVariable.ts +++ b/pusher/src/Enum/EnvironmentVariable.ts @@ -16,6 +16,8 @@ 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_REDIREC_URL = process.env.OPID_CLIENT_REDIREC_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 { SECRET_KEY, diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index d002ff8b..6e1848eb 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -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,15 @@ 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 ADMIN_URL + `/profile?token=${accessToken}`; + return `${OPID_PROFILE_SCREEN_PROVIDER}?accessToken=${accessToken}`; } async logoutOauth(token: string) { diff --git a/pusher/src/Services/AdminApi/MapDetailsData.ts b/pusher/src/Services/AdminApi/MapDetailsData.ts index 278b81bb..7a1f57ff 100644 --- a/pusher/src/Services/AdminApi/MapDetailsData.ts +++ b/pusher/src/Services/AdminApi/MapDetailsData.ts @@ -16,6 +16,7 @@ export const isMapDetailsData = new tg.IsInterface() tags: tg.isArray(tg.isString), textures: tg.isArray(isCharacterTexture), contactPage: tg.isUnion(tg.isString, tg.isUndefined), + authenticationMandatory: tg.isUnion(tg.isBoolean, tg.isUndefined), }) .get(); diff --git a/pusher/src/Services/JWTTokenManager.ts b/pusher/src/Services/JWTTokenManager.ts index 24393084..2f482dbf 100644 --- a/pusher/src/Services/JWTTokenManager.ts +++ b/pusher/src/Services/JWTTokenManager.ts @@ -6,13 +6,13 @@ import { adminApi, AdminBannedData } from "../Services/AdminApi"; export interface AuthTokenData { identifier: string; //will be a email if logged in or an uuid if anonymous - hydraAccessToken?: string; + accessToken?: string; } export const tokenInvalidException = "tokenInvalid"; class JWTTokenManager { - public createAuthToken(identifier: string, hydraAccessToken?: string) { - return Jwt.sign({ identifier, hydraAccessToken }, SECRET_KEY, { expiresIn: "30d" }); + public createAuthToken(identifier: string, accessToken?: string) { + return Jwt.sign({ identifier, accessToken }, SECRET_KEY, { expiresIn: "30d" }); } public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {