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

@ -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();