EC-134 Fix api token refresh (#749)

* Fix apikey token refresh

* Refactor: use class for TokenRequestTwoFactor
This commit is contained in:
Thomas Rittson 2022-04-01 11:28:23 +10:00 committed by GitHub
parent 4d58200ee9
commit e595c0548e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 82 additions and 80 deletions

View File

@ -14,6 +14,7 @@ import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe"; import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe";
import { AuthResult } from "jslib-common/models/domain/authResult"; import { AuthResult } from "jslib-common/models/domain/authResult";
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest"; import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
import { TwoFactorProviders } from "jslib-common/services/twoFactor.service"; import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
@ -191,11 +192,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
async doSubmit() { async doSubmit() {
this.formPromise = this.authService.logInTwoFactor( this.formPromise = this.authService.logInTwoFactor(
{ new TokenRequestTwoFactor(this.selectedProviderType, this.token, this.remember),
provider: this.selectedProviderType,
token: this.token,
remember: this.remember,
},
this.captchaToken this.captchaToken
); );
const response: AuthResult = await this.formPromise; const response: AuthResult = await this.formPromise;

View File

@ -211,12 +211,14 @@ import { ValidationService } from "./validation.service";
tokenService: TokenServiceAbstraction, tokenService: TokenServiceAbstraction,
platformUtilsService: PlatformUtilsServiceAbstraction, platformUtilsService: PlatformUtilsServiceAbstraction,
environmentService: EnvironmentServiceAbstraction, environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction messagingService: MessagingServiceAbstraction,
appIdService: AppIdServiceAbstraction
) => ) =>
new ApiService( new ApiService(
tokenService, tokenService,
platformUtilsService, platformUtilsService,
environmentService, environmentService,
appIdService,
async (expired: boolean) => messagingService.send("logout", { expired: expired }) async (expired: boolean) => messagingService.send("logout", { expired: expired })
), ),
deps: [ deps: [
@ -224,6 +226,7 @@ import { ValidationService } from "./validation.service";
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
EnvironmentServiceAbstraction, EnvironmentServiceAbstraction,
MessagingServiceAbstraction, MessagingServiceAbstraction,
AppIdServiceAbstraction,
], ],
}, },
{ {

View File

@ -18,6 +18,7 @@ import { AuthResult } from "jslib-common/models/domain/authResult";
import { EncString } from "jslib-common/models/domain/encString"; import { EncString } from "jslib-common/models/domain/encString";
import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials"; import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
import { PasswordTokenRequest } from "jslib-common/models/request/identityToken/passwordTokenRequest"; import { PasswordTokenRequest } from "jslib-common/models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
import { IdentityCaptchaResponse } from "jslib-common/models/response/identityCaptchaResponse"; import { IdentityCaptchaResponse } from "jslib-common/models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "jslib-common/models/response/identityTwoFactorResponse"; import { IdentityTwoFactorResponse } from "jslib-common/models/response/identityTwoFactorResponse";
@ -236,11 +237,11 @@ describe("LogInStrategy", () => {
it("sends 2FA token provided by user to server (single step)", async () => { it("sends 2FA token provided by user to server (single step)", async () => {
// This occurs if the user enters the 2FA code as an argument in the CLI // This occurs if the user enters the 2FA code as an argument in the CLI
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory()); apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
credentials.twoFactor = { credentials.twoFactor = new TokenRequestTwoFactor(
provider: twoFactorProviderType, twoFactorProviderType,
token: twoFactorToken, twoFactorToken,
remember: twoFactorRemember, twoFactorRemember
}; );
await passwordLogInStrategy.logIn(credentials); await passwordLogInStrategy.logIn(credentials);
@ -268,11 +269,7 @@ describe("LogInStrategy", () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory()); apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await passwordLogInStrategy.logInTwoFactor( await passwordLogInStrategy.logInTwoFactor(
{ new TokenRequestTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember),
provider: twoFactorProviderType,
token: twoFactorToken,
remember: twoFactorRemember,
},
null null
); );

View File

@ -5,7 +5,7 @@ import {
SsoLogInCredentials, SsoLogInCredentials,
} from "../models/domain/logInCredentials"; } from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest"; import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
export abstract class AuthService { export abstract class AuthService {
masterPasswordHash: string; masterPasswordHash: string;

View File

@ -19,7 +19,7 @@ import { DeviceRequest } from "../../models/request/deviceRequest";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest"; import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest"; import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest"; import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequest"; import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { KeysRequest } from "../../models/request/keysRequest"; import { KeysRequest } from "../../models/request/keysRequest";
import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse"; import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse"; import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
@ -86,18 +86,10 @@ export abstract class LogInStrategy {
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(); const storedTwoFactorToken = await this.tokenService.getTwoFactorToken();
if (storedTwoFactorToken != null) { if (storedTwoFactorToken != null) {
return { return new TokenRequestTwoFactor(TwoFactorProviderType.Remember, storedTwoFactorToken, false);
token: storedTwoFactorToken,
provider: TwoFactorProviderType.Remember,
remember: false,
};
} }
return { return new TokenRequestTwoFactor();
token: null,
provider: null,
remember: false,
};
} }
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) { protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {

View File

@ -13,7 +13,7 @@ import { AuthResult } from "../../models/domain/authResult";
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials"; import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey"; import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest"; import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequest"; import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { LogInStrategy } from "./logIn.strategy"; import { LogInStrategy } from "./logIn.strategy";

View File

@ -1,5 +1,5 @@
import { AuthenticationType } from "../../enums/authenticationType"; import { AuthenticationType } from "../../enums/authenticationType";
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequest"; import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
export class PasswordLogInCredentials { export class PasswordLogInCredentials {
readonly type = AuthenticationType.Password; readonly type = AuthenticationType.Password;

View File

@ -1,6 +1,7 @@
import { DeviceRequest } from "../deviceRequest"; import { DeviceRequest } from "../deviceRequest";
import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest"; import { TokenRequest } from "./tokenRequest";
import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
export class ApiTokenRequest extends TokenRequest { export class ApiTokenRequest extends TokenRequest {
constructor( constructor(

View File

@ -3,7 +3,8 @@ import { Utils } from "../../../misc/utils";
import { CaptchaProtectedRequest } from "../captchaProtectedRequest"; import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
import { DeviceRequest } from "../deviceRequest"; import { DeviceRequest } from "../deviceRequest";
import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest"; import { TokenRequest } from "./tokenRequest";
import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
export class PasswordTokenRequest extends TokenRequest implements CaptchaProtectedRequest { export class PasswordTokenRequest extends TokenRequest implements CaptchaProtectedRequest {
constructor( constructor(

View File

@ -1,6 +1,7 @@
import { DeviceRequest } from "../deviceRequest"; import { DeviceRequest } from "../deviceRequest";
import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest"; import { TokenRequest } from "./tokenRequest";
import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
export class SsoTokenRequest extends TokenRequest { export class SsoTokenRequest extends TokenRequest {
constructor( constructor(

View File

@ -1,11 +1,6 @@
import { TwoFactorProviderType } from "../../../enums/twoFactorProviderType";
import { DeviceRequest } from "../deviceRequest"; import { DeviceRequest } from "../deviceRequest";
export interface TokenRequestTwoFactor { import { TokenRequestTwoFactor } from "./tokenRequestTwoFactor";
provider: TwoFactorProviderType;
token: string;
remember: boolean;
}
export abstract class TokenRequest { export abstract class TokenRequest {
protected device?: DeviceRequest; protected device?: DeviceRequest;

View File

@ -0,0 +1,9 @@
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
export class TokenRequestTwoFactor {
constructor(
public provider: TwoFactorProviderType = null,
public token: string = null,
public remember: boolean = false
) {}
}

View File

@ -1,3 +1,7 @@
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { DeviceRequest } from "jslib-common/models/request/deviceRequest";
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service"; import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
import { EnvironmentService } from "../abstractions/environment.service"; import { EnvironmentService } from "../abstractions/environment.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service"; import { PlatformUtilsService } from "../abstractions/platformUtils.service";
@ -174,7 +178,6 @@ import { UserKeyResponse } from "../models/response/userKeyResponse";
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;
@ -184,6 +187,7 @@ export class ApiService implements ApiServiceAbstraction {
private tokenService: TokenService, private tokenService: TokenService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private appIdService: AppIdService,
private logoutCallback: (expired: boolean) => Promise<void>, private logoutCallback: (expired: boolean) => Promise<void>,
private customUserAgent: string = null private customUserAgent: string = null
) { ) {
@ -2332,20 +2336,6 @@ export class ApiService implements ApiServiceAbstraction {
throw new Error("Cannot refresh token, no refresh token or api keys are stored"); 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 === "") {
@ -2389,6 +2379,28 @@ export class ApiService implements ApiServiceAbstraction {
} }
} }
protected async doApiTokenRefresh(): Promise<void> {
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
const appId = await this.appIdService.getAppId();
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
const tokenRequest = new ApiTokenRequest(
clientId,
clientSecret,
new TokenRequestTwoFactor(),
deviceRequest
);
const response = await this.postIdentityToken(tokenRequest);
if (!(response instanceof IdentityTokenResponse)) {
throw new Error("Invalid response received when refreshing api token");
}
await this.tokenService.setToken(response.accessToken);
}
private async send( private async send(
method: "GET" | "POST" | "PUT" | "DELETE", method: "GET" | "POST" | "PUT" | "DELETE",
path: string, path: string,

View File

@ -23,7 +23,7 @@ import {
SsoLogInCredentials, SsoLogInCredentials,
} from "../models/domain/logInCredentials"; } from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey"; import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest"; import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
import { PreloginRequest } from "../models/request/preloginRequest"; import { PreloginRequest } from "../models/request/preloginRequest";
import { ErrorResponse } from "../models/response/errorResponse"; import { ErrorResponse } from "../models/response/errorResponse";

View File

@ -2489,11 +2489,10 @@ export class StateService<
protected async deAuthenticateAccount(userId: string) { protected async deAuthenticateAccount(userId: string) {
await this.setAccessToken(null, { userId: userId }); await this.setAccessToken(null, { userId: userId });
await this.setLastActive(null, { userId: userId }); await this.setLastActive(null, { userId: userId });
const index = this.state.authenticatedAccounts.indexOf(userId); this.state.authenticatedAccounts = this.state.authenticatedAccounts.filter(
if (index > -1) { (activeUserId) => activeUserId !== userId
this.state.authenticatedAccounts.splice(index, 1); );
await this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts); await this.storageService.save(keys.authenticatedAccounts, this.state.authenticatedAccounts);
}
} }
protected async removeAccountFromDisk(userId: string) { protected async removeAccountFromDisk(userId: string) {

View File

@ -24,7 +24,7 @@ import {
PasswordLogInCredentials, PasswordLogInCredentials,
SsoLogInCredentials, SsoLogInCredentials,
} from "jslib-common/models/domain/logInCredentials"; } from "jslib-common/models/domain/logInCredentials";
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequest"; import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest"; import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest"; import { UpdateTempPasswordRequest } from "jslib-common/models/request/updateTempPasswordRequest";
import { ErrorResponse } from "jslib-common/models/response/errorResponse"; import { ErrorResponse } from "jslib-common/models/response/errorResponse";
@ -150,11 +150,7 @@ export class LoginCommand {
const twoFactor = const twoFactor =
twoFactorToken == null twoFactorToken == null
? null ? null
: { : new TokenRequestTwoFactor(twoFactorMethod, twoFactorToken, false);
provider: twoFactorMethod,
token: twoFactorToken,
remember: false,
};
try { try {
if (this.validatedParams != null) { if (this.validatedParams != null) {
@ -258,21 +254,13 @@ export class LoginCommand {
} }
response = await this.authService.logInTwoFactor( response = await this.authService.logInTwoFactor(
{ new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken),
provider: selectedProvider.type,
token: twoFactorToken,
remember: false,
},
null null
); );
} }
if (response.captchaSiteKey) { if (response.captchaSiteKey) {
const twoFactorRequest: TokenRequestTwoFactor = { const twoFactorRequest = new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken);
provider: selectedProvider.type,
token: twoFactorToken,
remember: false,
};
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest); const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
// Error Response // Error Response

View File

@ -2,6 +2,7 @@ import * as FormData from "form-data";
import { HttpsProxyAgent } from "https-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent";
import * as fe from "node-fetch"; import * as fe from "node-fetch";
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service"; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TokenService } from "jslib-common/abstractions/token.service"; import { TokenService } from "jslib-common/abstractions/token.service";
@ -18,12 +19,18 @@ export class NodeApiService extends ApiService {
tokenService: TokenService, tokenService: TokenService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService, environmentService: EnvironmentService,
appIdService: AppIdService,
logoutCallback: (expired: boolean) => Promise<void>, 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(
this.apiKeyRefresh = apiKeyRefresh; tokenService,
platformUtilsService,
environmentService,
appIdService,
logoutCallback,
customUserAgent
);
} }
nativeFetch(request: Request): Promise<Response> { nativeFetch(request: Request): Promise<Response> {