native OIDC support
This commit is contained in:
parent
3a9ffd7557
commit
0bf49fa26a
@ -19,6 +19,6 @@ ACME_EMAIL=
|
|||||||
MAX_PER_GROUP=4
|
MAX_PER_GROUP=4
|
||||||
MAX_USERNAME_LENGTH=8
|
MAX_USERNAME_LENGTH=8
|
||||||
|
|
||||||
OPID_CLIENT_ID=
|
OIDC_CLIENT_ID=
|
||||||
OPID_CLIENT_SECRET=
|
OIDC_CLIENT_SECRET=
|
||||||
OPID_CLIENT_ISSUER=
|
OIDC_CLIENT_ISSUER=
|
||||||
|
@ -51,9 +51,9 @@ services:
|
|||||||
JITSI_URL: ${JITSI_URL}
|
JITSI_URL: ${JITSI_URL}
|
||||||
JITSI_ISS: ${JITSI_ISS}
|
JITSI_ISS: ${JITSI_ISS}
|
||||||
FRONT_URL : ${FRONT_URL}
|
FRONT_URL : ${FRONT_URL}
|
||||||
OPID_CLIENT_ID: ${OPID_CLIENT_ID}
|
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
|
||||||
OPID_CLIENT_SECRET: ${OPID_CLIENT_SECRET}
|
OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET}
|
||||||
OPID_CLIENT_ISSUER: ${OPID_CLIENT_ISSUER}
|
OIDC_CLIENT_ISSUER: ${OIDC_CLIENT_ISSUER}
|
||||||
labels:
|
labels:
|
||||||
- "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher"
|
- "traefik.http.middlewares.strip-pusher-prefix.stripprefix.prefixes=/pusher"
|
||||||
- "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)"
|
- "traefik.http.routers.pusher.rule=PathPrefix(`/pusher`)"
|
||||||
|
@ -67,9 +67,9 @@ services:
|
|||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
FRONT_URL: http://localhost
|
FRONT_URL: http://localhost
|
||||||
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
OIDC_CLIENT_ID: $OIDC_CLIENT_ID
|
||||||
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
OIDC_CLIENT_SECRET: $OIDC_CLIENT_SECRET
|
||||||
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
OIDC_CLIENT_ISSUER: $OIDC_CLIENT_ISSUER
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
@ -67,9 +67,9 @@ services:
|
|||||||
JITSI_URL: $JITSI_URL
|
JITSI_URL: $JITSI_URL
|
||||||
JITSI_ISS: $JITSI_ISS
|
JITSI_ISS: $JITSI_ISS
|
||||||
FRONT_URL: http://play.workadventure.localhost
|
FRONT_URL: http://play.workadventure.localhost
|
||||||
OPID_CLIENT_ID: $OPID_CLIENT_ID
|
OIDC_CLIENT_ID: $OIDC_CLIENT_ID
|
||||||
OPID_CLIENT_SECRET: $OPID_CLIENT_SECRET
|
OIDC_CLIENT_SECRET: $OIDC_CLIENT_SECRET
|
||||||
OPID_CLIENT_ISSUER: $OPID_CLIENT_ISSUER
|
OIDC_CLIENT_ISSUER: $OIDC_CLIENT_ISSUER
|
||||||
volumes:
|
volumes:
|
||||||
- ./pusher:/usr/src/app
|
- ./pusher:/usr/src/app
|
||||||
labels:
|
labels:
|
||||||
|
@ -9,7 +9,8 @@ import { Room } from "./Room";
|
|||||||
import { _ServiceWorker } from "../Network/ServiceWorker";
|
import { _ServiceWorker } from "../Network/ServiceWorker";
|
||||||
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
|
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
|
||||||
import { userIsConnected } from "../Stores/MenuStore";
|
import { userIsConnected } from "../Stores/MenuStore";
|
||||||
import {analyticsClient} from "../Administration/AnalyticsClient";
|
import { analyticsClient } from "../Administration/AnalyticsClient";
|
||||||
|
import { gameManager } from "../Phaser/Game/GameManager";
|
||||||
|
|
||||||
class ConnectionManager {
|
class ConnectionManager {
|
||||||
private localUser!: LocalUser;
|
private localUser!: LocalUser;
|
||||||
@ -39,26 +40,16 @@ class ConnectionManager {
|
|||||||
public loadOpenIDScreen() {
|
public loadOpenIDScreen() {
|
||||||
const state = localUserStore.generateState();
|
const state = localUserStore.generateState();
|
||||||
const nonce = localUserStore.generateNonce();
|
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);
|
localUserStore.setAuthToken(null);
|
||||||
|
|
||||||
//TODO fix me to redirect this URL by pusher
|
//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);
|
loginSceneVisibleIframeStore.set(false);
|
||||||
return null;
|
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);
|
window.location.assign(redirectUrl);
|
||||||
return redirectUrl;
|
return redirectUrl;
|
||||||
}
|
}
|
||||||
@ -208,13 +199,17 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
try {
|
||||||
this.localUser = new LocalUser(data.userUuid, []);
|
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
|
||||||
this.authToken = data.authToken;
|
this.localUser = new LocalUser(data.userUuid, []);
|
||||||
if (!isBenchmark) {
|
this.authToken = data.authToken;
|
||||||
// In benchmark, we don't have a local storage.
|
if (!isBenchmark) {
|
||||||
localUserStore.saveUser(this.localUser);
|
// In benchmark, we don't have a local storage.
|
||||||
localUserStore.setAuthToken(this.authToken);
|
localUserStore.saveUser(this.localUser);
|
||||||
|
localUserStore.setAuthToken(this.authToken);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.loadOpenIDScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,12 +288,14 @@ class ConnectionManager {
|
|||||||
}
|
}
|
||||||
const nonce = localUserStore.getNonce();
|
const nonce = localUserStore.getNonce();
|
||||||
const token = localUserStore.getAuthToken();
|
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
|
(res) => res.data
|
||||||
);
|
);
|
||||||
localUserStore.setAuthToken(authToken);
|
localUserStore.setAuthToken(authToken);
|
||||||
this.authToken = authToken;
|
this.authToken = authToken;
|
||||||
|
|
||||||
|
gameManager.setPlayerName(username);
|
||||||
|
|
||||||
//user connected, set connected store for menu at true
|
//user connected, set connected store for menu at true
|
||||||
userIsConnected.set(true);
|
userIsConnected.set(true);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ export class GameManager {
|
|||||||
|
|
||||||
//If player name was not set show login scene with player name
|
//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 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;
|
return LoginSceneName;
|
||||||
} else if (!this.characterLayers || !this.characterLayers.length) {
|
} else if (!this.characterLayers || !this.characterLayers.length) {
|
||||||
return SelectCharacterSceneName;
|
return SelectCharacterSceneName;
|
||||||
|
@ -23,9 +23,7 @@ export class LoginScene extends ResizableScene {
|
|||||||
loginSceneVisibleIframeStore.set(false);
|
loginSceneVisibleIframeStore.set(false);
|
||||||
//If authentication is mandatory, push authentication iframe
|
//If authentication is mandatory, push authentication iframe
|
||||||
if (
|
if (
|
||||||
localUserStore.getAuthToken() == undefined &&
|
localUserStore.getAuthToken() == undefined
|
||||||
gameManager.currentStartedRoom &&
|
|
||||||
gameManager.currentStartedRoom?.authenticationMandatory
|
|
||||||
) {
|
) {
|
||||||
connectionManager.loadOpenIDScreen();
|
connectionManager.loadOpenIDScreen();
|
||||||
loginSceneVisibleIframeStore.set(true);
|
loginSceneVisibleIframeStore.set(true);
|
||||||
|
@ -5,7 +5,7 @@ import { adminApi } from "../Services/AdminApi";
|
|||||||
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
import { AuthTokenData, jwtTokenManager } from "../Services/JWTTokenManager";
|
||||||
import { parse } from "query-string";
|
import { parse } from "query-string";
|
||||||
import { openIDClient } from "../Services/OpenIDClient";
|
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 Axios from "axios";
|
||||||
import { AxiosRequestConfig } from "axios";
|
import { AxiosRequestConfig } from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
@ -69,7 +69,7 @@ export class AuthenticateController extends BaseController {
|
|||||||
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
|
await openIDClient.checkTokenAuth(authTokenData.hydraAccessToken);
|
||||||
res.writeStatus("200");
|
res.writeStatus("200");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
return res.end(JSON.stringify({ authToken: token }));
|
return res.end(JSON.stringify({ authToken: token, username: authTokenData.username }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.info("User was not connected", err);
|
console.info("User was not connected", err);
|
||||||
}
|
}
|
||||||
@ -81,10 +81,10 @@ export class AuthenticateController extends BaseController {
|
|||||||
if (!sub) {
|
if (!sub) {
|
||||||
throw new Error("No sub in the response");
|
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");
|
res.writeStatus("200");
|
||||||
this.addCorsHeaders(res);
|
this.addCorsHeaders(res);
|
||||||
return res.end(JSON.stringify({ authToken }));
|
return res.end(JSON.stringify({ authToken: authToken, username: userInfo.username }));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return this.errorToResponse(e, res);
|
return this.errorToResponse(e, res);
|
||||||
}
|
}
|
||||||
@ -173,32 +173,38 @@ export class AuthenticateController extends BaseController {
|
|||||||
res.onAborted(() => {
|
res.onAborted(() => {
|
||||||
console.warn("Login request was aborted");
|
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 axiosConfig: AxiosRequestConfig = {};
|
||||||
const agent = new https.Agent({
|
|
||||||
rejectUnauthorized: false,
|
if (DEBUG_IGNORE_SSL) {
|
||||||
});
|
const agent = new https.Agent({
|
||||||
axiosConfig.httpsAgent = 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,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 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 FRONT_URL = process.env.FRONT_URL || "http://localhost";
|
||||||
export const OPID_CLIENT_ID = process.env.OPID_CLIENT_ID || "";
|
export const DISABLE_ANONYMOUS = process.env.DISABLE_ANONYMOUS ? process.env.DISABLE_ANONYMOUS == "true" : false;
|
||||||
export const OPID_CLIENT_SECRET = process.env.OPID_CLIENT_SECRET || "";
|
export const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID || "";
|
||||||
export const OPID_CLIENT_ISSUER = process.env.OPID_CLIENT_ISSUER || "";
|
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_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;
|
export const DEBUG_PUSHER_FORCE_ROOM_UPDATE = process.env.DEBUG_PUSHER_FORCE_ROOM_UPDATE ? process.env.DEBUG_PUSHER_FORCE_ROOM_UPDATE == "true" : false;
|
||||||
|
|
||||||
|
@ -5,14 +5,15 @@ import { TokenInterface } from "../Controller/AuthenticateController";
|
|||||||
import { adminApi, AdminBannedData } from "../Services/AdminApi";
|
import { adminApi, AdminBannedData } from "../Services/AdminApi";
|
||||||
|
|
||||||
export interface AuthTokenData {
|
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;
|
hydraAccessToken?: string;
|
||||||
|
username?: string;
|
||||||
}
|
}
|
||||||
export const tokenInvalidException = "tokenInvalid";
|
export const tokenInvalidException = "tokenInvalid";
|
||||||
|
|
||||||
class JWTTokenManager {
|
class JWTTokenManager {
|
||||||
public createAuthToken(identifier: string, hydraAccessToken?: string) {
|
public createAuthToken(identifier: string, hydraAccessToken?: string, username?: string) {
|
||||||
return Jwt.sign({ identifier, hydraAccessToken }, SECRET_KEY, { expiresIn: "30d" });
|
return Jwt.sign({ identifier, hydraAccessToken, username }, SECRET_KEY, { expiresIn: "30d" });
|
||||||
}
|
}
|
||||||
|
|
||||||
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {
|
public verifyJWTToken(token: string, ignoreExpiration: boolean = false): AuthTokenData {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Issuer, Client, IntrospectionResponse } 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";
|
import { OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_CLIENT_ISSUER, FRONT_URL } from "../Enum/EnvironmentVariable";
|
||||||
|
|
||||||
const opidRedirectUri = FRONT_URL + "/jwt";
|
const opidRedirectUri = FRONT_URL + "/jwt";
|
||||||
|
|
||||||
@ -8,10 +8,10 @@ class OpenIDClient {
|
|||||||
|
|
||||||
private initClient(): Promise<Client> {
|
private initClient(): Promise<Client> {
|
||||||
if (!this.issuerPromise) {
|
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({
|
return new issuer.Client({
|
||||||
client_id: OPID_CLIENT_ID,
|
client_id: OIDC_CLIENT_ID,
|
||||||
client_secret: OPID_CLIENT_SECRET,
|
client_secret: OIDC_CLIENT_SECRET,
|
||||||
redirect_uris: [opidRedirectUri],
|
redirect_uris: [opidRedirectUri],
|
||||||
response_types: ["code"],
|
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 this.initClient().then((client) => {
|
||||||
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
|
return client.callback(opidRedirectUri, { code }, { nonce }).then((tokenSet) => {
|
||||||
return client.userinfo(tokenSet).then((res) => {
|
return client.userinfo(tokenSet).then((res) => {
|
||||||
@ -41,6 +41,7 @@ class OpenIDClient {
|
|||||||
email: res.email as string,
|
email: res.email as string,
|
||||||
sub: res.sub,
|
sub: res.sub,
|
||||||
access_token: tokenSet.access_token as string,
|
access_token: tokenSet.access_token as string,
|
||||||
|
username: (res.preferred_username || res.username || res.nickname || res.name || res.email) as string,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user