Files
cloudly/ts/manager.auth/classes.authmanager.ts
T

192 lines
6.5 KiB
TypeScript

import * as plugins from '../plugins.js';
import type { Cloudly } from '../classes.cloudly.js';
import { logger } from '../logger.js';
import { Authorization } from './classes.authorization.js';
import { User } from './classes.user.js';
export interface IJwtData {
userId: string;
status: 'loggedIn' | 'loggedOut';
expiresAt: number;
}
export class CloudlyAuthManager {
cloudlyRef: Cloudly;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CUser = plugins.smartdata.setDefaultManagerForDoc(this, User);
public CAuthorization = plugins.smartdata.setDefaultManagerForDoc(this, Authorization);
public typedrouter = new plugins.typedrequest.TypedRouter();
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
}
public async createNewSecureToken() {
return plugins.smartunique.uniSimple('secureToken', 64);
}
public async start() {
// lets setup the smartjwtInstance
this.smartjwtInstance = new plugins.smartjwt.SmartJwt();
await this.smartjwtInstance.init();
const kvStore = await this.cloudlyRef.config.appData.getKvStore();
const existingJwtKeys: plugins.tsclass.network.IJwtKeypair = (await kvStore.readKey(
'jwtKeypair',
)) as plugins.tsclass.network.IJwtKeypair;
if (!existingJwtKeys) {
await this.smartjwtInstance.createNewKeyPair();
const newJwtKeys = this.smartjwtInstance.getKeyPairAsJson();
await kvStore.writeKey('jwtKeypair', newJwtKeys);
} else {
this.smartjwtInstance.setKeyPairAsJson(existingJwtKeys);
}
await this.bootstrapInitialAdmin();
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.admin.IReq_Admin_LoginWithUsernameAndPassword>(
'adminLoginWithUsernameAndPassword',
async (dataArg) => {
let jwt: string;
let expiresAtTimestamp: number = Date.now() + 3600 * 1000 * 24 * 7;
const user = await User.findUserByUsernameAndPassword(dataArg.username, dataArg.password);
if (!user) {
logger.log('warn', 'login failed');
throw new plugins.typedrequest.TypedResponseError('login failed');
} else {
jwt = await this.smartjwtInstance.createJWT({
userId: user.id,
status: 'loggedIn',
expiresAt: expiresAtTimestamp,
});
logger.log('success', 'login successful');
}
return {
identity: {
jwt,
userId: user.id,
name: user.data.username,
expiresAt: expiresAtTimestamp,
role: user.data.role,
type: user.data.type,
},
};
},
),
);
}
private async bootstrapInitialAdmin() {
const users = await this.CUser.getInstances({});
const hasHumanUser = users.some((userArg) => userArg.data?.type === 'human');
if (hasHumanUser) {
return;
}
const adminAccount = this.cloudlyRef.config.data.servezoneAdminaccount;
if (!adminAccount) {
throw new Error('SERVEZONE_ADMINACCOUNT is required for first-run Cloudly bootstrap');
}
const separatorIndex = adminAccount.indexOf(':');
if (separatorIndex <= 0 || separatorIndex === adminAccount.length - 1) {
throw new Error('SERVEZONE_ADMINACCOUNT must use username:password format');
}
const username = adminAccount.slice(0, separatorIndex).trim();
const password = adminAccount.slice(separatorIndex + 1);
if (!username || !password) {
throw new Error('SERVEZONE_ADMINACCOUNT must include a non-empty username and password');
}
const user = new this.CUser({
id: await this.CUser.getNewId(),
data: {
type: 'human',
username,
password,
role: 'admin',
},
});
await user.save();
logger.log('success', `created initial admin user ${username}`);
}
public async stop() {}
public validIdentityGuard = new plugins.smartguard.Guard<{
identity: plugins.servezoneInterfaces.data.IIdentity;
}>(
async (dataArg) => {
const jwt = dataArg.identity.jwt;
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
const expired = jwtData.expiresAt < Date.now();
plugins.smartexpect
.expect(jwtData.status)
.setFailMessage('user not logged in')
.toEqual('loggedIn');
plugins.smartexpect.expect(expired).setFailMessage(`jwt expired`).toBeFalse();
plugins.smartexpect
.expect(dataArg.identity.expiresAt)
.setFailMessage(
`expiresAt >>identity valid until:${dataArg.identity.expiresAt}, but jwt says: ${jwtData.expiresAt}<< has been tampered with`,
)
.toEqual(jwtData.expiresAt);
plugins.smartexpect
.expect(dataArg.identity.userId)
.setFailMessage('userId has been tampered with')
.toEqual(jwtData.userId);
if (expired) {
throw new Error('identity is expired');
}
return true;
},
{
failedHint: 'identity is not valid.',
name: 'validIdentityGuard',
},
);
public adminIdentityGuard = new plugins.smartguard.Guard<{
identity: plugins.servezoneInterfaces.data.IIdentity;
}>(
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
const jwt = dataArg.identity.jwt;
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
const user = await this.CUser.getInstance({ id: jwtData.userId });
const isAdminBool = user.data.role === 'admin';
console.log(`user is admin: ${isAdminBool}`);
return isAdminBool;
},
{
failedHint: 'user is not admin.',
name: 'adminIdentityGuard',
},
);
public adminOrClusterIdentityGuard = new plugins.smartguard.Guard<{
identity: plugins.servezoneInterfaces.data.IIdentity;
}>(
async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [this.validIdentityGuard]);
const jwt = dataArg.identity.jwt;
const jwtData: IJwtData = await this.smartjwtInstance.verifyJWTAndGetData(jwt);
const user = await this.CUser.getInstance({ id: jwtData.userId });
return user.data.role === 'admin' || user.data.role === 'cluster';
},
{
failedHint: 'user is not admin or cluster.',
name: 'adminOrClusterIdentityGuard',
},
);
}