1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 10:23:52 +00:00

refactor: introduce @bitwarden/token-provider

feat(token-provider): add TokenProvider abstraction and export from index

feat(token-provider): implement TokenProviderService (lift-and-shift of token logic from ApiService)

feat(token-provider): add prerelease header parity and flags import

feat(token-provider): export TokenProviderService from index
This commit is contained in:
addisonbeck
2025-08-08 19:16:24 -04:00
parent 63446fda81
commit 8d8edc0d9e
37 changed files with 830 additions and 349 deletions

1
.github/CODEOWNERS vendored
View File

@@ -24,6 +24,7 @@ apps/web/src/connectors @bitwarden/team-auth-dev
bitwarden_license/bit-web/src/app/auth @bitwarden/team-auth-dev
libs/angular/src/auth @bitwarden/team-auth-dev
libs/common/src/auth @bitwarden/team-auth-dev
libs/token-provider @bitwarden/team-auth-dev
## Tools team files ##
apps/browser/src/tools @bitwarden/team-tools-dev

View File

@@ -328,6 +328,7 @@ import {
UserAsymmetricKeysRegenerationApiService,
UserAsymmetricKeysRegenerationService,
} from "@bitwarden/key-management";
import { TokenApiService, DefaultTokenApiService } from "@bitwarden/token-provider";
import { SafeInjectionToken } from "@bitwarden/ui-common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
@@ -742,6 +743,19 @@ const safeProviders: SafeProvider[] = [
provide: HTTP_OPERATIONS,
useValue: { createRequest: (url, request) => new Request(url, request) },
}),
safeProvider({
provide: TokenApiService,
useClass: DefaultTokenApiService,
deps: [
PlatformUtilsServiceAbstraction,
EnvironmentService,
AppIdServiceAbstraction,
REFRESH_ACCESS_TOKEN_ERROR_CALLBACK,
LogService,
LOGOUT_CALLBACK,
VaultTimeoutSettingsService,
],
}),
safeProvider({
provide: ApiServiceAbstraction,
useClass: ApiService,

View File

@@ -1,24 +1 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Jsonify } from "type-fest";
import { DeviceType } from "../../../../enums";
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
export class DeviceRequest {
type: DeviceType;
name: string;
identifier: string;
pushToken?: string;
constructor(appId: string, platformUtilsService: PlatformUtilsService) {
this.type = platformUtilsService.getDevice();
this.name = platformUtilsService.getDeviceString();
this.identifier = appId;
this.pushToken = null;
}
static fromJSON(json: Jsonify<DeviceRequest>) {
return Object.assign(Object.create(DeviceRequest.prototype), json);
}
}
export { DeviceRequest } from "@bitwarden/token-provider";

View File

@@ -1,45 +1 @@
import { ClientType } from "../../../../enums";
import { Utils } from "../../../../platform/misc/utils";
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class PasswordTokenRequest extends TokenRequest {
constructor(
public email: string,
public masterPasswordHash: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
public newDeviceOtp?: string,
) {
super(twoFactor, device);
}
toIdentityToken(clientId: ClientType) {
const obj = super.toIdentityToken(clientId);
obj.grant_type = "password";
obj.username = this.email;
obj.password = this.masterPasswordHash;
if (this.newDeviceOtp) {
obj.newDeviceOtp = this.newDeviceOtp;
}
return obj;
}
alterIdentityTokenHeaders(headers: Headers) {
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
}
static fromJSON(json: any) {
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}
export { PasswordTokenRequest } from "@bitwarden/token-provider";

View File

@@ -1,35 +1 @@
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class SsoTokenRequest extends TokenRequest {
constructor(
public code: string,
public codeVerifier: string,
public redirectUri: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
) {
super(twoFactor, device);
}
toIdentityToken(clientId: string) {
const obj = super.toIdentityToken(clientId);
obj.grant_type = "authorization_code";
obj.code = this.code;
obj.code_verifier = this.codeVerifier;
obj.redirect_uri = this.redirectUri;
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(SsoTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}
export { SsoTokenRequest } from "@bitwarden/token-provider";

View File

@@ -1,11 +1 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TwoFactorProviderType } from "../../../enums/two-factor-provider-type";
export class TokenTwoFactorRequest {
constructor(
public provider: TwoFactorProviderType = null,
public token: string = null,
public remember: boolean = false,
) {}
}
export { TokenTwoFactorRequest } from "@bitwarden/token-provider";

View File

@@ -1,58 +1 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
export abstract class TokenRequest {
protected device?: DeviceRequest;
protected authRequest: string;
constructor(
protected twoFactor?: TokenTwoFactorRequest,
device?: DeviceRequest,
) {
this.device = device != null ? device : null;
}
alterIdentityTokenHeaders(headers: Headers) {
// Implemented in subclass if required
}
setTwoFactor(twoFactor: TokenTwoFactorRequest | undefined) {
this.twoFactor = twoFactor;
}
setAuthRequestAccessCode(accessCode: string) {
this.authRequest = accessCode;
}
protected toIdentityToken(clientId: string) {
const obj: any = {
scope: "api offline_access",
client_id: clientId,
};
if (this.device) {
obj.deviceType = this.device.type;
obj.deviceIdentifier = this.device.identifier;
obj.deviceName = this.device.name;
// no push tokens for browser apps yet
// obj.devicePushToken = this.device.pushToken;
}
//passswordless login
if (this.authRequest) {
obj.authRequest = this.authRequest;
}
if (this.twoFactor) {
if (this.twoFactor.token && this.twoFactor.provider != null) {
obj.twoFactorToken = this.twoFactor.token;
obj.twoFactorProvider = this.twoFactor.provider;
obj.twoFactorRemember = this.twoFactor.remember ? "1" : "0";
}
}
return obj;
}
}
export { TokenRequest } from "@bitwarden/token-provider";

View File

@@ -1,33 +1 @@
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class UserApiTokenRequest extends TokenRequest {
constructor(
public clientId: string,
public clientSecret: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
) {
super(twoFactor, device);
}
toIdentityToken() {
const obj = super.toIdentityToken(this.clientId);
obj.scope = this.clientId.startsWith("organization") ? "api.organization" : "api";
obj.grant_type = "client_credentials";
obj.client_secret = this.clientSecret;
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(UserApiTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}
export { UserApiTokenRequest } from "@bitwarden/token-provider";

View File

@@ -1,36 +1 @@
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class WebAuthnLoginTokenRequest extends TokenRequest {
constructor(
public token: string,
public deviceResponse: WebAuthnLoginAssertionResponseRequest,
device?: DeviceRequest,
) {
super(undefined, device);
}
toIdentityToken(clientId: string) {
const obj = super.toIdentityToken(clientId);
obj.grant_type = "webauthn";
obj.token = this.token;
// must be a string b/c sending as form encoded data
obj.deviceResponse = JSON.stringify(this.deviceResponse);
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(WebAuthnLoginTokenRequest.prototype), json, {
deviceResponse: WebAuthnLoginAssertionResponseRequest.fromJSON(json.deviceResponse),
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}
export { WebAuthnLoginTokenRequest } from "@bitwarden/token-provider";

View File

@@ -1,10 +1 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class IdentityDeviceVerificationResponse extends BaseResponse {
deviceVerified: boolean;
constructor(response: any) {
super(response);
this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false;
}
}
export { IdentityDeviceVerificationResponse } from "@bitwarden/token-provider";

View File

@@ -1,69 +1 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
import { KdfType } from "@bitwarden/key-management";
import { EncString } from "../../../key-management/crypto/models/enc-string";
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-decryption-options.response";
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
expiresIn: number;
refreshToken: string;
tokenType: string;
resetMasterPassword: boolean;
privateKey: string; // userKeyEncryptedPrivateKey
key?: EncString; // masterKeyEncryptedUserKey
twoFactorToken: string;
kdf: KdfType;
kdfIterations: number;
kdfMemory?: number;
kdfParallelism?: number;
forcePasswordReset: boolean;
masterPasswordPolicy: MasterPasswordPolicyResponse;
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
userDecryptionOptions: UserDecryptionOptionsResponse;
constructor(response: any) {
super(response);
this.accessToken = response.access_token;
this.expiresIn = response.expires_in;
this.refreshToken = response.refresh_token;
this.tokenType = response.token_type;
this.resetMasterPassword = this.getResponseProperty("ResetMasterPassword");
this.privateKey = this.getResponseProperty("PrivateKey");
const key = this.getResponseProperty("Key");
if (key) {
this.key = new EncString(key);
}
this.twoFactorToken = this.getResponseProperty("TwoFactorToken");
this.kdf = this.getResponseProperty("Kdf");
this.kdfIterations = this.getResponseProperty("KdfIterations");
this.kdfMemory = this.getResponseProperty("KdfMemory");
this.kdfParallelism = this.getResponseProperty("KdfParallelism");
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset");
this.apiUseKeyConnector = this.getResponseProperty("ApiUseKeyConnector");
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy"),
);
if (response.UserDecryptionOptions) {
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
this.getResponseProperty("UserDecryptionOptions"),
);
}
}
hasMasterKeyEncryptedUserKey(): boolean {
return Boolean(this.key);
}
}
export { IdentityTokenResponse } from "@bitwarden/token-provider";

View File

@@ -1,26 +1 @@
import { BaseResponse } from "../../../models/response/base.response";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
export class IdentityTwoFactorResponse extends BaseResponse {
// contains available two-factor providers
twoFactorProviders: TwoFactorProviderType[];
// a map of two-factor providers to necessary data for completion
twoFactorProviders2: Record<TwoFactorProviderType, Record<string, string>>;
ssoEmail2faSessionToken: string;
email?: string;
masterPasswordPolicy?: MasterPasswordPolicyResponse;
constructor(response: any) {
super(response);
this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders");
this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2");
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy"),
);
this.ssoEmail2faSessionToken = this.getResponseProperty("SsoEmail2faSessionToken");
this.email = this.getResponseProperty("Email");
}
}
export { IdentityTwoFactorResponse } from "@bitwarden/token-provider";

View File

@@ -0,0 +1,5 @@
# token-provider
Owned by: auth
Auth token provider (Identity and API token flows)

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "token-provider",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/token-provider",
};

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/token-provider",
"version": "0.0.1",
"description": "Auth token provider (Identity and API token flows)",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL-3.0",
"author": "auth"
}

View File

@@ -0,0 +1,33 @@
{
"name": "token-provider",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/token-provider/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/token-provider",
"main": "libs/token-provider/src/index.ts",
"tsConfig": "libs/token-provider/tsconfig.lib.json",
"assets": ["libs/token-provider/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/token-provider/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/token-provider/jest.config.js"
}
}
}
}

