diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 4c55b2eb48..6b336572df 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -1,15 +1,21 @@ import { CipherType } from 'jslib/enums'; import { + ApiService, + AppIdService, CryptoService, + TokenService, UtilsService, } from 'jslib/services'; import { + ApiService as ApiServiceAbstraction, + AppIdService as AppIdServiceAbstraction, CryptoService as CryptoServiceAbstraction, MessagingService as MessagingServiceAbstraction, PlatformUtilsService as PlatformUtilsServiceAbstraction, StorageService as StorageServiceAbstraction, + TokenService as TokenServiceAbstraction, UtilsService as UtilsServiceAbstraction, } from 'jslib/abstractions'; @@ -23,8 +29,6 @@ import TabsBackground from './tabs.background'; import WebRequestBackground from './webRequest.background'; import WindowsBackground from './windows.background'; -import ApiService from '../services/api.service'; -import AppIdService from '../services/appId.service'; import AutofillService from '../services/autofill.service'; import BrowserMessagingService from '../services/browserMessaging.service'; import BrowserPlatformUtilsService from '../services/browserPlatformUtils.service'; @@ -40,7 +44,6 @@ import LockService from '../services/lock.service'; import PasswordGenerationService from '../services/passwordGeneration.service'; import SettingsService from '../services/settings.service'; import SyncService from '../services/sync.service'; -import TokenService from '../services/token.service'; import TotpService from '../services/totp.service'; import UserService from '../services/user.service'; @@ -52,9 +55,9 @@ export default class MainBackground { utilsService: UtilsServiceAbstraction; constantsService: ConstantsService; cryptoService: CryptoServiceAbstraction; - tokenService: TokenService; - appIdService: AppIdService; - apiService: ApiService; + tokenService: TokenServiceAbstraction; + appIdService: AppIdServiceAbstraction; + apiService: ApiServiceAbstraction; environmentService: EnvironmentService; userService: UserService; settingsService: SettingsService; diff --git a/src/popup/app/services/background.service.ts b/src/popup/app/services/background.service.ts index 42efe1964f..7d0755b490 100644 --- a/src/popup/app/services/background.service.ts +++ b/src/popup/app/services/background.service.ts @@ -1,6 +1,9 @@ +import { ApiService } from 'jslib/abstractions/api.service'; +import { AppIdService } from 'jslib/abstractions/appId.service'; import { CryptoService } from 'jslib/abstractions/crypto.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { StorageService } from 'jslib/abstractions/storage.service'; +import { TokenService } from 'jslib/abstractions/token.service'; import { UtilsService } from 'jslib/abstractions/utils.service'; function getBackgroundService(service: string) { @@ -11,10 +14,10 @@ function getBackgroundService(service: string) { } export const storageService = getBackgroundService('storageService'); -export const tokenService = getBackgroundService('tokenService'); +export const tokenService = getBackgroundService('tokenService'); export const cryptoService = getBackgroundService('cryptoService'); export const userService = getBackgroundService('userService'); -export const apiService = getBackgroundService('apiService'); +export const apiService = getBackgroundService('apiService'); export const folderService = getBackgroundService('folderService'); export const cipherService = getBackgroundService('cipherService'); export const syncService = getBackgroundService('syncService'); @@ -22,7 +25,7 @@ export const autofillService = getBackgroundService('autofillService'); export const passwordGenerationService = getBackgroundService('passwordGenerationService'); export const platformUtilsService = getBackgroundService('platformUtilsService'); export const utilsService = getBackgroundService('utilsService'); -export const appIdService = getBackgroundService('appIdService'); +export const appIdService = getBackgroundService('appIdService'); export const i18nService = getBackgroundService('i18nService'); export const constantsService = getBackgroundService('constantsService'); export const settingsService = getBackgroundService('settingsService'); diff --git a/src/services/api.service.ts b/src/services/api.service.ts deleted file mode 100644 index dd86d4129c..0000000000 --- a/src/services/api.service.ts +++ /dev/null @@ -1,461 +0,0 @@ -import AppIdService from './appId.service'; -import ConstantsService from './constants.service'; -import TokenService from './token.service'; - -import { PlatformUtilsService } from 'jslib/abstractions'; - -import { EnvironmentUrls } from 'jslib/models/domain'; - -import { - CipherRequest, - DeviceRequest, - DeviceTokenRequest, - FolderRequest, - PasswordHintRequest, - RegisterRequest, - TokenRequest, - TwoFactorEmailRequest, -} from 'jslib/models/request'; - -import { - AttachmentResponse, - CipherResponse, - DeviceResponse, - DomainsResponse, - ErrorResponse, - FolderResponse, - GlobalDomainResponse, - IdentityTokenResponse, - KeysResponse, - ListResponse, - ProfileOrganizationResponse, - ProfileResponse, - SyncResponse, -} from 'jslib/models/response'; - -export default class ApiService { - urlsSet: boolean = false; - baseUrl: string; - identityBaseUrl: string; - deviceType: string; - logoutCallback: Function; - - constructor(private tokenService: TokenService, platformUtilsService: PlatformUtilsService, - logoutCallback: Function) { - this.logoutCallback = logoutCallback; - this.deviceType = platformUtilsService.getDevice().toString(); - } - - setUrls(urls: EnvironmentUrls) { - this.urlsSet = true; - - if (urls.base != null) { - this.baseUrl = urls.base + '/api'; - this.identityBaseUrl = urls.base + '/identity'; - return; - } - - if (urls.api != null && urls.identity != null) { - this.baseUrl = urls.api; - this.identityBaseUrl = urls.identity; - return; - } - - /* tslint:disable */ - // Desktop - //this.baseUrl = 'http://localhost:4000'; - //this.identityBaseUrl = 'http://localhost:33656'; - - // Desktop HTTPS - //this.baseUrl = 'https://localhost:44377'; - //this.identityBaseUrl = 'https://localhost:44392'; - - // Desktop external - //this.baseUrl = 'http://192.168.1.3:4000'; - //this.identityBaseUrl = 'http://192.168.1.3:33656'; - - // Preview - //this.baseUrl = 'https://preview-api.bitwarden.com'; - //this.identityBaseUrl = 'https://preview-identity.bitwarden.com'; - - // Production - this.baseUrl = 'https://api.bitwarden.com'; - this.identityBaseUrl = 'https://identity.bitwarden.com'; - /* tslint:enable */ - } - - // Auth APIs - - async postIdentityToken(request: TokenRequest): Promise { - const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { - body: this.qsStringify(request.toIdentityToken()), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - let responseJson: any = null; - const typeHeader = response.headers.get('content-type'); - if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { - responseJson = await response.json(); - } - - if (responseJson != null) { - if (response.status === 200) { - return new IdentityTokenResponse(responseJson); - } else if (response.status === 400 && responseJson.TwoFactorProviders2 && - Object.keys(responseJson.TwoFactorProviders2).length) { - await this.tokenService.clearTwoFactorToken(request.email); - return responseJson.TwoFactorProviders2; - } - } - - return Promise.reject(new ErrorResponse(responseJson, response.status, true)); - } - - async refreshIdentityToken(): Promise { - try { - await this.doRefreshToken(); - } catch (e) { - return Promise.reject(null); - } - } - - // Two Factor APIs - - async postTwoFactorEmail(request: TwoFactorEmailRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/two-factor/send-email-login', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Account APIs - - async getAccountRevisionDate(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/accounts/revision-date', { - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - })); - - if (response.status === 200) { - return (await response.json() as number); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async postPasswordHint(request: PasswordHintRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/accounts/password-hint', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async postRegister(request: RegisterRequest): Promise { - const response = await fetch(new Request(this.baseUrl + '/accounts/register', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Folder APIs - - async postFolder(request: FolderRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new FolderResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async putFolder(id: string, request: FolderRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new FolderResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async deleteFolder(id: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/folders/' + id, { - cache: 'no-cache', - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Cipher APIs - - async postCipher(request: CipherRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers', { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async putCipher(id: string, request: CipherRequest): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { - body: JSON.stringify(request), - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Content-Type': 'application/json; charset=utf-8', - 'Device-Type': this.deviceType, - }), - method: 'PUT', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async deleteCipher(id: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id, { - cache: 'no-cache', - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Attachments APIs - - async postCipherAttachment(id: string, data: FormData): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment', { - body: data, - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new CipherResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - async deleteCipherAttachment(id: string, attachmentId: string): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/ciphers/' + id + '/attachment/' + attachmentId, { - cache: 'no-cache', - headers: new Headers({ - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - method: 'DELETE', - })); - - if (response.status !== 200) { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Sync APIs - - async getSync(): Promise { - const authHeader = await this.handleTokenState(); - const response = await fetch(new Request(this.baseUrl + '/sync', { - cache: 'no-cache', - headers: new Headers({ - 'Accept': 'application/json', - 'Authorization': authHeader, - 'Device-Type': this.deviceType, - }), - })); - - if (response.status === 200) { - const responseJson = await response.json(); - return new SyncResponse(responseJson); - } else { - const error = await this.handleError(response, false); - return Promise.reject(error); - } - } - - // Helpers - - private async handleError(response: Response, tokenError: boolean): Promise { - if ((tokenError && response.status === 400) || response.status === 401 || response.status === 403) { - this.logoutCallback(true); - return null; - } - - let responseJson: any = null; - const typeHeader = response.headers.get('content-type'); - if (typeHeader != null && typeHeader.indexOf('application/json') > -1) { - responseJson = await response.json(); - } - - return new ErrorResponse(responseJson, response.status, tokenError); - } - - private async handleTokenState(): Promise { - let accessToken: string; - if (this.tokenService.tokenNeedsRefresh()) { - const tokenResponse = await this.doRefreshToken(); - accessToken = tokenResponse.accessToken; - } else { - accessToken = await this.tokenService.getToken(); - } - - return 'Bearer ' + accessToken; - } - - private async doRefreshToken(): Promise { - const refreshToken = await this.tokenService.getRefreshToken(); - if (refreshToken == null || refreshToken === '') { - throw new Error(); - } - - const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { - body: this.qsStringify({ - grant_type: 'refresh_token', - client_id: 'browser', - refresh_token: refreshToken, - }), - cache: 'no-cache', - headers: new Headers({ - 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', - 'Accept': 'application/json', - 'Device-Type': this.deviceType, - }), - method: 'POST', - })); - - if (response.status === 200) { - const responseJson = await response.json(); - const tokenResponse = new IdentityTokenResponse(responseJson); - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); - return tokenResponse; - } else { - const error = await this.handleError(response, true); - return Promise.reject(error); - } - } - - private qsStringify(params: any): string { - return Object.keys(params).map((key) => { - return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); - }).join('&'); - } -} diff --git a/src/services/appId.service.ts b/src/services/appId.service.ts deleted file mode 100644 index 136eed0f82..0000000000 --- a/src/services/appId.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { UtilsService } from 'jslib/services'; - -import { StorageService } from 'jslib/abstractions'; - -export default class AppIdService { - constructor(private storageService: StorageService) { - } - - getAppId(): Promise { - return this.makeAndGetAppId('appId'); - } - - getAnonymousAppId(): Promise { - return this.makeAndGetAppId('anonymousAppId'); - } - - private async makeAndGetAppId(key: string) { - const existingId = await this.storageService.get(key); - if (existingId != null) { - return existingId; - } - - const guid = UtilsService.newGuid(); - await this.storageService.save(key, guid); - return guid; - } -} diff --git a/src/services/autofill.service.ts b/src/services/autofill.service.ts index e0191d63f4..38b1cce136 100644 --- a/src/services/autofill.service.ts +++ b/src/services/autofill.service.ts @@ -8,13 +8,13 @@ import AutofillPageDetails from '../models/domain/autofillPageDetails'; import AutofillScript from '../models/domain/autofillScript'; import CipherService from './cipher.service'; -import TokenService from './token.service'; import TotpService from './totp.service'; import { UtilsService } from 'jslib/services'; import { PlatformUtilsService, + TokenService, UtilsService as UtilsServiceAbstraction, } from 'jslib/abstractions'; diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 4d943c0732..fedf79f890 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -17,11 +17,11 @@ import { } from 'jslib/models/response'; import { + ApiService, CryptoService, StorageService, } from 'jslib/abstractions'; -import ApiService from './api.service'; import ConstantsService from './constants.service'; import SettingsService from './settings.service'; import UserService from './user.service'; diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index 1d0da1a83d..faae63995c 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -1,9 +1,11 @@ -import ApiService from './api.service'; import ConstantsService from './constants.service'; import { EnvironmentUrls } from 'jslib/models/domain'; -import { StorageService } from 'jslib/abstractions'; +import { + ApiService, + StorageService, +} from 'jslib/abstractions'; export default class EnvironmentService { baseUrl: string; diff --git a/src/services/folder.service.ts b/src/services/folder.service.ts index 5a0a9995fe..b2a843aa04 100644 --- a/src/services/folder.service.ts +++ b/src/services/folder.service.ts @@ -1,4 +1,3 @@ -import ApiService from './api.service'; import UserService from './user.service'; import { FolderData } from 'jslib/models/data'; @@ -10,6 +9,7 @@ import { FolderRequest } from 'jslib/models/request'; import { FolderResponse } from 'jslib/models/response'; import { + ApiService, CryptoService, StorageService, } from 'jslib/abstractions'; diff --git a/src/services/sync.service.ts b/src/services/sync.service.ts index 1094c1b363..943d19021b 100644 --- a/src/services/sync.service.ts +++ b/src/services/sync.service.ts @@ -1,4 +1,3 @@ -import ApiService from './api.service'; import CipherService from './cipher.service'; import CollectionService from './collection.service'; import FolderService from './folder.service'; @@ -6,6 +5,7 @@ import SettingsService from './settings.service'; import UserService from './user.service'; import { + ApiService, CryptoService, MessagingService, StorageService, diff --git a/src/services/token.service.ts b/src/services/token.service.ts deleted file mode 100644 index 0fa40ab1ae..0000000000 --- a/src/services/token.service.ts +++ /dev/null @@ -1,176 +0,0 @@ -import ConstantsService from './constants.service'; - -import { UtilsService } from 'jslib/services/utils.service'; - -import { StorageService } from 'jslib/abstractions'; - -const Keys = { - accessToken: 'accessToken', - refreshToken: 'refreshToken', - twoFactorTokenPrefix: 'twoFactorToken_', -}; - -export default class TokenService { - token: string; - decodedToken: any; - refreshToken: string; - - constructor(private storageService: StorageService) { - } - - setTokens(accessToken: string, refreshToken: string): Promise { - return Promise.all([ - this.setToken(accessToken), - this.setRefreshToken(refreshToken), - ]); - } - - setToken(token: string): Promise { - this.token = token; - this.decodedToken = null; - return this.storageService.save(Keys.accessToken, token); - } - - async getToken(): Promise { - if (this.token != null) { - return this.token; - } - - this.token = await this.storageService.get(Keys.accessToken); - return this.token; - } - - setRefreshToken(refreshToken: string): Promise { - this.refreshToken = refreshToken; - return this.storageService.save(Keys.refreshToken, refreshToken); - } - - async getRefreshToken(): Promise { - if (this.refreshToken != null) { - return this.refreshToken; - } - - this.refreshToken = await this.storageService.get(Keys.refreshToken); - return this.refreshToken; - } - - setTwoFactorToken(token: string, email: string): Promise { - return this.storageService.save(Keys.twoFactorTokenPrefix + email, token); - } - - getTwoFactorToken(email: string): Promise { - return this.storageService.get(Keys.twoFactorTokenPrefix + email); - } - - clearTwoFactorToken(email: string): Promise { - return this.storageService.remove(Keys.twoFactorTokenPrefix + email); - } - - clearToken(): Promise { - this.token = null; - this.decodedToken = null; - this.refreshToken = null; - - return Promise.all([ - this.storageService.remove(Keys.accessToken), - this.storageService.remove(Keys.refreshToken), - ]); - } - - // jwthelper methods - // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js - - decodeToken(): any { - if (this.decodedToken) { - return this.decodedToken; - } - - if (this.token == null) { - throw new Error('Token not found.'); - } - - const parts = this.token.split('.'); - if (parts.length !== 3) { - throw new Error('JWT must have 3 parts'); - } - - const decoded = UtilsService.urlBase64Decode(parts[1]); - if (decoded == null) { - throw new Error('Cannot decode the token'); - } - - this.decodedToken = JSON.parse(decoded); - return this.decodedToken; - } - - getTokenExpirationDate(): Date { - const decoded = this.decodeToken(); - if (typeof decoded.exp === 'undefined') { - return null; - } - - const d = new Date(0); // The 0 here is the key, which sets the date to the epoch - d.setUTCSeconds(decoded.exp); - return d; - } - - tokenSecondsRemaining(offsetSeconds: number = 0): number { - const d = this.getTokenExpirationDate(); - if (d == null) { - return 0; - } - - const msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); - return Math.round(msRemaining / 1000); - } - - tokenNeedsRefresh(minutes: number = 5): boolean { - const sRemaining = this.tokenSecondsRemaining(); - return sRemaining < (60 * minutes); - } - - getUserId(): string { - const decoded = this.decodeToken(); - if (typeof decoded.sub === 'undefined') { - throw new Error('No user id found'); - } - - return decoded.sub as string; - } - - getEmail(): string { - const decoded = this.decodeToken(); - if (typeof decoded.email === 'undefined') { - throw new Error('No email found'); - } - - return decoded.email as string; - } - - getName(): string { - const decoded = this.decodeToken(); - if (typeof decoded.name === 'undefined') { - throw new Error('No name found'); - } - - return decoded.name as string; - } - - getPremium(): boolean { - const decoded = this.decodeToken(); - if (typeof decoded.premium === 'undefined') { - return false; - } - - return decoded.premium as boolean; - } - - getIssuer(): string { - const decoded = this.decodeToken(); - if (typeof decoded.iss === 'undefined') { - throw new Error('No issuer found'); - } - - return decoded.iss as string; - } -} diff --git a/src/services/user.service.ts b/src/services/user.service.ts index cd0f3278e1..4610174e48 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -1,6 +1,4 @@ -import TokenService from './token.service'; - -import { StorageService } from 'jslib/abstractions'; +import { StorageService, TokenService } from 'jslib/abstractions'; const Keys = { userId: 'userId',