@bitblit/ratchet-warden-common
Version:
Typescript library to simplify using simplewebauthn and secondary auth methods over GraphQL
343 lines • 14 kB
JavaScript
import { Logger } from "@bitblit/ratchet-common/logger/logger";
import { StringRatchet } from "@bitblit/ratchet-common/lang/string-ratchet";
import { JwtDecodeOnlyRatchet } from "@bitblit/ratchet-common/jwt/jwt-decode-only-ratchet";
import { timer } from "rxjs";
import { startAuthentication, startRegistration } from "@simplewebauthn/browser";
import { WardenUtils } from "../common/util/warden-utils.js";
import { WardenLoginRequestType } from "../common/model/warden-login-request-type";
export class WardenUserService {
options;
loggedInTimerSubscription;
_autoRefreshEnabled = false;
constructor(options) {
this.options = options;
Logger.info('Initializing user service');
const stored = this.options.loggedInUserProvider.fetchLoggedInUserWrapper();
if (WardenUtils.wrapperIsExpired(stored)) {
Logger.info('Stored token is expired, removing it');
this.options.loggedInUserProvider.logOutUser();
}
else {
this.options.eventProcessor.onSuccessfulLogin(stored);
}
const timerSeconds = this.options.loginCheckTimerPingSeconds || 2.5;
this.loggedInTimerSubscription = timer(0, timerSeconds * 1000).subscribe((t) => this.checkForAutoLogoutOrRefresh(t));
}
cleanShutDown() {
if (this.loggedInTimerSubscription) {
this.loggedInTimerSubscription.unsubscribe();
}
}
get serviceOptions() {
return this.options;
}
async createAccount(contact, sendCode, label, tags) {
const rval = await this.options.wardenClient.createAccount(contact, sendCode, label, tags);
if (this.options.recentLoginProvider && StringRatchet.trimToNull(rval)) {
this.options.recentLoginProvider.saveNewUser(rval, label, contact);
}
return rval;
}
async addContactToLoggedInUser(contact) {
return this.options.wardenClient.addContactToLoggedInUser(contact);
}
get autoRefreshEnabled() {
return this._autoRefreshEnabled;
}
set autoRefreshEnabled(newValue) {
if (newValue) {
if (this.options.allowAutoRefresh) {
this._autoRefreshEnabled = true;
}
else {
throw new Error('Cannot enable auto-refresh - this is disabled in the user service options');
}
}
else {
this._autoRefreshEnabled = false;
}
}
async checkForAutoLogoutOrRefresh(t) {
Logger.debug('Checking for auto-logout or refresh : %s', t);
const current = this.fetchLoggedInUserWrapper();
if (current) {
const thresholdSeconds = this.options.autoLoginHandlingThresholdSeconds || 10;
const secondsLeft = current.expirationEpochSeconds - Math.floor(Date.now() / 1000);
if (secondsLeft < thresholdSeconds) {
if (this.autoRefreshEnabled) {
Logger.info('Under threshold, initiating auto-refresh');
const result = await this.refreshToken();
this.options.eventProcessor.onAutomaticTokenRefresh(result);
}
else {
Logger.info('Under threshold, initiating auto-logout');
this.logout();
}
}
}
}
logout() {
this.options.loggedInUserProvider.logOutUser();
this.options.eventProcessor.onLogout();
}
fetchLoggedInUserId() {
const tmp = this.options.loggedInUserProvider.fetchLoggedInUserWrapper();
const rval = tmp?.userObject?.wardenData?.userId;
return rval;
}
fetchLoggedInUserWrapper() {
let tmp = this.options.loggedInUserProvider.fetchLoggedInUserWrapper();
if (tmp) {
if (WardenUtils.wrapperIsExpired(tmp)) {
Logger.info('Token is expired - auto logout triggered');
this.logout();
tmp = null;
}
}
return tmp;
}
loggedInUserHasGlobalRole(roleId) {
let rval = false;
const token = this.fetchLoggedInUserJwtObject();
rval = token ? WardenUtils.userHasGlobalRole(WardenUtils.wardenUserDecorationFromToken(token), roleId) : false;
return rval;
}
loggedInUserHasRoleOnTeam(teamId, roleId) {
let rval = false;
const token = this.fetchLoggedInUserJwtObject();
rval = token ? WardenUtils.userHasRoleOnTeam(WardenUtils.wardenUserDecorationFromToken(token), teamId, roleId) : false;
return rval;
}
isLoggedIn() {
const t = this.fetchLoggedInUserWrapper();
return !!t;
}
fetchLoggedInUserJwtObject() {
const t = this.fetchLoggedInUserWrapper();
return t ? t.userObject : null;
}
fetchLoggedInUserJwtToken() {
const t = this.fetchLoggedInUserWrapper();
return t ? t.jwtToken : null;
}
fetchLoggedInUserObject() {
const t = this.fetchLoggedInUserJwtObject();
return t?.user;
}
fetchLoggedInProxyObject() {
const t = this.fetchLoggedInUserJwtObject();
return t?.proxy;
}
fetchLoggedInGlobalRoleIds() {
const t = this.fetchLoggedInUserJwtObject();
return t?.globalRoleIds;
}
fetchLoggedInTeamRoleMappingsGlobalRoleIds() {
const t = this.fetchLoggedInUserJwtObject();
return t?.teamRoleMappings;
}
fetchLoggedInUserExpirationEpochSeconds() {
const t = this.fetchLoggedInUserJwtObject();
return t ? t.exp : null;
}
fetchLoggedInUserRemainingSeconds() {
const t = this.fetchLoggedInUserJwtObject();
return t ? t.exp - Math.floor(Date.now() / 1000) : null;
}
async updateLoggedInUserFromTokenString(token) {
let rval = null;
if (!StringRatchet.trimToNull(token)) {
Logger.info('Called updateLoggedInUserFromTokenString with empty string - logging out');
this.logout();
}
else {
Logger.info('updateLoggedInUserFromTokenString : %s', token);
const parsed = JwtDecodeOnlyRatchet.decodeTokenNoVerify(token);
if (parsed) {
rval = {
userObject: parsed,
jwtToken: token,
expirationEpochSeconds: parsed.exp,
};
this.options.loggedInUserProvider.setLoggedInUserWrapper(rval);
this.updateRecentLoginsFromWardenEntrySummary(parsed.wardenData);
this.options.eventProcessor.onSuccessfulLogin(rval);
}
else {
Logger.warn('Failed to parse token %s - ignoring login and triggering failure');
this.options.eventProcessor.onLoginFailure('Could not parse token string');
}
}
return rval;
}
async refreshToken() {
let rval = null;
const currentWrapper = this.fetchLoggedInUserWrapper();
if (!currentWrapper) {
Logger.info('Could not refresh - no token available');
}
else {
const newToken = await this.options.wardenClient.refreshJwtToken(currentWrapper.jwtToken);
rval = await this.updateLoggedInUserFromTokenString(newToken);
}
return rval;
}
async sendExpiringCode(contact) {
return this.options.wardenClient.sendExpiringValidationToken(contact);
}
async processWardenLoginResults(resp) {
let rval = null;
if (resp) {
Logger.info('Warden: response : %j ', resp);
if (resp.jwtToken) {
Logger.info('Applying login');
rval = await this.updateLoggedInUserFromTokenString(resp.jwtToken);
}
else if (resp.error) {
this.options.eventProcessor.onLoginFailure(resp.error);
}
else {
Logger.error('Response contained neither token nor error');
this.options.eventProcessor.onLoginFailure('Response contained neither token nor error');
}
}
else {
Logger.error('Login call failed');
this.options.eventProcessor.onLoginFailure('Login call returned null');
}
return rval;
}
updateRecentLoginsFromWardenEntrySummary(res) {
if (this.options.recentLoginProvider && res) {
Logger.info('UserService : Saving recent login %j', res);
this.options.recentLoginProvider.saveRecentLogin(res);
}
else {
Logger.info('Not saving recent login - no storage configured or no data passed');
}
}
updateRecentLoginsFromLoggedInUserWrapper(res) {
this.updateRecentLoginsFromWardenEntrySummary(res?.userObject?.wardenData);
}
async executeWebAuthnBasedLogin(userId) {
const resp = await this.executeWebAuthnLoginToWardenLoginResults(userId);
const rval = await this.processWardenLoginResults(resp);
this.updateRecentLoginsFromLoggedInUserWrapper(rval);
return rval;
}
async removeWebAuthnRegistrationFromLoggedInUser(input) {
const rval = await this.options.wardenClient.removeWebAuthnRegistrationFromLoggedInUser(input);
return rval;
}
async removeContactFromLoggedInUser(input) {
const rval = await this.options.wardenClient.removeContactFromLoggedInUser(input);
return rval;
}
async executeValidationTokenBasedLogin(contact, token, createUserIfMissing) {
Logger.info('Warden: executeValidationTokenBasedLogin : %j : %s : %s', contact, token, createUserIfMissing);
const resp = await this.options.wardenClient.performLoginCmd({
type: WardenLoginRequestType.ExpiringToken,
contact: contact,
expiringToken: token,
createUserIfMissing: createUserIfMissing,
});
const rval = await this.processWardenLoginResults(resp);
this.updateRecentLoginsFromLoggedInUserWrapper(rval);
return rval;
}
async executeThirdPartyTokenBasedLogin(thirdParty, token, createUserIfMissing) {
Logger.info('Warden: executeThirdPartyTokenBasedLogin : %j : %s : %s', thirdParty, token, createUserIfMissing);
const resp = await this.options.wardenClient.performLoginCmd({
type: WardenLoginRequestType.ThirdParty,
thirdPartyToken: {
thirdParty: thirdParty,
token: token,
},
createUserIfMissing: createUserIfMissing,
});
const rval = await this.processWardenLoginResults(resp);
this.updateRecentLoginsFromLoggedInUserWrapper(rval);
return rval;
}
async saveCurrentDeviceAsWebAuthnForCurrentUser() {
const input = await this.options.wardenClient.generateWebAuthnRegistrationChallengeForLoggedInUser();
const creds = await startRegistration(input);
const deviceLabel = StringRatchet.trimToEmpty(this.options?.deviceLabelGenerator ? this.options.deviceLabelGenerator() : this.defaultDeviceLabelGenerator());
const output = await this.options.wardenClient.addWebAuthnRegistrationToLoggedInUser(this.options.applicationName, deviceLabel, creds);
this.updateRecentLoginsFromWardenEntrySummary(output);
return output;
}
async exportWebAuthnRegistrationEntryForLoggedInUser(origin) {
return this.options.wardenClient.exportWebAuthnRegistrationEntryForLoggedInUser(origin);
}
async importWebAuthnRegistrationEntryForLoggedInUser(token) {
return this.options.wardenClient.importWebAuthnRegistrationEntryForLoggedInUser(token);
}
defaultDeviceLabelGenerator() {
let rval = '';
if (navigator) {
if (navigator['userAgentData'] &&
navigator['userAgentData']['brands'] &&
navigator['userAgentData']['brands'][1] &&
navigator['userAgentData']['brands'][1]['brand']) {
rval = navigator['userAgentData']['brands'][1]['brand'];
}
else {
rval = navigator.userAgent;
}
if (navigator.platform) {
rval += ' on ' + navigator.platform;
}
}
else {
rval = 'Unknown device';
}
return rval;
}
async executeWebAuthnLoginToWardenLoginResults(userId) {
let rval = null;
try {
const resp = await this.options.wardenClient.generateWebAuthnAuthenticationChallengeForUserId(userId);
const input = {
optionsJSON: resp,
useBrowserAutofill: false,
verifyBrowserAutofillInput: false,
};
Logger.info('Got login challenge : %j', input);
const creds = await startAuthentication(input);
Logger.info('Got creds: %j', creds);
const loginCmd = {
type: WardenLoginRequestType.WebAuthn,
userId: userId,
webAuthn: creds,
};
rval = await this.options.wardenClient.performLoginCmd(loginCmd);
if (rval?.jwtToken) {
}
}
catch (err) {
Logger.error('WebauthN Failed : %s', err);
}
return rval;
}
async executeThirdPartyLoginToWardenLoginResults(thirdParty, token) {
let rval = null;
try {
const loginCmd = {
type: WardenLoginRequestType.ThirdParty,
thirdPartyToken: {
thirdParty: thirdParty,
token: token,
}
};
rval = await this.options.wardenClient.performLoginCmd(loginCmd);
if (rval?.jwtToken) {
}
}
catch (err) {
Logger.error('Third party Failed : %s', err);
}
return rval;
}
}
//# sourceMappingURL=warden-user-service.js.map