View File

@@ -0,0 +1,339 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { LogService } from "@bitwarden/logging";
import { PlatformUtilsService } from "@bitwarden/platform-utils";
import { LogoutReason } from "@bitwarden/auth/common";
import { DeviceRequest } from "./device.request";
import { PasswordTokenRequest } from "./password-token.request";
import { SsoTokenRequest } from "./sso-token.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { UserApiTokenRequest } from "./user-api-token.request";
import { WebAuthnLoginTokenRequest } from "./webauthn-login-token.request";
import { IdentityDeviceVerificationResponse } from "./identity-device-verification.response";
import { IdentityTokenResponse } from "./identity-token.response";
import { IdentityTwoFactorResponse } from "./identity-two-factor.response";
import { DeviceType } from "@bitwarden/device-type";
import { ClientType } from "@bitwarden/client-type";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { VaultTimeoutAction } from "@bitwarden/common/key-management/vault-timeout/enums/vault-timeout-action.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { flagEnabled } from "@bitwarden/common/platform/misc/flags";
import { TokenApiService } from "./token-api.service";
export type HttpOperations = {
createRequest: (url: string, request: RequestInit) => Request;
};
export class DefaultTokenApiService implements TokenApiService {
private device: DeviceType;
private deviceType: string;
private refreshTokenPromise: Promise<string> | undefined;
/**
* The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required.
*/
private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE =
"new device verification required";
constructor(
private platformUtilsService: PlatformUtilsService,
private environmentService: EnvironmentService,
private appIdService: AppIdService,
private refreshAccessTokenErrorCallback: () => void,
private logService: LogService,
private logoutCallback: (logoutReason: LogoutReason) => Promise<void>,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private customUserAgent: string = null,
) {
this.device = platformUtilsService.getDevice();
this.deviceType = this.device.toString();
}
async postIdentityToken(
request:
| UserApiTokenRequest
| PasswordTokenRequest
| SsoTokenRequest
| WebAuthnLoginTokenRequest,
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
> {
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
Accept: "application/json",
"Device-Type": this.deviceType,
});
if (flagEnabled("prereleaseBuild")) {
headers.set("Is-Prerelease", "1");
}
if (flagEnabled("prereleaseBuild")) {
headers.set("Is-Prerelease", "1");
}
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
request.alterIdentityTokenHeaders(headers);
const identityToken =
request instanceof UserApiTokenRequest
? request.toIdentityToken()
: request.toIdentityToken(this.platformUtilsService.getClientType());
const env = await firstValueFrom(this.environmentService.environment$);
const response = await this.fetch(
this.httpOperations.createRequest(env.getIdentityUrl() + "/connect/token", {
body: this.qsStringify(identityToken),
credentials: await this.getCredentials(),
cache: "no-store",
headers: headers,
method: "POST",
}),
);
let responseJson: any = null;
if (this.isJsonResponse(response)) {
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
) {
return new IdentityTwoFactorResponse(responseJson);
} else if (
response.status === 400 &&
responseJson?.ErrorModel?.Message ===
DefaultTokenApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE
) {
return new IdentityDeviceVerificationResponse(responseJson);
}
}
return Promise.reject(new ErrorResponse(responseJson, response.status, true));
}
async refreshIdentityToken(): Promise<any> {
try {
await this.refreshToken();
} catch (e) {
this.logService.error("Error refreshing access token: ", e);
throw e;
}
}
async getActiveBearerToken(): Promise<string> {
let accessToken = await this.tokenService.getAccessToken();
if (await this.tokenService.tokenNeedsRefresh()) {
accessToken = await this.refreshToken();
}
return accessToken;
}
async fetch(request: Request): Promise<Response> {
if (request.method === "GET") {
request.headers.set("Cache-Control", "no-store");
request.headers.set("Pragma", "no-cache");
}
request.headers.set("Bitwarden-Client-Name", this.platformUtilsService.getClientType());
request.headers.set(
"Bitwarden-Client-Version",
await this.platformUtilsService.getApplicationVersionNumber(),
);
return this.nativeFetch(request);
}
nativeFetch(request: Request): Promise<Response> {
return fetch(request);
}
private async handleError(
response: Response,
tokenError: boolean,
authed: boolean,
): Promise<ErrorResponse> {
let responseJson: any = null;
if (this.isJsonResponse(response)) {
responseJson = await response.json();
} else if (this.isTextPlainResponse(response)) {
responseJson = { Message: await response.text() };
}
if (authed) {
if (
response.status === 401 ||
response.status === 403 ||
(tokenError &&
response.status === 400 &&
responseJson != null &&
responseJson.error === "invalid_grant")
) {
await this.logoutCallback("invalidGrantError");
}
}
return new ErrorResponse(responseJson, response.status, tokenError);
}
private qsStringify(params: any): string {
return Object.keys(params)
.map((key) => {
return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]);
})
.join("&");
}
private async getCredentials(): Promise<RequestCredentials> {
const env = await firstValueFrom(this.environmentService.environment$);
if (this.platformUtilsService.getClientType() !== ClientType.Web || env.hasBaseUrl()) {
return "include";
}
return undefined;
}
private isJsonResponse(response: Response): boolean {
const typeHeader = response.headers.get("content-type");
return typeHeader != null && typeHeader.indexOf("application/json") > -1;
}
private isTextPlainResponse(response: Response): boolean {
const typeHeader = response.headers.get("content-type");
return typeHeader != null && typeHeader.indexOf("text/plain") > -1;
}
// Token refresh helpers (lift-and-shift parity with ApiService)
private async internalRefreshToken(): Promise<string> {
const refreshToken = await this.tokenService.getRefreshToken();
if (refreshToken != null && refreshToken !== "") {
return this.refreshAccessToken();
}
const clientId = await this.tokenService.getClientId();
const clientSecret = await this.tokenService.getClientSecret();
if (!clientId || !clientSecret) {
// fall through
} else {
return this.refreshApiToken();
}
this.refreshAccessTokenErrorCallback();
throw new Error("Cannot refresh access token, no refresh token or api keys are stored.");
}
protected refreshToken(): Promise<string> {
if (this.refreshTokenPromise === undefined) {
this.refreshTokenPromise = this.internalRefreshToken();
void this.refreshTokenPromise.finally(() => {
this.refreshTokenPromise = undefined;
});
}
return this.refreshTokenPromise;
}
protected async refreshAccessToken(): Promise<string> {
const refreshToken = await this.tokenService.getRefreshToken();
if (refreshToken == null || refreshToken === "") {
throw new Error();
}
const headers = new Headers({
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
Accept: "application/json",
"Device-Type": this.deviceType,
});
if (this.customUserAgent != null) {
headers.set("User-Agent", this.customUserAgent);
}
const env = await firstValueFrom(this.environmentService.environment$);
const decodedToken = await this.tokenService.decodeAccessToken();
const response = await this.fetch(
this.httpOperations.createRequest(env.getIdentityUrl() + "/connect/token", {
body: this.qsStringify({
grant_type: "refresh_token",
client_id: decodedToken.client_id,
refresh_token: refreshToken,
}),
cache: "no-store",
credentials: await this.getCredentials(),
headers: headers,
method: "POST",
}),
);
if (response.status === 200) {
const responseJson = await response.json();
const tokenResponse = new IdentityTokenResponse(responseJson);
const newDecodedAccessToken = await this.tokenService.decodeAccessToken(
tokenResponse.accessToken,
);
const userId = newDecodedAccessToken.sub;
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
const vaultTimeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
const refreshedTokens = await this.tokenService.setTokens(
tokenResponse.accessToken,
vaultTimeoutAction as VaultTimeoutAction,
vaultTimeout,
tokenResponse.refreshToken,
);
return refreshedTokens.accessToken;
} else {
const error = await this.handleError(response, true, true);
return Promise.reject(error);
}
}
protected async refreshApiToken(): Promise<string> {
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 UserApiTokenRequest(
clientId,
clientSecret,
new TokenTwoFactorRequest(),
deviceRequest,
);
const response = await this.postIdentityToken(tokenRequest);
if (!(response instanceof IdentityTokenResponse)) {
throw new Error("Invalid response received when refreshing api token");
}
const newDecodedAccessToken = await this.tokenService.decodeAccessToken(response.accessToken);
const userId = newDecodedAccessToken.sub;
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(userId),
);
const vaultTimeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(userId),
);
const refreshedToken = await this.tokenService.setAccessToken(
response.accessToken,
vaultTimeoutAction as VaultTimeoutAction,
vaultTimeout,
);
return refreshedToken;
}
}

