1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-27 12:36:14 +01:00

Generalize token refreshing to include reauth by api key (#456)

This commit is contained in:
Matt Gibson 2021-08-13 09:28:03 -04:00 committed by GitHub
parent 0180d0cce5
commit 1f0127966e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 22 deletions

View File

@ -2,11 +2,15 @@ export abstract class TokenService {
token: string; token: string;
decodedToken: any; decodedToken: any;
refreshToken: string; refreshToken: string;
setTokens: (accessToken: string, refreshToken: string) => Promise<any>; setTokens: (accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]) => Promise<any>;
setToken: (token: string) => Promise<any>; setToken: (token: string) => Promise<any>;
getToken: () => Promise<string>; getToken: () => Promise<string>;
setRefreshToken: (refreshToken: string) => Promise<any>; setRefreshToken: (refreshToken: string) => Promise<any>;
getRefreshToken: () => Promise<string>; getRefreshToken: () => Promise<string>;
setClientId: (clientId: string) => Promise<any>;
getClientId: () => Promise<string>;
setClientSecret: (clientSecret: string) => Promise<any>;
getClientSecret: () => Promise<string>;
toggleTokens: () => Promise<any>; toggleTokens: () => Promise<any>;
setTwoFactorToken: (token: string, email: string) => Promise<any>; setTwoFactorToken: (token: string, email: string) => Promise<any>;
getTwoFactorToken: (email: string) => Promise<string>; getTwoFactorToken: (email: string) => Promise<string>;

View File

@ -165,6 +165,7 @@ import { IdentityCaptchaResponse } from '../models/response/identityCaptchaRespo
import { SendAccessView } from '../models/view/sendAccessView'; import { SendAccessView } from '../models/view/sendAccessView';
export class ApiService implements ApiServiceAbstraction { export class ApiService implements ApiServiceAbstraction {
protected apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>;
private device: DeviceType; private device: DeviceType;
private deviceType: string; private deviceType: string;
private isWebClient = false; private isWebClient = false;
@ -226,7 +227,7 @@ export class ApiService implements ApiServiceAbstraction {
async refreshIdentityToken(): Promise<any> { async refreshIdentityToken(): Promise<any> {
try { try {
await this.doRefreshToken(); await this.doAuthRefresh();
} catch (e) { } catch (e) {
return Promise.reject(null); return Promise.reject(null);
} }
@ -1415,7 +1416,7 @@ export class ApiService implements ApiServiceAbstraction {
async getActiveBearerToken(): Promise<string> { async getActiveBearerToken(): Promise<string> {
let accessToken = await this.tokenService.getToken(); let accessToken = await this.tokenService.getToken();
if (this.tokenService.tokenNeedsRefresh()) { if (this.tokenService.tokenNeedsRefresh()) {
await this.doRefreshToken(); await this.doAuthRefresh();
accessToken = await this.tokenService.getToken(); accessToken = await this.tokenService.getToken();
} }
return accessToken; return accessToken;
@ -1461,6 +1462,31 @@ export class ApiService implements ApiServiceAbstraction {
} }
} }
protected async doAuthRefresh(): Promise<void> {
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<void> {
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<void> { protected async doRefreshToken(): Promise<void> {
const refreshToken = await this.tokenService.getRefreshToken(); const refreshToken = await this.tokenService.getRefreshToken();
if (refreshToken == null || refreshToken === '') { if (refreshToken == null || refreshToken === '') {
@ -1491,7 +1517,7 @@ export class ApiService implements ApiServiceAbstraction {
if (response.status === 200) { if (response.status === 200) {
const responseJson = await response.json(); const responseJson = await response.json();
const tokenResponse = new IdentityTokenResponse(responseJson); const tokenResponse = new IdentityTokenResponse(responseJson);
await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken, null);
} else { } else {
const error = await this.handleError(response, true, true); const error = await this.handleError(response, true, true);
return Promise.reject(error); return Promise.reject(error);

View File

@ -280,7 +280,7 @@ export class AuthService implements AuthServiceAbstraction {
let emailPassword: string[] = []; let emailPassword: string[] = [];
let codeCodeVerifier: string[] = []; let codeCodeVerifier: string[] = [];
let clientIdClientSecret: string[] = []; let clientIdClientSecret: [string, string] = [null, null];
if (email != null && hashedPassword != null) { if (email != null && hashedPassword != null) {
emailPassword = [email, hashedPassword]; emailPassword = [email, hashedPassword];
@ -344,7 +344,7 @@ export class AuthService implements AuthServiceAbstraction {
await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); 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(), await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(),
tokenResponse.kdf, tokenResponse.kdfIterations); tokenResponse.kdf, tokenResponse.kdfIterations);
if (this.setCryptoKeys) { if (this.setCryptoKeys) {

View File

@ -9,31 +9,61 @@ const Keys = {
accessToken: 'accessToken', accessToken: 'accessToken',
refreshToken: 'refreshToken', refreshToken: 'refreshToken',
twoFactorTokenPrefix: 'twoFactorToken_', twoFactorTokenPrefix: 'twoFactorToken_',
clientId: 'apikey_clientId',
clientSecret: 'apikey_clientSecret',
}; };
export class TokenService implements TokenServiceAbstraction { export class TokenService implements TokenServiceAbstraction {
token: string; token: string;
decodedToken: any; decodedToken: any;
refreshToken: string; refreshToken: string;
clientId: string;
clientSecret: string;
constructor(private storageService: StorageService) { constructor(private storageService: StorageService) {
} }
async setTokens(accessToken: string, refreshToken: string): Promise<any> { async setTokens(accessToken: string, refreshToken: string, clientIdClientSecret: [string, string]): Promise<any> {
await this.setToken(accessToken); await this.setToken(accessToken);
await this.setRefreshToken(refreshToken); await this.setRefreshToken(refreshToken);
if (clientIdClientSecret != null) {
await this.setClientId(clientIdClientSecret[0]);
await this.setClientSecret(clientIdClientSecret[1]);
}
}
async setClientId(clientId: string): Promise<any> {
this.clientId = clientId;
return this.storeTokenValue(Keys.clientId, clientId);
}
async getClientId(): Promise<string> {
if (this.clientId != null) {
return this.clientId;
}
this.clientId = await this.storageService.get<string>(Keys.clientId);
return this.clientId;
}
async setClientSecret(clientSecret: string): Promise<any> {
this.clientSecret = clientSecret;
return this.storeTokenValue(Keys.clientSecret, clientSecret);
}
async getClientSecret(): Promise<string> {
if (this.clientSecret != null) {
return this.clientSecret;
}
this.clientSecret = await this.storageService.get<string>(Keys.clientSecret);
return this.clientSecret;
} }
async setToken(token: string): Promise<any> { async setToken(token: string): Promise<any> {
this.token = token; this.token = token;
this.decodedToken = null; this.decodedToken = null;
return this.storeTokenValue(Keys.accessToken, token);
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);
} }
async getToken(): Promise<string> { async getToken(): Promise<string> {
@ -47,13 +77,7 @@ export class TokenService implements TokenServiceAbstraction {
async setRefreshToken(refreshToken: string): Promise<any> { async setRefreshToken(refreshToken: string): Promise<any> {
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
return this.storeTokenValue(Keys.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);
} }
async getRefreshToken(): Promise<string> { async getRefreshToken(): Promise<string> {
@ -68,6 +92,8 @@ export class TokenService implements TokenServiceAbstraction {
async toggleTokens(): Promise<any> { async toggleTokens(): Promise<any> {
const token = await this.getToken(); const token = await this.getToken();
const refreshToken = await this.getRefreshToken(); const refreshToken = await this.getRefreshToken();
const clientId = await this.getClientId();
const clientSecret = await this.getClientSecret();
const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey); const timeout = await this.storageService.get(ConstantsService.vaultTimeoutKey);
const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey); const action = await this.storageService.get(ConstantsService.vaultTimeoutActionKey);
if ((timeout != null || timeout === 0) && action === 'logOut') { if ((timeout != null || timeout === 0) && action === 'logOut') {
@ -75,11 +101,15 @@ export class TokenService implements TokenServiceAbstraction {
await this.clearToken(); await this.clearToken();
this.token = token; this.token = token;
this.refreshToken = refreshToken; this.refreshToken = refreshToken;
this.clientId = clientId;
this.clientSecret = clientSecret;
return; return;
} }
await this.setToken(token); await this.setToken(token);
await this.setRefreshToken(refreshToken); await this.setRefreshToken(refreshToken);
await this.setClientId(clientId);
await this.setClientSecret(clientSecret);
} }
setTwoFactorToken(token: string, email: string): Promise<any> { setTwoFactorToken(token: string, email: string): Promise<any> {
@ -98,9 +128,13 @@ export class TokenService implements TokenServiceAbstraction {
this.token = null; this.token = null;
this.decodedToken = null; this.decodedToken = null;
this.refreshToken = null; this.refreshToken = null;
this.clientId = null;
this.clientSecret = null;
await this.storageService.remove(Keys.accessToken); await this.storageService.remove(Keys.accessToken);
await this.storageService.remove(Keys.refreshToken); await this.storageService.remove(Keys.refreshToken);
await this.storageService.remove(Keys.clientId);
await this.storageService.remove(Keys.clientSecret);
} }
// jwthelper methods // jwthelper methods
@ -209,6 +243,15 @@ export class TokenService implements TokenServiceAbstraction {
return decoded.iss as string; 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<boolean> { private async skipTokenStorage(): Promise<boolean> {
const timeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey); const timeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
const action = await this.storageService.get<string>(ConstantsService.vaultTimeoutActionKey); const action = await this.storageService.get<string>(ConstantsService.vaultTimeoutActionKey);

View File

@ -17,8 +17,9 @@ import { TokenService } from 'jslib-common/abstractions/token.service';
export class NodeApiService extends ApiService { export class NodeApiService extends ApiService {
constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService, constructor(tokenService: TokenService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, logoutCallback: (expired: boolean) => Promise<void>, environmentService: EnvironmentService, logoutCallback: (expired: boolean) => Promise<void>,
customUserAgent: string = null) { customUserAgent: string = null, apiKeyRefresh: (clientId: string, clientSecret: string) => Promise<any>) {
super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent); super(tokenService, platformUtilsService, environmentService, logoutCallback, customUserAgent);
this.apiKeyRefresh = apiKeyRefresh;
} }
nativeFetch(request: Request): Promise<Response> { nativeFetch(request: Request): Promise<Response> {