FEATURE: users can now login via an openID client

This commit is contained in:
kharhamel 2021-07-27 16:37:01 +02:00
parent 74975ac9d8
commit 9c803a69ff
26 changed files with 866 additions and 1536 deletions

View file

@ -4,6 +4,7 @@ import { BaseController } from "./BaseController";
import { adminApi } from "../Services/AdminApi";
import { jwtTokenManager } from "../Services/JWTTokenManager";
import { parse } from "query-string";
import { openIDClient } from "../Services/OpenIDClient";
export interface TokenInterface {
userUuid: string;
@ -12,11 +13,58 @@ export interface TokenInterface {
export class AuthenticateController extends BaseController {
constructor(private App: TemplatedApp) {
super();
this.openIDLogin();
this.openIDCallback();
this.register();
this.verify();
this.anonymLogin();
}
openIDLogin() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-screen", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { nonce, state } = parse(req.getQuery());
if (!state || !nonce) {
res.writeStatus("400 Unauthorized").end("missing state and nonce URL parameters");
return;
}
try {
const loginUri = await openIDClient.authorizationUrl(state as string, nonce as string);
res.writeStatus("302");
res.writeHeader("Location", loginUri);
return res.end();
} catch (e) {
return this.errorToResponse(e, res);
}
});
}
openIDCallback() {
//eslint-disable-next-line @typescript-eslint/no-misused-promises
this.App.get("/login-callback", async (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => {
console.warn("/message request was aborted");
});
const { code, nonce } = parse(req.getQuery());
try {
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);
res.writeStatus("200");
this.addCorsHeaders(res);
return res.end(JSON.stringify({ authToken }));
} catch (e) {
return this.errorToResponse(e, res);
}
});
}
//Try to login with an admin token
private register() {
this.App.options("/register", (res: HttpResponse, req: HttpRequest) => {
@ -39,11 +87,12 @@ export class AuthenticateController extends BaseController {
if (typeof organizationMemberToken != "string") throw new Error("No organization token");
const data = await adminApi.fetchMemberDataByToken(organizationMemberToken);
const userUuid = data.userUuid;
const email = data.email;
const roomUrl = data.roomUrl;
const mapUrlStart = data.mapUrlStart;
const textures = data.textures;
const authToken = jwtTokenManager.createJWTToken(userUuid);
const authToken = jwtTokenManager.createAuthToken(email || userUuid);
res.writeStatus("200 OK");
this.addCorsHeaders(res);
res.end(
@ -63,45 +112,6 @@ export class AuthenticateController extends BaseController {
});
}
private verify() {
this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res);
res.end();
});
this.App.get("/verify", (res: HttpResponse, req: HttpRequest) => {
(async () => {
const query = parse(req.getQuery());
res.onAborted(() => {
console.warn("verify request was aborted");
});
try {
await jwtTokenManager.getUserUuidFromToken(query.token as string);
} catch (e) {
res.writeStatus("400 Bad Request");
this.addCorsHeaders(res);
res.end(
JSON.stringify({
success: false,
message: "Invalid JWT token",
})
);
return;
}
res.writeStatus("200 OK");
this.addCorsHeaders(res);
res.end(
JSON.stringify({
success: true,
})
);
})();
});
}
//permit to login on application. Return token to connect on Websocket IO.
private anonymLogin() {
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
@ -115,7 +125,7 @@ export class AuthenticateController extends BaseController {
});
const userUuid = v4();
const authToken = jwtTokenManager.createJWTToken(userUuid);
const authToken = jwtTokenManager.createAuthToken(userUuid);
res.writeStatus("200 OK");
this.addCorsHeaders(res);
res.end(

View file

@ -22,7 +22,7 @@ import {
import { UserMovesMessage } from "../Messages/generated/messages_pb";
import { TemplatedApp } from "uWebSockets.js";
import { parse } from "query-string";
import { jwtTokenManager } from "../Services/JWTTokenManager";
import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenManager";
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers";
@ -173,31 +173,34 @@ export class IoSocketController {
characterLayers = [characterLayers];
}
const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId);
const tokenData =
token && typeof token === "string" ? jwtTokenManager.decodeJWTToken(token) : null;
const userIdentifier = tokenData ? tokenData.identifier : "";
let memberTags: string[] = [];
let memberVisitCardUrl: string | null = null;
let memberMessages: unknown;
let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId);
let userData: FetchMemberDataByUuidResponse = {
userUuid: userIdentifier,
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
if (ADMIN_API_URL) {
try {
let userData: FetchMemberDataByUuidResponse = {
uuid: v4(),
tags: [],
visitCardUrl: null,
textures: [],
messages: [],
anonymous: true,
};
try {
userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId);
userData = await adminApi.fetchMemberDataByUuid(userIdentifier, roomId, IPAddress);
} catch (err) {
if (err?.response?.status == 404) {
// If we get an HTTP 404, the token is invalid. Let's perform an anonymous login!
console.warn(
'Cannot find user with uuid "' +
userUuid +
'Cannot find user with email "' +
(userIdentifier || "anonymous") +
'". Performing an anonymous login instead.'
);
} else if (err?.response?.status == 403) {
@ -235,7 +238,12 @@ export class IoSocketController {
throw new Error("Use the login URL to connect");
}
} catch (e) {
console.log("access not granted for user " + userUuid + " and room " + roomId);
console.log(
"access not granted for user " +
(userIdentifier || "anonymous") +
" and room " +
roomId
);
console.error(e);
throw new Error("User cannot access this world");
}
@ -257,7 +265,7 @@ export class IoSocketController {
// Data passed here is accessible on the "websocket" socket object.
url,
token,
userUuid,
userUuid: userData.userUuid,
IPAddress,
roomId,
name,
@ -287,15 +295,10 @@ export class IoSocketController {
context
);
} catch (e) {
/*if (e instanceof Error) {
console.log(e.message);
res.writeStatus("401 Unauthorized").end(e.message);
} else {
res.writeStatus("500 Internal Server Error").end('An error occurred');
}*/
return res.upgrade(
res.upgrade(
{
rejected: true,
reason: e.reason || null,
message: e.message ? e.message : "500 Internal Server Error",
},
websocketKey,
@ -310,12 +313,14 @@ export class IoSocketController {
open: (ws) => {
if (ws.rejected === true) {
//FIX ME to use status code
if (ws.message === "World is full") {
if (ws.reason === tokenInvalidException) {
socketManager.emitTokenExpiredMessage(ws);
} else if (ws.message === "World is full") {
socketManager.emitWorldFullMessage(ws);
} else {
socketManager.emitConnexionErrorMessage(ws, ws.message as string);
}
ws.close();
setTimeout(() => ws.close(), 0);
return;
}