View File

@@ -3,8 +3,7 @@
import { Jsonify } from "type-fest";
import { DeviceType } from "@bitwarden/device-type";
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
import { PlatformUtilsService } from "@bitwarden/platform-utils";
export class DeviceRequest {
type: DeviceType;

View File

@@ -0,0 +1,10 @@
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class IdentityDeviceVerificationResponse extends BaseResponse {
deviceVerified: boolean;
constructor(response: any) {
super(response);
this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false;
}
}

View File

@@ -0,0 +1,67 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { UserDecryptionOptionsResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
import { KdfType } from "@bitwarden/key-management";
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
expiresIn: number;
refreshToken: string;
tokenType: string;
resetMasterPassword: boolean;
privateKey: string; // userKeyEncryptedPrivateKey
key?: EncString; // masterKeyEncryptedUserKey
twoFactorToken: string;
kdf: KdfType;
kdfIterations: number;
kdfMemory?: number;
kdfParallelism?: number;
forcePasswordReset: boolean;
masterPasswordPolicy: MasterPasswordPolicyResponse;
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
userDecryptionOptions: UserDecryptionOptionsResponse;
constructor(response: any) {
super(response);
this.accessToken = response.access_token;
this.expiresIn = response.expires_in;
this.refreshToken = response.refresh_token;
this.tokenType = response.token_type;
this.resetMasterPassword = this.getResponseProperty("ResetMasterPassword");
this.privateKey = this.getResponseProperty("PrivateKey");
const key = this.getResponseProperty("Key");
if (key) {
this.key = new EncString(key);
}
this.twoFactorToken = this.getResponseProperty("TwoFactorToken");
this.kdf = this.getResponseProperty("Kdf");
this.kdfIterations = this.getResponseProperty("KdfIterations");
this.kdfMemory = this.getResponseProperty("KdfMemory");
this.kdfParallelism = this.getResponseProperty("KdfParallelism");
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset");
this.apiUseKeyConnector = this.getResponseProperty("ApiUseKeyConnector");
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy"),
);
if (response.UserDecryptionOptions) {
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
this.getResponseProperty("UserDecryptionOptions"),
);
}
}
hasMasterKeyEncryptedUserKey(): boolean {
return Boolean(this.key);
}
}

