FEATURE: users can now login via an openID client
This commit is contained in:
parent
74975ac9d8
commit
9c803a69ff
26 changed files with 866 additions and 1536 deletions
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue