Merge pull request #1559 from thecodingmachine/openIdDisabledAnonymousSelfHost
Open id disabled anonymous self hosted
This commit is contained in:
commit
a4b8a8422f
@ -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=
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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[] = [];
|
||||
|
@ -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));
|
||||
|
80
pusher/src/Controller/OpenIdProfileController.ts
Normal file
80
pusher/src/Controller/OpenIdProfileController.ts
Normal file
@ -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>" +
|
||||
`
|
||||
<header>
|
||||
<style>
|
||||
*{
|
||||
font-family: PixelFont-7, monospace;
|
||||
}
|
||||
body{
|
||||
text-align: center;
|
||||
color: white;
|
||||
}
|
||||
section{
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
</header>
|
||||
<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>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user