View File

@@ -0,0 +1,25 @@
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
export class IdentityTwoFactorResponse extends BaseResponse {
// contains available two-factor providers
twoFactorProviders: TwoFactorProviderType[];
// a map of two-factor providers to necessary data for completion
twoFactorProviders2: Record<TwoFactorProviderType, Record<string, string>>;
ssoEmail2faSessionToken: string;
email?: string;
masterPasswordPolicy?: MasterPasswordPolicyResponse;
constructor(response: any) {
super(response);
this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders");
this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2");
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy"),
);
this.ssoEmail2faSessionToken = this.getResponseProperty("SsoEmail2faSessionToken");
this.email = this.getResponseProperty("Email");
}
}

View File

@@ -0,0 +1,5 @@
export * from "./token-api.service";
export * from "./default-token-api.service";
export * from "./identity-token.response";
export * from "./identity-two-factor.response";
export * from "./identity-device-verification.response";

View File

@@ -0,0 +1,45 @@
import { ClientType } from "@bitwarden/client-type";
import { fromUtf8ToUrlB64 } from "@bitwarden/encoding";
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class PasswordTokenRequest extends TokenRequest {
constructor(
public email: string,
public masterPasswordHash: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
public newDeviceOtp?: string,
) {
super(twoFactor, device);
}
toIdentityToken(clientId: ClientType) {
const obj = super.toIdentityToken(clientId);
obj.grant_type = "password";
obj.username = this.email;
obj.password = this.masterPasswordHash;
if (this.newDeviceOtp) {
obj.newDeviceOtp = this.newDeviceOtp;
}
return obj;
}
alterIdentityTokenHeaders(headers: Headers) {
headers.set("Auth-Email", fromUtf8ToUrlB64(this.email) ?? "");
}
static fromJSON(json: any) {
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -0,0 +1,35 @@
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class SsoTokenRequest extends TokenRequest {
constructor(
public code: string,
public codeVerifier: string,
public redirectUri: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
) {
super(twoFactor, device);
}
toIdentityToken(clientId: string) {
const obj = super.toIdentityToken(clientId);
obj.grant_type = "authorization_code";
obj.code = this.code;
obj.code_verifier = this.codeVerifier;
obj.redirect_uri = this.redirectUri;
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(SsoTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -0,0 +1,21 @@
import { IdentityDeviceVerificationResponse } from "./identity-device-verification.response";
import { IdentityTokenResponse } from "./identity-token.response";
import { IdentityTwoFactorResponse } from "./identity-two-factor.response";
import { PasswordTokenRequest } from "./password-token.request";
import { SsoTokenRequest } from "./sso-token.request";
import { UserApiTokenRequest } from "./user-api-token.request";
import { WebAuthnLoginTokenRequest } from "./webauthn-login-token.request";
export abstract class TokenApiService {
abstract postIdentityToken(
request:
| UserApiTokenRequest
| PasswordTokenRequest
| SsoTokenRequest
| WebAuthnLoginTokenRequest,
): Promise<
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
>;
abstract refreshIdentityToken(): Promise<any>;
abstract getActiveBearerToken(): Promise<string>;
}

View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("token-provider", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

View File

@@ -0,0 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { TwoFactorProviderType } from "../../../enums/two-factor-provider-type";
export class TokenTwoFactorRequest {
constructor(
public provider: TwoFactorProviderType = null,
public token: string = null,
public remember: boolean = false,
) {}
}

View File

@@ -0,0 +1,58 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
export abstract class TokenRequest {
protected device?: DeviceRequest;
protected authRequest: string;
constructor(
protected twoFactor?: TokenTwoFactorRequest,
device?: DeviceRequest,
) {
this.device = device != null ? device : null;
}
alterIdentityTokenHeaders(headers: Headers) {
// Implemented in subclass if required
}
setTwoFactor(twoFactor: TokenTwoFactorRequest | undefined) {
this.twoFactor = twoFactor;
}
setAuthRequestAccessCode(accessCode: string) {
this.authRequest = accessCode;
}
protected toIdentityToken(clientId: string) {
const obj: any = {
scope: "api offline_access",
client_id: clientId,
};
if (this.device) {
obj.deviceType = this.device.type;
obj.deviceIdentifier = this.device.identifier;
obj.deviceName = this.device.name;
// no push tokens for browser apps yet
// obj.devicePushToken = this.device.pushToken;
}
//passswordless login
if (this.authRequest) {
obj.authRequest = this.authRequest;
}
if (this.twoFactor) {
if (this.twoFactor.token && this.twoFactor.provider != null) {
obj.twoFactorToken = this.twoFactor.token;
obj.twoFactorProvider = this.twoFactor.provider;
obj.twoFactorRemember = this.twoFactor.remember ? "1" : "0";
}
}
return obj;
}
}

View File

@@ -0,0 +1,33 @@
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class UserApiTokenRequest extends TokenRequest {
constructor(
public clientId: string,
public clientSecret: string,
protected twoFactor: TokenTwoFactorRequest,
device?: DeviceRequest,
) {
super(twoFactor, device);
}
toIdentityToken() {
const obj = super.toIdentityToken(this.clientId);
obj.scope = this.clientId.startsWith("organization") ? "api.organization" : "api";
obj.grant_type = "client_credentials";
obj.client_secret = this.clientSecret;
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(UserApiTokenRequest.prototype), json, {
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -0,0 +1,36 @@
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
import { DeviceRequest } from "./device.request";
import { TokenTwoFactorRequest } from "./token-two-factor.request";
import { TokenRequest } from "./token.request";
export class WebAuthnLoginTokenRequest extends TokenRequest {
constructor(
public token: string,
public deviceResponse: WebAuthnLoginAssertionResponseRequest,
device?: DeviceRequest,
) {
super(undefined, device);
}
toIdentityToken(clientId: string) {
const obj = super.toIdentityToken(clientId);
obj.grant_type = "webauthn";
obj.token = this.token;
// must be a string b/c sending as form encoded data
obj.deviceResponse = JSON.stringify(this.deviceResponse);
return obj;
}
static fromJSON(json: any) {
return Object.assign(Object.create(WebAuthnLoginTokenRequest.prototype), json, {
deviceResponse: WebAuthnLoginAssertionResponseRequest.fromJSON(json.deviceResponse),
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
twoFactor: json.twoFactor
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
: undefined,
});
}
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

9
package-lock.json generated
View File

@@ -432,6 +432,11 @@
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/token-provider": {
"name": "@bitwarden/token-provider",
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/tools/export/vault-export/vault-export-core": {
"name": "@bitwarden/vault-export-core",
"version": "0.0.0",
@@ -4735,6 +4740,10 @@
"resolved": "libs/storage-test-utils",
"link": true
},
"node_modules/@bitwarden/token-provider": {
"resolved": "libs/token-provider",
"link": true
},
"node_modules/@bitwarden/ui-common": {
"resolved": "libs/ui/common",
"link": true

View File

@@ -56,6 +56,7 @@
"@bitwarden/state-test-utils": ["libs/state-test-utils/src/index.ts"],
"@bitwarden/storage-core": ["libs/storage-core/src/index.ts"],
"@bitwarden/storage-test-utils": ["libs/storage-test-utils/src/index.ts"],
"@bitwarden/token-provider": ["libs/token-provider/src/index.ts"],
"@bitwarden/ui-common": ["./libs/ui/common/src"],
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
"@bitwarden/user-core": ["libs/user-core/src/index.ts"],