Active authentication Oauth (#1377)

* Active authentication Oauth

 - Google authentication
 - GitHub authentication
 - Linkedin authentication

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Finish connexion et get user info connexion

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Fix lint error

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Change the expires token for 30 days

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update connexion stratgey

 - Set last room when it will be created and not when connexion is openned
 - Add '/login' end point permit to logout and open iframe to log user
 - Add logout feature permit to logout in front

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Implement logout and revoke token with hydra

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Fix pull develop conflict

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Profile url (#1399)

* Create function that permit to get profile URL

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Continue profil user

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Add menu and logout button

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update last room use

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Profile callback permit to get url profile setting from admin

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Finish profile show

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Delete profileUrl will be not use today

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Correct lint

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update size of iframe

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Delete console log

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>

* Update feedback ARP

Signed-off-by: Gregoire Parant <g.parant@thecodingmachine.com>
This commit is contained in:
grégoire parant 2021-09-05 18:17:49 +02:00 committed by GitHub
parent a0d863569b
commit d2b8d7dc04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 306 additions and 64 deletions

View file

View file

@ -1,15 +1,17 @@
<script lang="typescript">
import {gameManager} from "../../Phaser/Game/GameManager";
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
import {menuIconVisiblilityStore, menuVisiblilityStore} from "../../Stores/MenuStore";
import {menuIconVisiblilityStore, menuVisiblilityStore, userIsConnected} from "../../Stores/MenuStore";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {PROFILE_URL} from "../../Enum/EnvironmentVariable";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {enableCameraSceneVisibilityStore} from "../../Stores/MediaStore";
//import {connectionManager} from "../../Connexion/ConnectionManager";
function disableMenuStores(){
@ -35,6 +37,16 @@
gameManager.leaveGame(SelectCharacterSceneName,new SelectCharacterScene());
}
function logOut(){
disableMenuStores();
loginSceneVisibleStore.set(true);
connectionManager.logout();
}
function getProfileUrl(){
return PROFILE_URL + `?token=${localUserStore.getAuthToken()}`;
}
function openEnableCameraScene(){
disableMenuStores();
enableCameraSceneVisibilityStore.showEnableCameraScene();
@ -45,17 +57,26 @@
/*function clickLogin() {
connectionManager.loadOpenIDScreen();
}*/
</script>
<div class="customize-main">
{#if $userIsConnected}
<section>
{#if PROFILE_URL != undefined}
<iframe title="profile" src="{getProfileUrl()}"></iframe>
{/if}
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={logOut}>Log out</button>
</section>
{:else}
<section>
<a type="button" class="nes-btn" href="/login">Sing in</a>
</section>
{/if}
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditNameScene}>Edit Name</button>
</section>
<section>
<button type="button" class="nes-btn is-rounded" on:click|preventDefault={openEditSkinScene}>Edit Skin</button>
</section>
<section>
<button type="button" class="nes-btn" on:click|preventDefault={openEditCompanionScene}>Edit Companion</button>
</section>
<section>
@ -74,6 +95,12 @@
align-items: center;
margin-bottom: 20px;
iframe{
width: 100%;
height: 50vh;
border: none;
}
button {
height: 50px;
width: 250px;

View file

@ -7,6 +7,8 @@ import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room";
import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore";
class ConnectionManager {
private localUser!: LocalUser;
@ -15,6 +17,7 @@ class ConnectionManager {
private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false;
private authToken: string | null = null;
private _currentRoom: Room | null = null;
private serviceWorker?: _ServiceWorker;
@ -30,28 +33,39 @@ class ConnectionManager {
}
/**
* @return Promise<void>
* TODO fix me to be move in game manager
*/
public loadOpenIDScreen(): Promise<void> {
public loadOpenIDScreen() {
const state = localUserStore.generateState();
const nonce = localUserStore.generateNonce();
localUserStore.setAuthToken(null);
//TODO refactor this and don't realise previous call
return Axios.get(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`)
.then(() => {
window.location.assign(`http://${PUSHER_URL}/login-screen?state=${state}&nonce=${nonce}`);
})
.catch((err) => {
console.error(err, "We don't have URL to regenerate authentication user");
//TODO show modal login
window.location.reload();
});
//TODO fix me to redirect this URL by pusher
if (!this._currentRoom || !this._currentRoom.iframeAuthentication) {
loginSceneVisibleIframeStore.set(false);
return null;
}
const redirectUrl = `${this._currentRoom.iframeAuthentication}?state=${state}&nonce=${nonce}`;
window.location.assign(redirectUrl);
return redirectUrl;
}
public logout() {
/**
* Logout
*/
public async logout() {
//user logout, set connected store for menu at false
userIsConnected.set(false);
//Logout user in pusher and hydra
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/logout-callback`, { params: { token } }).then(
(res) => res.data
);
localUserStore.setAuthToken(null);
window.location.reload();
//Go on login page can permit to clear token and start authentication process
window.location.assign("/login");
}
/**
@ -60,8 +74,13 @@ class ConnectionManager {
public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
let room: Room | null = null;
if (connexionType === GameConnexionTypes.jwt) {
this._currentRoom = null;
if (connexionType === GameConnexionTypes.login) {
//TODO clear all cash and redirect on login scene (iframe)
localUserStore.setAuthToken(null);
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.jwt) {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get("code");
const state = urlParams.get("state");
@ -71,14 +90,15 @@ class ConnectionManager {
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);
localUserStore.setCode(code);
try {
await this.checkAuthUserConnexion();
} catch (err) {
console.error(err);
this.loadOpenIDScreen();
}
this._currentRoom = await Room.createRoom(new URL(localUserStore.getLastRoomUrl()));
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (connexionType === GameConnexionTypes.register) {
//@deprecated
const organizationMemberToken = urlManager.getOrganizationToken();
@ -92,7 +112,7 @@ class ConnectionManager {
const roomUrl = data.roomUrl;
room = await Room.createRoom(
this._currentRoom = await Room.createRoom(
new URL(
window.location.protocol +
"//" +
@ -102,7 +122,7 @@ class ConnectionManager {
window.location.hash
)
);
urlManager.pushRoomIdToUrl(room);
urlManager.pushRoomIdToUrl(this._currentRoom);
} else if (
connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous ||
@ -112,12 +132,18 @@ class ConnectionManager {
//todo: add here some kind of warning if authToken has expired.
if (!this.authToken) {
await this.anonymousLogin();
} else {
try {
await this.checkAuthUserConnexion();
} catch (err) {
console.error(err);
}
}
this.localUser = localUserStore.getLocalUser() as LocalUser; //if authToken exist in localStorage then localUser cannot be null
let roomPath: string;
if (connexionType === GameConnexionTypes.empty) {
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
roomPath = localUserStore.getLastRoomUrl();
//get last room path from cache api
try {
const lastRoomUrl = await localUserStore.getLastRoomUrlCacheApi();
@ -138,13 +164,13 @@ class ConnectionManager {
}
//get detail map for anonymous login and set texture in local storage
room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) {
this._currentRoom = await Room.createRoom(new URL(roomPath));
if (this._currentRoom.textures != undefined && this._currentRoom.textures.length > 0) {
//check if texture was changed
if (this.localUser.textures.length === 0) {
this.localUser.textures = room.textures;
this.localUser.textures = this._currentRoom.textures;
} else {
room.textures.forEach((newTexture) => {
this._currentRoom.textures.forEach((newTexture) => {
const alreadyExistTexture = this.localUser.textures.find((c) => newTexture.id === c.id);
if (this.localUser.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return;
@ -155,12 +181,12 @@ class ConnectionManager {
localUserStore.saveUser(this.localUser);
}
}
if (room == undefined) {
if (this._currentRoom == undefined) {
return Promise.reject(new Error("Invalid URL"));
}
this.serviceWorker = new _ServiceWorker();
return Promise.resolve(room);
return Promise.resolve(this._currentRoom);
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
@ -215,9 +241,6 @@ class ConnectionManager {
});
connection.onConnect((connect: OnConnectInterface) => {
//save last room url connected
localUserStore.setLastRoomUrl(roomUrl);
resolve(connect);
});
}).catch((err) => {
@ -237,6 +260,34 @@ class ConnectionManager {
get getConnexionType() {
return this.connexionType;
}
async checkAuthUserConnexion() {
//set connected store for menu at false
userIsConnected.set(false);
const state = localUserStore.getState();
const code = localUserStore.getCode();
if (!state || !localUserStore.verifyState(state)) {
throw "Could not validate state!";
}
if (!code) {
throw "No Auth code provided";
}
const nonce = localUserStore.getNonce();
const token = localUserStore.getAuthToken();
const { authToken } = await Axios.get(`${PUSHER_URL}/login-callback`, { params: { code, nonce, token } }).then(
(res) => res.data
);
localUserStore.setAuthToken(authToken);
this.authToken = authToken;
//user connected, set connected store for menu at true
userIsConnected.set(true);
}
get currentRoom() {
return this._currentRoom;
}
}
export const connectionManager = new ConnectionManager();

View file

@ -1,5 +1,6 @@
import { areCharacterLayersValid, isUserNameValid, LocalUser } from "./LocalUser";
import { v4 as uuidv4 } from "uuid";
import { START_ROOM_URL } from "../Enum/EnvironmentVariable";
const playerNameKey = "playerName";
const selectedPlayerKey = "selectedPlayer";
@ -17,6 +18,7 @@ const authToken = "authToken";
const state = "state";
const nonce = "nonce";
const notification = "notificationPermission";
const code = "code";
const cameraSetup = "cameraSetup";
const cacheAPIIndex = "workavdenture-cache";
@ -126,7 +128,9 @@ class LocalUserStore {
});
}
getLastRoomUrl(): string {
return localStorage.getItem(lastRoomUrl) ?? "";
return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
);
}
getLastRoomUrlCacheApi(): Promise<string | undefined> {
return caches.open(cacheAPIIndex).then((cache) => {
@ -161,19 +165,24 @@ class LocalUserStore {
verifyState(value: string): boolean {
const oldValue = localStorage.getItem(state);
localStorage.removeItem(state);
return oldValue === value;
}
getState(): string | null {
return localStorage.getItem(state);
}
generateNonce(): string {
const newNonce = uuidv4();
localStorage.setItem(nonce, newNonce);
return newNonce;
}
getNonce(): string | null {
const oldValue = localStorage.getItem(nonce);
localStorage.removeItem(nonce);
return oldValue;
return localStorage.getItem(nonce);
}
setCode(value: string): void {
localStorage.setItem(code, value);
}
getCode(): string | null {
return localStorage.getItem(code);
}
setCameraSetup(cameraId: string) {

View file

@ -14,6 +14,8 @@ export interface RoomRedirect {
export class Room {
public readonly id: string;
public readonly isPublic: boolean;
private _authenticationMandatory: boolean = false;
private _iframeAuthentication?: string;
private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined;
private instance: string | undefined;
@ -101,6 +103,8 @@ export class Room {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
this._authenticationMandatory = data.authenticationMandatory || false;
this._iframeAuthentication = data.iframeAuthentication;
return new MapDetail(data.mapUrl, data.textures);
}
@ -186,4 +190,12 @@ export class Room {
}
return this._mapUrl;
}
get authenticationMandatory(): boolean {
return this._authenticationMandatory;
}
get iframeAuthentication(): string | undefined {
return this._iframeAuthentication;
}
}

View file

@ -78,6 +78,11 @@ export class RoomConnection implements RoomConnection {
*
* @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 name
* @param characterLayers
* @param position
* @param viewport
* @param companion
*/
public constructor(
token: string | null,
@ -218,7 +223,7 @@ export class RoomConnection implements RoomConnection {
worldFullMessageStream.onMessage();
this.closed = true;
} else if (message.hasTokenexpiredmessage()) {
connectionManager.loadOpenIDScreen();
connectionManager.logout();
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()) {
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());

View file

@ -19,6 +19,7 @@ export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == "true";
export const NODE_ENV = process.env.NODE_ENV || "development";
export const CONTACT_URL = process.env.CONTACT_URL || undefined;
export const PROFILE_URL = process.env.PROFILE_URL || undefined;
export const isMobile = (): boolean => window.innerWidth <= 800 || window.innerHeight <= 600;

View file

@ -35,7 +35,9 @@ export class GameManager {
this.startRoom = await connectionManager.initGameConnexion();
this.loadMap(this.startRoom);
if (!this.playerName) {
//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())) {
return LoginSceneName;
} else if (!this.characterLayers || !this.characterLayers.length) {
return SelectCharacterSceneName;
@ -143,6 +145,10 @@ export class GameManager {
if (this.currentGameSceneName === null) throw "No current scene id set!";
return this.scenePlugin.get(this.currentGameSceneName) as GameScene;
}
public get currentStartedRoom() {
return this.startRoom;
}
}
export const gameManager = new GameManager();

View file

@ -1,7 +1,9 @@
import { gameManager } from "../Game/GameManager";
import { SelectCharacterSceneName } from "./SelectCharacterScene";
import { ResizableScene } from "./ResizableScene";
import { loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import { loginSceneVisibleIframeStore, loginSceneVisibleStore } from "../../Stores/LoginSceneStore";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { gameManager } from "../Game/GameManager";
export const LoginSceneName = "LoginScene";
@ -18,6 +20,16 @@ export class LoginScene extends ResizableScene {
preload() {}
create() {
loginSceneVisibleIframeStore.set(false);
//If authentication is mandatory, push authentication iframe
if (
localUserStore.getAuthToken() == undefined &&
gameManager.currentStartedRoom &&
gameManager.currentStartedRoom?.authenticationMandatory
) {
connectionManager.loadOpenIDScreen();
loginSceneVisibleIframeStore.set(true);
}
loginSceneVisibleStore.set(true);
}

View file

@ -1,3 +1,4 @@
import { writable } from "svelte/store";
export const loginSceneVisibleStore = writable(false);
export const loginSceneVisibleIframeStore = writable(false);

View file

@ -6,6 +6,8 @@ import { CONTACT_URL } from "../Enum/EnvironmentVariable";
export const menuIconVisiblilityStore = writable(false);
export const menuVisiblilityStore = writable(false);
export const menuInputFocusStore = writable(false);
export const loginUrlStore = writable(false);
export const userIsConnected = writable(false);
let warningContainerTimeout: Timeout | null = null;
function createWarningContainerStore() {

View file

@ -2,6 +2,7 @@ import { writable } from "svelte/store";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator";
import { localUserStore } from "../Connexion/LocalUserStore";
let idCount = 0;

View file

@ -1,4 +1,5 @@
import type { Room } from "../Connexion/Room";
import { localUserStore } from "../Connexion/LocalUserStore";
export enum GameConnexionTypes {
anonymous = 1,
@ -7,13 +8,16 @@ export enum GameConnexionTypes {
empty,
unknown,
jwt,
login,
}
//this class is responsible with analysing and editing the game's url
class UrlManager {
public getGameConnexionType(): GameConnexionTypes {
const url = window.location.pathname.toString();
if (url === "/jwt") {
if (url === "/login") {
return GameConnexionTypes.login;
} else if (url === "/jwt") {
return GameConnexionTypes.jwt;
} else if (url.includes("_/")) {
return GameConnexionTypes.anonymous;
@ -35,6 +39,8 @@ class UrlManager {
public pushRoomIdToUrl(room: Room): void {
if (window.location.pathname === room.id) return;
//Set last room visited! (connected or nor, must to be saved in localstorage and cache API)
localUserStore.setLastRoomUrl(room.key);
const hash = window.location.hash;
const search = room.search.toString();
history.pushState({}, "WorkAdventure", room.id + (search ? "?" + search : "") + hash);

View file

@ -7,6 +7,7 @@ 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 { PROFILE_URL } from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? "development";
const buildNpmTypingsForApi = !!process.env.BUILD_TYPINGS;
@ -191,6 +192,7 @@ module.exports = {
UPLOADER_URL: null,
ADMIN_URL: undefined,
CONTACT_URL: null,
PROFILE_URL: null,
DEBUG_MODE: null,
STUN_SERVER: null,
TURN_SERVER: null,

View file

@ -2,7 +2,7 @@ import { v4 } from "uuid";
import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js";
import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi";
import { jwtTokenManager } from "../Services/JWTTokenManager";
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
@ -17,6 +17,7 @@ export class AuthenticateController extends BaseController {
this.openIDCallback();
this.register();
this.anonymLogin();
this.profileCallback();
}
openIDLogin() {
@ -48,14 +49,31 @@ export class AuthenticateController extends BaseController {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { code, nonce } = parse(req.getQuery());
const { code, nonce, 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) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken: token }));
} catch (err) {
console.info("User was not connected", err);
}
}
//user have not token created, check data on hydra and create token
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);
const authToken = jwtTokenManager.createAuthToken(email, userInfo.access_token);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken }));
@ -63,6 +81,30 @@ export class AuthenticateController extends BaseController {
return this.errorToResponse(e, res);
}
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/logout-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { token } = parse(req.getQuery());
try {
const authTokenData: AuthTokenData = jwtTokenManager.verifyJWTToken(token as string, false);
if (authTokenData.hydraAccessToken == undefined) {
throw Error("Token cannot to be logout on Hydra");
}
await openIDClient.logoutUser(authTokenData.hydraAccessToken);
} catch (error) {
console.error("openIDCallback => logout-callback", error);
} finally {
res.writeStatus("200");
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
}
});
}
//Try to login with an admin token
@ -136,4 +178,39 @@ export class AuthenticateController extends BaseController {
);
});
}
profileCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/profile-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { userIdentify, 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) {
throw Error("Token cannot to be check on Hydra");
}
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
//get login profile
res.writeStatus("302");
res.writeHeader("Location", adminApi.getProfileUrl(authTokenData.hydraAccessToken));
this.addCorsHeaders(res);
// eslint-disable-next-line no-unsafe-finally
return res.end();
} catch (error) {
return this.errorToResponse(error, res);
}
}
} catch (error) {
this.errorToResponse(error, res);
}
});
}
}

View file

@ -2,6 +2,7 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY";
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false;
const API_URL = process.env.API_URL || "";
const ADMIN_API_URL = process.env.ADMIN_API_URL || "";
const ADMIN_URL = process.env.ADMIN_URL || "";
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken";
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;
@ -19,6 +20,7 @@ export {
SECRET_KEY,
API_URL,
ADMIN_API_URL,
ADMIN_URL,
ADMIN_API_TOKEN,
ALLOW_ARTILLERY,
CPU_OVERHEAT_THRESHOLD,

View file

@ -1,4 +1,4 @@
import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { ADMIN_API_TOKEN, ADMIN_API_URL, ADMIN_URL } from "../Enum/EnvironmentVariable";
import Axios from "axios";
import { GameRoomPolicyTypes } from "_Model/PusherRoom";
import { CharacterTexture } from "./AdminApi/CharacterTexture";
@ -141,6 +141,15 @@ class AdminApi {
return data.data;
});
}
/*TODO add constant to use profile companny*/
getProfileUrl(accessToken: string): string {
if (!ADMIN_URL) {
throw new Error("No admin backoffice set!");
}
return ADMIN_URL + `/profile?token=${accessToken}`;
}
}
export const adminApi = new AdminApi();

View file

@ -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;
}
export const tokenInvalidException = "tokenInvalid";
class JWTTokenManager {
public createAuthToken(identifier: string) {
//TODO fix me 200d when ory authentication will be available
return Jwt.sign({ identifier }, SECRET_KEY, { expiresIn: "200d" });
public createAuthToken(identifier: string, hydraAccessToken?: string) {
return Jwt.sign({ identifier, hydraAccessToken }, SECRET_KEY, { expiresIn: "30d" });
}
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {

View file

@ -1,4 +1,4 @@
import { Issuer, Client } from "openid-client";
import { Issuer, Client, IntrospectionResponse } from "openid-client";
import { OPID_CLIENT_ID, OPID_CLIENT_SECRET, OPID_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
const opidRedirectUri = FRONT_URL + "/jwt";
@ -31,13 +31,32 @@ class OpenIDClient {
});
}
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string }> {
public getUserInfo(code: string, nonce: string): Promise<{ email: string; sub: string; access_token: string }> {
return this.initClient().then((client) => {
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
return client.userinfo(tokenSet);
return client.userinfo(tokenSet).then((res) => {
return {
...res,
email: res.email as string,
sub: res.sub,
access_token: tokenSet.access_token as string,
};
});
});
});
}
public logoutUser(token: string): Promise<void> {
return this.initClient().then((client) => {
return client.revoke(token);
});
}
public checkTokenAuth(token: string): Promise<IntrospectionResponse> {
return this.initClient().then((client) => {
return client.userinfo(token);
});
}
}
export const openIDClient = new OpenIDClient();