UNPKG

@bitblit/ratchet-warden-common

Version:

Typescript library to simplify using simplewebauthn and secondary auth methods over GraphQL

343 lines 14 kB
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