diff --git a/.env.template b/.env.template index 34537b6b..e8b9e59c 100644 --- a/.env.template +++ b/.env.template @@ -19,6 +19,6 @@ ACME_EMAIL= MAX_PER_GROUP=4 MAX_USERNAME_LENGTH=8 -OPID_CLIENT_ID= -OPID_CLIENT_SECRET= -OPID_CLIENT_ISSUER= +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +OIDC_CLIENT_ISSUER= diff --git a/contrib/docker/docker-compose.single-domain.prod.yaml b/contrib/docker/docker-compose.single-domain.prod.yaml index c820d92e..490309b8 100644 --- a/contrib/docker/docker-compose.single-domain.prod.yaml +++ b/contrib/docker/docker-compose.single-domain.prod.yaml @@ -51,9 +51,9 @@ services: JITSI_URL: ${JITSI_URL} JITSI_ISS: ${JITSI_ISS} FRONT_URL : ${FRONT_URL} - OPID_CLIENT_ID: ${OPID_CLIENT_ID} - OPID_CLIENT_SECRET: ${OPID_CLIENT_SECRET} - OPID_CLIENT_ISSUER: ${OPID_CLIENT_ISSUER} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET} + OIDC_CLIENT_ISSUER: ${OIDC_CLIENT_ISSUER} labels: - "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher" - "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)" diff --git a/docker-compose.single-domain.yaml b/docker-compose.single-domain.yaml index 4e85d702..d2cbf5b0 100644 --- a/docker-compose.single-domain.yaml +++ b/docker-compose.single-domain.yaml @@ -67,9 +67,9 @@ services: JITSI_URL: $JITSI_URL 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 + OIDC_CLIENT_ID: $OIDC_CLIENT_ID + OIDC_CLIENT_SECRET: $OIDC_CLIENT_SECRET + OIDC_CLIENT_ISSUER: $OIDC_CLIENT_ISSUER volumes: - ./pusher:/usr/src/app labels: diff --git a/docker-compose.yaml b/docker-compose.yaml index 4b3904dd..a33706c0 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -67,9 +67,9 @@ services: JITSI_URL: $JITSI_URL 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 + OIDC_CLIENT_ID: $OIDC_CLIENT_ID + OIDC_CLIENT_SECRET: $OIDC_CLIENT_SECRET + OIDC_CLIENT_ISSUER: $OIDC_CLIENT_ISSUER volumes: - ./pusher:/usr/src/app labels: diff --git a/front/src/Connexion/ConnectionManager.ts b/front/src/Connexion/ConnectionManager.ts index 8e792cdd..9269fcf3 100644 --- a/front/src/Connexion/ConnectionManager.ts +++ b/front/src/Connexion/ConnectionManager.ts @@ -9,7 +9,8 @@ import { Room } from "./Room"; import { _ServiceWorker } from "../Network/ServiceWorker"; import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore"; import { userIsConnected } from "../Stores/MenuStore"; -import {analyticsClient} from "../Administration/AnalyticsClient"; +import { analyticsClient } from "../Administration/AnalyticsClient"; +import { gameManager } from "../Phaser/Game/GameManager"; class ConnectionManager { private localUser!: LocalUser; @@ -39,26 +40,16 @@ class ConnectionManager { public loadOpenIDScreen() { const state = localUserStore.generateState(); const nonce = localUserStore.generateNonce(); - - let loginUrl = `${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}` - - if (loginUrl.startsWith("/")) { - loginUrl = window.location.protocol + - "//" + - window.location.host + - loginUrl; - } else { - loginUrl = `http://` + loginUrl; - } - localUserStore.setAuthToken(null); //TODO fix me to redirect this URL by pusher - if (!this._currentRoom || !this._currentRoom.iframeAuthentication) { + if (!this._currentRoom) { + console.error("cannot get currentRoom!"); loginSceneVisibleIframeStore.set(false); return null; } - const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}&playUri=${this._currentRoom.key}`; + + const redirectUrl = `${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}&playUri=${this._currentRoom.key}`; window.location.assign(redirectUrl); return redirectUrl; } @@ -208,13 +199,17 @@ class ConnectionManager { } public async anonymousLogin(isBenchmark: boolean = false): Promise { - const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data); - this.localUser = new LocalUser(data.userUuid, []); - this.authToken = data.authToken; - if (!isBenchmark) { - // In benchmark, we don't have a local storage. - localUserStore.saveUser(this.localUser); - localUserStore.setAuthToken(this.authToken); + try { + const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data); + this.localUser = new LocalUser(data.userUuid, []); + this.authToken = data.authToken; + if (!isBenchmark) { + // In benchmark, we don't have a local storage. + localUserStore.saveUser(this.localUser); + localUserStore.setAuthToken(this.authToken); + } + } catch (error) { + this.loadOpenIDScreen(); } } @@ -293,12 +288,14 @@ class ConnectionManager { } const nonce = localUserStore.getNonce(); const token = localUserStore.getAuthToken(); - const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then( + const { authToken, username } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then( (res) => res.data ); localUserStore.setAuthToken(authToken); this.authToken = authToken; + gameManager.setPlayerName(username); + //user connected, set connected store for menu at true userIsConnected.set(true); } diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index e7cfa122..cb8ed376 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -57,7 +57,7 @@ export class GameManager { //If player name was not set show login scene with player name //If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous) - if (!this.playerName || (this.startRoom.authenticationMandatory && !localUserStore.getAuthToken())) { + if (!this.playerName || !localUserStore.getAuthToken()) { return LoginSceneName; } else if (!this.characterLayers || !this.characterLayers.length) { return SelectCharacterSceneName; diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 7421934d..921912aa 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -23,9 +23,7 @@ export class LoginScene extends ResizableScene { loginSceneVisibleIframeStore.set(false); //If authentication is mandatory, push authentication iframe if ( - localUserStore.getAuthToken() == undefined && - gameManager.currentStartedRoom && - gameManager.currentStartedRoom?.authenticationMandatory + localUserStore.getAuthToken() == undefined ) { connectionManager.loadOpenIDScreen(); loginSceneVisibleIframeStore.set(true); diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 7db86b14..b1152af7 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -5,7 +5,7 @@ import { adminApi } from "../Services/AdminApi"; import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager"; import { parse } from "query-string"; import { openIDClient } from "../Services/OpenIDClient"; -import { FRONT_URL, DEBUG_IGNORE_SSL } from "../Enum/EnvironmentVariable" +import { FRONT_URL, DEBUG_IGNORE_SSL, DISABLE_ANONYMOUS } from "../Enum/EnvironmentVariable" import Axios from "axios"; import { AxiosRequestConfig } from "axios"; import https from "https"; @@ -69,7 +69,7 @@ export class AuthenticateController extends BaseController { await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken); res.writeStatus("200"); this.addCorsHeaders(res); - return res.end(JSON.stringify({ authToken: token })); + return res.end(JSON.stringify({ authToken: token, username: authTokenData.username })); } catch (err) { console.info("User was not connected", err); } @@ -81,10 +81,10 @@ export class AuthenticateController extends BaseController { if (!sub) { throw new Error("No sub in the response"); } - const authToken = jwtTokenManager.createAuthToken(sub, userInfo.access_token); + const authToken = jwtTokenManager.createAuthToken(sub, userInfo.access_token, userInfo.username); res.writeStatus("200"); this.addCorsHeaders(res); - return res.end(JSON.stringify({ authToken })); + return res.end(JSON.stringify({ authToken: authToken, username: userInfo.username })); } catch (e) { return this.errorToResponse(e, res); } @@ -173,32 +173,38 @@ export class AuthenticateController extends BaseController { res.onAborted(() => { console.warn("Login request was aborted"); }); - let userUuid = v4(); - const axiosConfig: AxiosRequestConfig = {}; + if (DISABLE_ANONYMOUS) { + res.writeStatus("403 FORBIDDEN"); + res.end(); + } else { + let userUuid = v4(); - if (DEBUG_IGNORE_SSL) { - const agent = new https.Agent({ - rejectUnauthorized: false, - }); - axiosConfig.httpsAgent = agent; + const axiosConfig: AxiosRequestConfig = {}; + + if (DEBUG_IGNORE_SSL) { + const agent = new https.Agent({ + rejectUnauthorized: false, + }); + axiosConfig.httpsAgent = agent; + } + + const response = await Axios.get(FRONT_URL, axiosConfig); + + if (response.headers[ 'bstlyuserid' ]) { + userUuid = response.headers[ 'bstlyuserid' ]; + } + + const authToken = jwtTokenManager.createAuthToken(userUuid); + res.writeStatus("200 OK"); + this.addCorsHeaders(res); + res.end( + JSON.stringify({ + authToken, + userUuid, + }) + ); } - - const response = await Axios.get(FRONT_URL, axiosConfig); - - if (response.headers[ 'bstlyuserid' ]) { - userUuid = response.headers[ 'bstlyuserid' ]; - } - - const authToken = jwtTokenManager.createAuthToken(userUuid); - res.writeStatus("200 OK"); - this.addCorsHeaders(res); - res.end( - JSON.stringify({ - authToken, - userUuid, - }) - ); }); } diff --git a/pusher/src/Enum/EnvironmentVariable.ts b/pusher/src/Enum/EnvironmentVariable.ts index 82b2dee3..348f67e0 100644 --- a/pusher/src/Enum/EnvironmentVariable.ts +++ b/pusher/src/Enum/EnvironmentVariable.ts @@ -12,9 +12,10 @@ 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 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 DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS ? process.env.DISABLE_ANONYMOUS == "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 const DEBUG_IGNORE_SSL = process.env.DEBUG_IGNORE_SSL ? process.env.DEBUG_IGNORE_SSL == "true" : false; export const DEBUG_PUSHER_FORCE_ROOM_UPDATE = process.env.DEBUG_PUSHER_FORCE_ROOM_UPDATE ? process.env.DEBUG_PUSHER_FORCE_ROOM_UPDATE == "true" : false; diff --git a/pusher/src/Services/JWTTokenManager.ts b/pusher/src/Services/JWTTokenManager.ts index 24393084..1aab08e6 100644 --- a/pusher/src/Services/JWTTokenManager.ts +++ b/pusher/src/Services/JWTTokenManager.ts @@ -5,14 +5,15 @@ import { TokenInterface } from "../Controller/AuthenticateController"; import { adminApi, AdminBannedData } from "../Services/AdminApi"; export interface AuthTokenData { - identifier: string; //will be a email if logged in or an uuid if anonymous + identifier: string; //will be a sub (id) if logged in or an uuid if anonymous hydraAccessToken?: string; + username?: 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, hydraAccessToken?: string, username?: string) { + return Jwt.sign({ identifier, hydraAccessToken, username }, SECRET_KEY, { expiresIn: "30d" }); } public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData { diff --git a/pusher/src/Services/OpenIDClient.ts b/pusher/src/Services/OpenIDClient.ts index da636c02..ee5ce869 100644 --- a/pusher/src/Services/OpenIDClient.ts +++ b/pusher/src/Services/OpenIDClient.ts @@ -1,5 +1,5 @@ import { Issuer, Client, IntrospectionResponse } from "openid-client"; -import { OPID_CLIENT_ID, OPID_CLIENT_SECRET, OPID_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable"; +import { OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable"; const opidRedirectUri = FRONT_URL + "/jwt"; @@ -8,10 +8,10 @@ class OpenIDClient { private initClient(): Promise { if (!this.issuerPromise) { - this.issuerPromise = Issuer.discover(OPID_CLIENT_ISSUER).then((issuer) => { + this.issuerPromise = Issuer.discover(OIDC_CLIENT_ISSUER).then((issuer) => { return new issuer.Client({ - client_id: OPID_CLIENT_ID, - client_secret: OPID_CLIENT_SECRET, + client_id: OIDC_CLIENT_ID, + client_secret: OIDC_CLIENT_SECRET, redirect_uris: [opidRedirectUri], response_types: ["code"], }); @@ -32,7 +32,7 @@ class OpenIDClient { }); } - public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string; access_token: string }> { + 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.userinfo(tokenSet).then((res) => { @@ -41,6 +41,7 @@ class OpenIDClient { email: res.email as string, sub: res.sub, access_token: tokenSet.access_token as string, + username: (res.preferred_username || res.username || res.nickname || res.name || res.email) as string, }; }); });