@casual-simulation/aux-vm-browser
Version:
A set of utilities required to securely run an AUX in a web browser.
490 lines • 19.2 kB
JavaScript
import { BehaviorSubject, NEVER, Subject, Subscription, filter, firstValueFrom, switchMap, } from 'rxjs';
import { asyncResult, hasValue, reportInst, generateV1ConnectionToken, } from '@casual-simulation/aux-common';
/**
* Defines a class that is able to coordinate authentication across multiple simulations.
*/
export class AuthCoordinator {
get onMissingPermission() {
return this._onMissingPermission;
}
get onNotAuthorized() {
return this._onNotAuthorized;
}
get onShowAccountInfo() {
return this._onShowAccountInfo;
}
get onRequestAccess() {
return this._onRequestAccess;
}
get onGrantEntitlements() {
return this._onGrantEntitlements;
}
get authEndpoints() {
const helper = this.authHelper;
if (helper) {
return helper.endpoints;
}
return new Map();
}
get onAuthEndpointDiscovered() {
return this._onAuthHelper.pipe(switchMap((helper) => helper ? helper.onEndpointDiscovered : NEVER));
}
get authHelper() {
return this._onAuthHelper.value;
}
set authHelper(value) {
this._onAuthHelper.next(value);
}
constructor(manager) {
this._onMissingPermission = new Subject();
this._onRequestAccess = new Subject();
this._onNotAuthorized = new Subject();
this._onShowAccountInfo = new Subject();
this._onGrantEntitlements = new Subject();
this._onAuthHelper = new BehaviorSubject(null);
this._simulationManager = manager;
this._sub = new Subscription();
this._sub.add(this._simulationManager.watchSimulations((sim) => {
let sub = new Subscription();
sub.add(sim.onAuthMessage.subscribe(async (msg) => {
if (msg.type === 'request') {
this._handleAuthRequest(sim, msg);
}
else if (msg.type === 'external_permission_request') {
this._onRequestAccess.next({
simulationId: sim.id,
origin: msg.origin,
reason: msg.reason,
user: msg.user,
});
}
}));
sub.add(sim.localEvents.subscribe((event) => {
if (event.type === 'show_account_info') {
this.showAccountInfo(sim.id);
if (hasValue(event.taskId)) {
sim.helper.transaction(asyncResult(event.taskId, null));
}
}
else if (event.type === 'grant_record_entitlements') {
this._onGrantEntitlements.next({
simulationId: sim.id,
action: event,
});
}
}));
return sub;
}));
}
async openAccountDashboard(simId) {
var _a;
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
await sim.auth.primary.openAccountPage();
}
else {
await ((_a = this.authHelper) === null || _a === void 0 ? void 0 : _a.primary.openAccountPage());
}
}
async logout(simId) {
var _a;
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
await sim.auth.primary.logout();
}
else {
await ((_a = this.authHelper) === null || _a === void 0 ? void 0 : _a.primary.logout());
}
}
async showReportInst(simId) {
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
await sim.helper.transaction(reportInst());
}
}
async showAccountInfo(simId) {
console.log(`[AuthCoordinator] [${simId}] Show account info`);
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
const endpoint = sim.auth.primary;
const status = endpoint.currentLoginStatus;
if (status) {
this._onShowAccountInfo.next({
simulationId: sim.id,
loginStatus: status,
endpoint: endpoint.origin,
});
}
}
else if (this.authHelper) {
const endpoint = this.authHelper.primary;
const status = endpoint.currentLoginStatus;
if (status) {
this._onShowAccountInfo.next({
simulationId: null,
loginStatus: status,
endpoint: endpoint.origin,
});
}
}
}
async changeLogin(simId, origin) {
console.log(`[AuthCoordinator] [${simId}] Changing login...`);
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
const endpoint = sim.auth.primary;
await endpoint.logout();
await endpoint.authenticate();
const key = await endpoint.getConnectionKey();
if (key) {
const connectionId = sim.configBotId;
const recordName = sim.origin.recordName;
const inst = sim.inst;
const token = generateV1ConnectionToken(key, connectionId, recordName, inst);
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: origin,
indicator: {
connectionToken: token,
},
});
}
}
}
async requestAccessToMissingPermission(simId, origin, reason) {
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
const promise = firstValueFrom(sim.onAuthMessage.pipe(filter((m) => m.origin === origin &&
m.type === 'external_permission_result')));
console.log(`[AuthCoordinator] [${sim.id}] Requesting permission`, reason);
sim.sendAuthMessage({
type: 'permission_request',
origin: origin,
reason,
});
const response = await Promise.race([
promise,
new Promise((resolve) => {
setTimeout(() => {
resolve({
type: 'external_permission_result',
origin,
success: false,
recordName: reason.recordName,
resourceKind: reason.resourceKind,
resourceId: reason.resourceKind,
subjectType: reason.subjectType,
subjectId: reason.subjectId,
errorCode: 'not_authorized',
errorMessage: 'The request expired.',
});
}, 45 * 1000);
}),
]);
console.log(`[AuthCoordinator] [${sim.id}] Got permission result`, response);
if (response.type === 'external_permission_result') {
console.log(`[AuthCoordinator] [${sim.id}] Got permission result`, response);
return {
...response,
type: 'permission_result',
};
}
}
return {
type: 'permission_result',
origin,
success: false,
recordName: reason.recordName,
resourceKind: reason.resourceKind,
resourceId: reason.resourceKind,
subjectType: reason.subjectType,
subjectId: reason.subjectId,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
async grantAccessToMissingPermission(simId, origin, reason, expireTimeMs = null, actions = null) {
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
const recordName = reason.recordName;
const resourceKind = reason.resourceKind;
const resourceId = reason.resourceId;
const subjectType = reason.subjectType;
const subjectId = reason.subjectId;
if (!actions) {
const result = await sim.auth.primary.grantPermission(recordName, {
resourceKind,
resourceId,
subjectType,
subjectId,
action: null,
options: {},
expireTimeMs,
});
if (result.success === true) {
sim.sendAuthMessage({
type: 'permission_result',
success: true,
origin,
recordName,
resourceKind,
resourceId,
subjectType,
subjectId,
});
}
return result;
}
else {
for (let action of actions) {
const result = await sim.auth.primary.grantPermission(recordName, {
resourceKind,
resourceId,
subjectType,
subjectId,
action: action,
options: {},
expireTimeMs,
});
if (result.success === false) {
return result;
}
}
sim.sendAuthMessage({
type: 'permission_result',
success: true,
origin,
recordName,
resourceKind,
resourceId,
subjectType,
subjectId,
});
return {
success: true,
};
}
}
console.error('[AuthCoordinator] Could not find simulation to grant access to.', simId, origin, reason);
return {
success: false,
errorCode: 'server_error',
errorMessage: 'A server error occurred.',
};
}
async respondToPermissionRequest(simId, origin, result) {
const sim = this._simulationManager.simulations.get(simId);
if (sim) {
sim.sendAuthMessage(result);
}
}
async grantEntitlements(entitlementGrantEvent) {
const sim = this._simulationManager.simulations.get(entitlementGrantEvent.simulationId);
if (sim) {
await sim.records.grantEntitlements(entitlementGrantEvent.action);
}
}
async denyEntitlements(entitlementGrantEvent) {
const sim = this._simulationManager.simulations.get(entitlementGrantEvent.simulationId);
if (sim) {
sim.helper.transaction(asyncResult(entitlementGrantEvent.action.taskId, {
success: false,
errorCode: 'not_authorized',
errorMessage: 'The request for access was denied.',
}));
}
}
async _handleAuthRequest(sim, request) {
if (request.kind === 'need_indicator') {
await this._handleNeedIndicator(sim, request);
}
else if (request.kind === 'invalid_indicator') {
await this._handleInvalidIndicator(sim, request);
}
else if (request.kind === 'not_authorized') {
await this._handleNotAuthorized(sim, request);
}
}
async _handleNeedIndicator(sim, request) {
console.log(`[AuthCoordinator] [${sim.id}] Needs indicator`);
const endpoint = sim.auth.primary;
const key = await endpoint.getConnectionKey();
if (!key) {
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionId.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: request.origin,
indicator: {
connectionId: sim.configBotId,
},
});
}
else {
const connectionId = sim.configBotId;
const recordName = sim.origin.recordName;
const inst = sim.inst;
const token = generateV1ConnectionToken(key, connectionId, recordName, inst);
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: request.origin,
indicator: {
connectionToken: token,
},
});
}
}
async _handleInvalidIndicator(sim, request) {
console.log(`[AuthCoordinator] [${sim.id}] [${request.errorCode}] Invalid indicator.`);
const endpoint = sim.auth.primary;
let key;
if (request.errorCode === 'invalid_token') {
if (await endpoint.isAuthenticated()) {
console.log(`[AuthCoordinator] [${sim.id}] Logging out and back in...`);
await endpoint.relogin();
}
}
else {
key = await endpoint.getConnectionKey();
if (!key) {
console.log(`[AuthCoordinator] [${sim.id}] Logging in...`);
await endpoint.authenticate();
}
}
if (!key) {
key = await endpoint.getConnectionKey();
}
if (key) {
const connectionId = sim.configBotId;
const recordName = sim.origin.recordName;
const inst = sim.inst;
const token = generateV1ConnectionToken(key, connectionId, recordName, inst);
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: request.origin,
indicator: {
connectionToken: token,
},
});
}
}
async _handleNotAuthorized(sim, request) {
var _a, _b;
if (request.errorCode === 'not_logged_in') {
await this._handleNotLoggedIn(sim, request);
}
else if (((_a = request.reason) === null || _a === void 0 ? void 0 : _a.type) === 'missing_permission') {
await this._handleMissingPermission(sim, request, request.reason);
}
else if (((_b = request.reason) === null || _b === void 0 ? void 0 : _b.type) === 'invalid_token') {
// Trying to watch a branch that the current connection token doesn't support
await this._handleInvalidToken(sim, request);
}
else {
await this._handleNotAuthorizedError(sim, request);
}
}
async _handleInvalidToken(sim, request) {
var _a, _b, _c;
const recordName = (_a = request.resource) === null || _a === void 0 ? void 0 : _a.recordName;
const inst = (_b = request.resource) === null || _b === void 0 ? void 0 : _b.inst;
const branch = (_c = request.resource) === null || _c === void 0 ? void 0 : _c.branch;
if (!recordName || !inst || !branch) {
console.log(`[AuthCoordinator] [${sim.id}] Invalid token request missing recordName, inst, or branch`);
return;
}
// Only allow automatically loading branches that start with 'doc/'
// This is a temporary solution to prevent loading actual existing inst data and instead only allow loading
// shared documents from other records
if (!branch.startsWith('doc/')) {
console.error(`[AuthCoordinator] [${sim.id}] Invalid token request branch does not start with 'doc/'`);
return;
}
console.log(`[AuthCoordinator] [${sim.id}] Needs new indicator`);
const endpoint = sim.auth.primary;
const key = await endpoint.getConnectionKey();
if (!key) {
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionId.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: request.origin,
indicator: {
connectionId: sim.configBotId,
},
});
}
else {
const connectionId = sim.configBotId;
const token = generateV1ConnectionToken(key, connectionId, recordName, inst);
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: request.origin,
indicator: {
connectionToken: token,
},
});
}
}
async _handleNotLoggedIn(sim, request) {
console.log(`[AuthCoordinator] [${sim.id}] Not logged in`);
const endpoint = sim.auth.primary;
let key = await endpoint.getConnectionKey();
if (!key) {
if (!(await endpoint.isAuthenticated())) {
console.log(`[AuthCoordinator] [${sim.id}] Logging in...`);
await endpoint.authenticate();
key = await endpoint.getConnectionKey();
}
}
if (key) {
const connectionId = sim.configBotId;
const recordName = sim.origin.recordName;
const inst = sim.inst;
const token = generateV1ConnectionToken(key, connectionId, recordName, inst);
console.log(`[AuthCoordinator] [${sim.id}] Sending connectionToken.`);
sim.sendAuthMessage({
type: 'response',
success: true,
origin: request.origin,
indicator: {
connectionToken: token,
},
});
}
}
async _handleMissingPermission(sim, request, reason) {
console.log(`[AuthCoordinator] [${sim.id}] Missing permission ${reason.resourceKind}.${reason.action}.`);
this._onMissingPermission.next({
simulationId: sim.id,
errorCode: request.errorCode,
errorMessage: request.errorMessage,
origin: request.origin,
reason,
});
}
async _handleNotAuthorizedError(sim, request) {
console.log(`[AuthCoordinator] [${sim.id}] Not authorized: ${request.errorMessage}.`);
this._onNotAuthorized.next({
simulationId: sim.id,
errorCode: request.errorCode,
errorMessage: request.errorMessage,
origin: request.origin,
});
}
unsubscribe() {
return this._sub.unsubscribe();
}
get closed() {
return this._sub.closed;
}
}
//# sourceMappingURL=AuthCoordinator.js.map