Skip to content

Commit

Permalink
wip: normalized the session.auth creation w/ types
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Oct 20, 2023
1 parent db8cbbb commit 65fc8b2
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 82 deletions.
21 changes: 0 additions & 21 deletions core/components/AdminVault/providers/CitizenFX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export default class CitizenFXProvider {

/**
* Processes the callback and returns the tokenSet
* @param {object} ctx
*/
async processCallback(ctx: InitializedCtx, redirectUri: string, stateKern: string) {
if (!this.client) throw new Error(`${modulename} is not ready`);
Expand Down Expand Up @@ -95,24 +94,4 @@ export default class CitizenFXProvider {

return { ...parsed, picture };
}


/**
* Returns the session auth object
* NOTE: increasing session duration to 24 hours since we do not have refresh tokens
*
* @param {object} tokenSet
* @param {object} userInfo
* @param {string} identifier
*/
async getUserSessionInfo(tokenSet, userInfo, identifier: string) {
return {
type: 'cfxre',
forumUsername: userInfo.name,
identifier: identifier,
// expires_at: tokenSet.expires_at * 1000,
expires_at: Date.now() + 86_400_000, //24h
picture: userInfo.picture,
};
}
};
29 changes: 17 additions & 12 deletions core/components/WebServer/authLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,21 @@ const console = consoleFactory(modulename);
*/
export class AuthedAdmin {
public readonly name: string;
public readonly permissions: string[];
public readonly isMaster: boolean;
public readonly permissions: string[];
public readonly isTempPassword: boolean;
public readonly profilePicture: string | undefined;
public readonly csrfToken?: string;
readonly #txAdmin: TxAdmin;

constructor(txAdmin: TxAdmin, vaultAdmin: any) {
constructor(txAdmin: TxAdmin, vaultAdmin: any, csrfToken?: string) {
this.#txAdmin = txAdmin;
this.name = vaultAdmin.name;
this.isMaster = vaultAdmin.master;
this.permissions = vaultAdmin.permissions;
this.isTempPassword = (typeof vaultAdmin.password_temporary !== 'undefined');

this.csrfToken = csrfToken;

const cachedPfp = txAdmin.persistentCache.get(`admin:picture:${vaultAdmin.name}`);
this.profilePicture = typeof cachedPfp === 'string' ? cachedPfp : undefined;
}
Expand Down Expand Up @@ -86,9 +88,9 @@ type AuthLogicReturnType = {
success: false;
rejectReason?: string;
};
const successResp = (txAdmin: TxAdmin, vaultAdmin: any) => ({
const successResp = (txAdmin: TxAdmin, vaultAdmin: any, csrfToken?: string) => ({
success: true,
admin: new AuthedAdmin(txAdmin, vaultAdmin),
admin: new AuthedAdmin(txAdmin, vaultAdmin, csrfToken),
} as const)
const failResp = (reason?: string) => ({
success: false,
Expand All @@ -103,17 +105,20 @@ const validPassSessAuthSchema = z.object({
type: z.literal('password'),
username: z.string(),
csrfToken: z.string(),
expires_at: z.literal(false),
expiresAt: z.literal(false),
password_hash: z.string(),
});
export type PassSessAuthType = z.infer<typeof validPassSessAuthSchema>;

const validCfxreSessAuthSchema = z.object({
type: z.literal('cfxre'),
username: z.string(),
csrfToken: z.string(),
expires_at: z.number(),
forumUsername: z.string(),
expiresAt: z.number(),
identifier: z.string(),
});
export type CfxreSessAuthType = z.infer<typeof validCfxreSessAuthSchema>;

const validSessAuthSchema = z.discriminatedUnion('type', [
validPassSessAuthSchema,
validCfxreSessAuthSchema
Expand Down Expand Up @@ -151,7 +156,7 @@ export const normalAuthLogic = (
const sessAuth = validationResult.data;

// Checking for expiration
if (sessAuth.expires_at !== false && Date.now() > sessAuth.expires_at) {
if (sessAuth.expiresAt !== false && Date.now() > sessAuth.expiresAt) {
return failResp(`Expired session from '${sess.auth.username}'.`);
}

Expand All @@ -166,15 +171,15 @@ export const normalAuthLogic = (
if (vaultAdmin.password_hash !== sessAuth.password_hash) {
return failResp(`Password hash doesn't match for '${sessAuth.username}'.`);
}
return successResp(txAdmin, vaultAdmin);
return successResp(txAdmin, vaultAdmin, sessAuth.csrfToken);
} else if (sessAuth.type === 'cfxre') {
if (
typeof vaultAdmin.providers.citizenfx !== 'object'
|| vaultAdmin.providers.citizenfx.identifier !== sessAuth.identifier
) {
return failResp(`Cfxre identifier doesn't match for '${sessAuth.username}'.`);
}
return successResp(txAdmin, vaultAdmin);
return successResp(txAdmin, vaultAdmin, sessAuth.csrfToken);
} else {
return failResp('Invalid auth type.');
}
Expand Down Expand Up @@ -232,7 +237,7 @@ export const nuiAuthLogic = (
//this one is handled differently in resource/menu/client/cl_base.lua
return failResp('admin_not_found');
}
return successResp(vaultAdmin, txAdmin);
return successResp(txAdmin, vaultAdmin, undefined);
} catch (error) {
console.debug(`Error validating session data: ${(error as Error).message}`);
return failResp('Error validating auth header');
Expand Down
20 changes: 0 additions & 20 deletions core/components/WebServer/ctxTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,7 @@ import { AuthedAdminType } from "./authLogic";

/**
* Session stuff
* FIXME: move the cfx and password to their respective routes
*/
export type CommonSessionType = {
username: string;
picture?: string;
csrfToken: string;
}

export type CfxreSessionType = CommonSessionType & {
type: 'cfxre';
expires_at: number;
provider_uid: string;
provider_identifier: string;
}

export type PasswordSessionType = CommonSessionType & {
type: 'password';
expires_at: false;
password_hash: string;
}

//From the koa-session docs, the DefinitelyTyped package is wrong.
export type DefaultCtxSession = Readonly<{
isNew?: true;
Expand Down
2 changes: 1 addition & 1 deletion core/components/WebServer/getReactIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@ export default async function getReactIndex(ctx: CtxWithVars | AuthedCtx) {
permissions: authedAdmin.permissions,
isTempPassword: authedAdmin.isTempPassword,
profilePicture: authedAdmin.profilePicture,
csrfToken: authedAdmin.csrfToken, //might not exist
},
csrfToken: (ctx.session?.auth?.csrfToken) ? ctx.session.auth.csrfToken : 'not_set',
}

//Prepare placeholders
Expand Down
2 changes: 1 addition & 1 deletion core/components/WebServer/middlewares/authMws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const apiAuthMw = async (ctx: InitializedCtx, next: Function) => {
//For web routes, we need to check the CSRF token
//For nui routes, we need to check the luaComToken, which is already done in nuiAuthLogic above
if (ctx.txVars.isWebInterface) {
const sessToken = ctx.session?.auth?.csrfToken; //it should exist because of authLogic
const sessToken = authResult.admin?.csrfToken; //it should exist for nui because of authLogic
const headerToken = ctx.headers['x-txadmin-csrftoken'];
if (!sessToken || !headerToken || sessToken !== headerToken) {
console.verbose.warn(`Invalid CSRF token: ${ctx.path}`);
Expand Down
24 changes: 17 additions & 7 deletions core/components/WebServer/middlewares/ctxUtilsMw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import DynamicAds from '../../DynamicAds';
import { Next } from 'koa';
import { CtxWithVars } from '../ctxTypes';
import consts from '@extras/consts';
import { AuthedAdminType } from '../authLogic';
const console = consoleFactory(modulename);

//Types
Expand Down Expand Up @@ -101,11 +102,17 @@ async function loadWebTemplate(name: string) {
* Renders normal views.
* Footer and header are configured inside the view template itself.
*/
async function renderView(view: string, reqSess: any, data: any, txVars: CtxTxVars, dynamicAds: DynamicAds) {
data.adminIsMaster = (reqSess && reqSess.auth && reqSess.auth.username && reqSess.auth.master === true);
data.adminUsername = (reqSess && reqSess.auth && reqSess.auth.username) ? reqSess.auth.username : 'unknown user';
data.profilePicture = (reqSess && reqSess.auth && reqSess.auth.picture) ? reqSess.auth.picture : DEFAULT_AVATAR;
data.isTempPassword = (reqSess && reqSess.auth && reqSess.auth.isTempPassword);
async function renderView(
view: string,
possiblyAuthedAdmin: AuthedAdminType | undefined,
data: any,
txVars: CtxTxVars,
dynamicAds: DynamicAds
) {
data.adminUsername = possiblyAuthedAdmin?.name ?? 'unknown user';
data.adminIsMaster = possiblyAuthedAdmin && possiblyAuthedAdmin.isMaster;
data.profilePicture = possiblyAuthedAdmin?.profilePicture ?? DEFAULT_AVATAR;
data.isTempPassword = possiblyAuthedAdmin && possiblyAuthedAdmin.isTempPassword;
data.isLinux = !txEnv.isWindows;
data.showAdvanced = (convars.isDevMode || console.isVerbose);
data.dynamicAd = txVars.isWebInterface && dynamicAds.pick('main');
Expand Down Expand Up @@ -153,6 +160,9 @@ export default async function setupUtilsMw(ctx: CtxWithVars, next: Next) {
//Usage stats
txAdmin.statisticsManager?.pageViews.count(view);

//Typescript is very annoying
const possiblyAuthedAdmin = ctx.admin as AuthedAdminType | undefined;

// Setting up default render data:
const baseViewData = {
isWebInterface,
Expand All @@ -169,7 +179,7 @@ export default async function setupUtilsMw(ctx: CtxWithVars, next: Next) {
isZapHosting: convars.isZapHosting, //not in use
isPterodactyl: convars.isPterodactyl, //not in use
isWebInterface: isWebInterface,
csrfToken: (ctx.session?.auth?.csrfToken) ? ctx.session.auth.csrfToken : 'not_set',
csrfToken: (possiblyAuthedAdmin?.csrfToken) ? possiblyAuthedAdmin.csrfToken : 'not_set',
TX_BASE_PATH: (isWebInterface) ? '' : consts.nuiWebpipePath,
PAGE_TITLE: data?.headerTitle ?? 'txAdmin',
}),
Expand All @@ -179,7 +189,7 @@ export default async function setupUtilsMw(ctx: CtxWithVars, next: Next) {
if (view == 'login') {
ctx.body = await renderLoginView(renderData, ctx.txVars, txAdmin.dynamicAds);
} else {
ctx.body = await renderView(view, ctx.session, renderData, ctx.txVars, txAdmin.dynamicAds);
ctx.body = await renderView(view, possiblyAuthedAdmin, renderData, ctx.txVars, txAdmin.dynamicAds);
}
ctx.type = 'text/html';
};
Expand Down
23 changes: 14 additions & 9 deletions core/webroutes/authentication/addMaster.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
const modulename = 'WebServer:AuthAddMaster';
import { UserInfoType } from '@core/components/AdminVault/providers/CitizenFX';
import { CfxreSessAuthType } from '@core/components/WebServer/authLogic';
import { InitializedCtx } from '@core/components/WebServer/ctxTypes';
import consoleFactory from '@extras/console';
import { TokenSet } from 'openid-client';
const console = consoleFactory(modulename);

//Helper functions
Expand Down Expand Up @@ -190,17 +192,19 @@ async function handleSave(ctx: InitializedCtx) {
}

//Checking if session is still present
const userInfo = ctx.session.tmpAddMasterUserInfo as UserInfoType;
const tokenSet = ctx.session.tmpAddMasterTokenSet as TokenSet;
if (
typeof ctx.session.tmpAddMasterUserInfo === 'undefined'
|| typeof ctx.session.tmpAddMasterUserInfo?.name !== 'string'
typeof userInfo?.name !== 'string'
|| typeof tokenSet?.access_token !== 'string'
) {
return returnJustMessage(
ctx,
'Invalid Session.',
'You may have restarted txAdmin right before entering this page. Please try again.',
);
}
const userInfo = ctx.session.tmpAddMasterUserInfo as UserInfoType;


//Getting identifier
let identifier;
Expand Down Expand Up @@ -243,13 +247,14 @@ async function handleSave(ctx: InitializedCtx) {

//Login user
try {
ctx.session.auth = await ctx.txAdmin.adminVault.providers.citizenfx.getUserSessionInfo(
ctx.session.tmpAddMasterTokenSet,
userInfo,
ctx.session.auth = {
type: 'cfxre',
username: userInfo.name,
csrfToken: ctx.txAdmin.adminVault.genCsrfToken(),
expiresAt: Date.now() + 86_400_000, //24h,
identifier,
);
ctx.session.auth.username = userInfo.name;
ctx.session.auth.csrfToken = ctx.txAdmin.adminVault.genCsrfToken();
} satisfies CfxreSessAuthType;

delete ctx.session.tmpAddMasterTokenSet;
delete ctx.session.tmpAddMasterUserInfo;
} catch (error) {
Expand Down
13 changes: 9 additions & 4 deletions core/webroutes/authentication/providerCallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import crypto from 'node:crypto';
import { isValidRedirectPath } from '@core/extras/helpers';
import consoleFactory from '@extras/console';
import { InitializedCtx } from '@core/components/WebServer/ctxTypes';
import { CfxreSessAuthType } from '@core/components/WebServer/authLogic';
const console = consoleFactory(modulename);

//Helper functions
Expand Down Expand Up @@ -120,13 +121,17 @@ export default async function AuthProviderCallback(ctx: InitializedCtx) {
}

//Setting session
ctx.session.auth = await ctx.txAdmin.adminVault.providers.citizenfx.getUserSessionInfo(tokenSet, userInfo, identifier);
ctx.session.auth.username = vaultAdmin.name;
ctx.session.auth.csrfToken = ctx.txAdmin.adminVault.genCsrfToken();
ctx.session.auth = {
type: 'cfxre',
username: userInfo.name,
csrfToken: ctx.txAdmin.adminVault.genCsrfToken(),
expiresAt: Date.now() + 86_400_000, //24h,
identifier,
} satisfies CfxreSessAuthType;

//Save the updated provider identifier & data to the admins file
await ctx.txAdmin.adminVault.refreshAdminSocialData(vaultAdmin.name, 'citizenfx', identifier, userInfo);

//If the user has a picture, save it to the cache
if (userInfo.picture) {
ctx.txAdmin.persistentCache.set(`admin:picture:${vaultAdmin.name}`, userInfo.picture);
Expand Down
7 changes: 3 additions & 4 deletions core/webroutes/authentication/verifyPassword.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const modulename = 'WebServer:AuthVerifyPassword';
import { PassSessAuthType } from '@core/components/WebServer/authLogic';
import { InitializedCtx } from '@core/components/WebServer/ctxTypes';
import { isValidRedirectPath } from '@core/extras/helpers';
import consoleFactory from '@extras/console';
Expand Down Expand Up @@ -34,15 +35,13 @@ export default async function AuthVerifyPassword(ctx: InitializedCtx) {
}

//Setting up session
const providerWithPicture = Object.values(vaultAdmin.providers).find((provider) => provider.data && provider.data.picture);
ctx.session.auth = {
type: 'password',
username: vaultAdmin.name,
picture: (providerWithPicture) ? providerWithPicture.data.picture : undefined,
password_hash: vaultAdmin.password_hash,
expires_at: false,
expiresAt: false,
csrfToken: ctx.txAdmin.adminVault.genCsrfToken(),
};
} satisfies PassSessAuthType;

ctx.txAdmin.logger.admin.write(vaultAdmin.name, `logged in from ${ctx.ip} via password`);
ctx.txAdmin.statisticsManager.loginOrigins.count(ctx.txVars.hostType);
Expand Down
3 changes: 1 addition & 2 deletions docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ Processo:
- [x] check `playerDatabase.registerAction` expiration type error
- [x] translation fix the `The player was xxxxx` lines in all files
- [x] corrigir comportamento dos profile pictures
- [ ] renomear csrfToken e forumUsername pra snake case, mas no AuthedAdmin pode ficar tudo como camel
- sim, colocar o csrfToken pra dentro do admin, e tb editar o preAuth
- [x] normalize the session.auth creation w/ types
- [ ] checar o que fazer com as stats no `core/webroutes/authentication/self.ts`
- [ ] escrever teste automatizado pros authMiddlewares e authLogic

Expand Down
2 changes: 1 addition & 1 deletion shared/InjectedTxConstsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export type ReactPreauthType = {
isMaster: boolean;
isTempPassword: boolean;
profilePicture: any;
csrfToken?: string;
}

export type InjectedTxConsts = {
Expand All @@ -17,5 +18,4 @@ export type InjectedTxConsts = {

//Auth
preAuth: ReactPreauthType | false;
csrfToken: string; //FIXME: probably inside preAuth
}

0 comments on commit 65fc8b2

Please sign in to comment.