From 1f0127966e85aa29f9e50144de9b2a03b00de5d4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 13 Aug 2021 09:28:03 -0400 Subject: [PATCH] Generalize token refreshing to include reauth by api key (#456) --- common/src/abstractions/token.service.ts | 6 +- common/src/services/api.service.ts | 32 ++++++++++- common/src/services/auth.service.ts | 4 +- common/src/services/token.service.ts | 73 +++++++++++++++++++----- node/src/services/nodeApi.service.ts | 3 +- 5 files changed, 96 insertions(+), 22 deletions(-) diff --git a/common/src/abstractions/token.service.ts b/common/src/abstractions/token.service.ts index f55bed38d2..fbed6974c2 100644 --- a/common/src/abstractions/token.service.ts +++ b/common/src/abstractions/token.service.ts @@ -2,11 +2,15 @@ export abstract class TokenService { token: string; decodedToken: any; refreshToken: string; - setTokens: (accessToken: string, refreshToken: string) => Promise; + setTokens: (accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]) => Promise; setToken: (token: string) => Promise; getToken: () => Promise; setRefreshToken: (refreshToken: string) => Promise; getRefreshToken: () => Promise; + setClientId: (clientId: string) => Promise; + getClientId: () => Promise; + setClientSecret: (clientSecret: string) => Promise; + getClientSecret: () => Promise; toggleTokens: () => Promise; setTwoFactorToken: (token: string, email: string) => Promise; getTwoFactorToken: (email: string) => Promise; diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index f158f1c8ff..285f3b8b74 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -165,6 +165,7 @@ import { IdentityCaptchaResponse } from '../models/response/identityCaptchaRespo import { SendAccessView } from '../models/view/sendAccessView'; export class ApiService implements ApiServiceAbstraction { + protected apiKeyRefresh: (clientId: string, clientSecret: string) => Promise; private device: DeviceType; private deviceType: string; private isWebClient = false; @@ -226,7 +227,7 @@ export class ApiService implements ApiServiceAbstraction { async refreshIdentityToken(): Promise { try { - await this.doRefreshToken(); + await this.doAuthRefresh(); } catch (e) { return Promise.reject(null); } @@ -1415,7 +1416,7 @@ export class ApiService implements ApiServiceAbstraction { async getActiveBearerToken(): Promise { let accessToken = await this.tokenService.getToken(); if (this.tokenService.tokenNeedsRefresh()) { - await this.doRefreshToken(); + await this.doAuthRefresh(); accessToken = await this.tokenService.getToken(); } return accessToken; @@ -1461,6 +1462,31 @@ export class ApiService implements ApiServiceAbstraction { } } + protected async doAuthRefresh(): Promise { + const refreshToken = await this.tokenService.getRefreshToken(); + if (refreshToken != null && refreshToken !== '') { + return this.doRefreshToken(); + } + + const clientId = await this.tokenService.getClientId(); + const clientSecret = await this.tokenService.getClientSecret(); + if (!Utils.isNullOrWhitespace(clientId) && !Utils.isNullOrWhitespace(clientSecret)) { + return this.doApiTokenRefresh(); + } + + throw new Error('Cannot refresh token, no refresh token or api keys are stored'); + } + + protected async doApiTokenRefresh(): Promise { + const clientId = await this.tokenService.getClientId(); + const clientSecret = await this.tokenService.getClientSecret(); + if (Utils.isNullOrWhitespace(clientId) || Utils.isNullOrWhitespace(clientSecret) || this.apiKeyRefresh == null) { + throw new Error(); + } + + await this.apiKeyRefresh(clientId, clientSecret); + } + protected async doRefreshToken(): Promise { const refreshToken = await this.tokenService.getRefreshToken(); if (refreshToken == null || refreshToken === '') { @@ -1491,7 +1517,7 @@ export class ApiService implements ApiServiceAbstraction { if (response.status === 200) { const responseJson = await response.json(); const tokenResponse = new IdentityTokenResponse(responseJson); - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken, null); } else { const error = await this.handleError(response, true, true); return Promise.reject(error); diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 06594ffa93..0098db5c66 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -280,7 +280,7 @@ export class AuthService implements AuthServiceAbstraction { let emailPassword: string[] = []; let codeCodeVerifier: string[] = []; - let clientIdClientSecret: string[] = []; + let clientIdClientSecret: [string, string] = [null, null]; if (email != null && hashedPassword != null) { emailPassword = [email, hashedPassword]; @@ -344,7 +344,7 @@ export class AuthService implements AuthServiceAbstraction { await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } - await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken, clientIdClientSecret); await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), tokenResponse.kdf, tokenResponse.kdfIterations); if (this.setCryptoKeys) { diff --git a/common/src/services/token.service.ts b/common/src/services/token.service.ts index be1cf40c15..0c3b9c4015 100644 --- a/common/src/services/token.service.ts +++ b/common/src/services/token.service.ts @@ -9,31 +9,61 @@ const Keys = { accessToken: 'accessToken', refreshToken: 'refreshToken', twoFactorTokenPrefix: 'twoFactorToken_', + clientId: 'apikey_clientId', + clientSecret: 'apikey_clientSecret', }; export class TokenService implements TokenServiceAbstraction { token: string; decodedToken: any; refreshToken: string; + clientId: string; + clientSecret: string; constructor(private storageService: StorageService) { } - async setTokens(accessToken: string, refreshToken: string): Promise { + async setTokens(accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]): Promise { await this.setToken(accessToken); await this.setRefreshToken(refreshToken); + if (clientIdClientSecret != null) { + await this.setClientId(clientIdClientSecret[0]); + await this.setClientSecret(clientIdClientSecret[1]); + } + } + + async setClientId(clientId: string): Promise { + this.clientId = clientId; + return this.storeTokenValue(Keys.clientId, clientId); + } + + async getClientId(): Promise { + if (this.clientId != null) { + return this.clientId; + } + + this.clientId = await this.storageService.get(Keys.clientId); + return this.clientId; + } + + async setClientSecret(clientSecret: string): Promise { + this.clientSecret = clientSecret; + return this.storeTokenValue(Keys.clientSecret, clientSecret); + } + + async getClientSecret(): Promise { + if (this.clientSecret != null) { + return this.clientSecret; + } + + this.clientSecret = await this.storageService.get(Keys.clientSecret); + return this.clientSecret; } async setToken(token: string): Promise { this.token = token; this.decodedToken = null; - - if (await this.skipTokenStorage()) { - // if we have a vault timeout and the action is log out, don't store token - return; - } - - return this.storageService.save(Keys.accessToken, token); + return this.storeTokenValue(Keys.accessToken, token); } async getToken(): Promise { @@ -47,13 +77,7 @@ export class TokenService implements TokenServiceAbstraction { async setRefreshToken(refreshToken: string): Promise { this.refreshToken = refreshToken; - - if (await this.skipTokenStorage()) { - // if we have a vault timeout and the action is log out, don't store token - return; - } - - return this.storageService.save(Keys.refreshToken, refreshToken); + return this.storeTokenValue(Keys.refreshToken, refreshToken); } async getRefreshToken(): Promise { @@ -68,6 +92,8 @@ export class TokenService implements TokenServiceAbstraction { async toggleTokens(): Promise { const token = await this.getToken(); const refreshToken = await this.getRefreshToken(); + const clientId = await this.getClientId(); + const clientSecret = await this.getClientSecret(); const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); if ((timeout != null || timeout === 0) && action === 'logOut') { @@ -75,11 +101,15 @@ export class TokenService implements TokenServiceAbstraction { await this.clearToken(); this.token = token; this.refreshToken = refreshToken; + this.clientId = clientId; + this.clientSecret = clientSecret; return; } await this.setToken(token); await this.setRefreshToken(refreshToken); + await this.setClientId(clientId); + await this.setClientSecret(clientSecret); } setTwoFactorToken(token: string, email: string): Promise { @@ -98,9 +128,13 @@ export class TokenService implements TokenServiceAbstraction { this.token = null; this.decodedToken = null; this.refreshToken = null; + this.clientId = null; + this.clientSecret = null; await this.storageService.remove(Keys.accessToken); await this.storageService.remove(Keys.refreshToken); + await this.storageService.remove(Keys.clientId); + await this.storageService.remove(Keys.clientSecret); } // jwthelper methods @@ -209,6 +243,15 @@ export class TokenService implements TokenServiceAbstraction { return decoded.iss as string; } + private async storeTokenValue(key: string, value: string) { + if (await this.skipTokenStorage()) { + // if we have a vault timeout and the action is log out, don't store token + return; + } + + return this.storageService.save(key, value); + } + private async skipTokenStorage(): Promise { const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); diff --git a/node/src/services/nodeApi.service.ts b/node/src/services/nodeApi.service.ts index 7b0c283430..3cfc5bb824 100644 --- a/node/src/services/nodeApi.service.ts +++ b/node/src/services/nodeApi.service.ts @@ -17,8 +17,9 @@ import { TokenService } from 'jslib-common/abstractions/token.service'; export class NodeApiService extends ApiService { constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService, logoutCallback: (expired: boolean) => Promise, - customUserAgent: string = null) { + customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise) { super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent); + this.apiKeyRefresh = apiKeyRefresh; } nativeFetch(request: Request): Promise {