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:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
5
libs/token-provider/README.md
Normal file
5
libs/token-provider/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# token-provider
|
||||
|
||||
Owned by: auth
|
||||
|
||||
Auth token provider (Identity and API token flows)
|
||||
3
libs/token-provider/eslint.config.mjs
Normal file
3
libs/token-provider/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "../../eslint.config.mjs";
|
||||
|
||||
export default [...baseConfig];
|
||||
10
libs/token-provider/jest.config.js
Normal file
10
libs/token-provider/jest.config.js
Normal 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",
|
||||
};
|
||||
11
libs/token-provider/package.json
Normal file
11
libs/token-provider/package.json
Normal 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"
|
||||
}
|
||||
33
libs/token-provider/project.json
Normal file
33
libs/token-provider/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
339
libs/token-provider/src/default-token-api.service.ts
Normal file
339
libs/token-provider/src/default-token-api.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
67
libs/token-provider/src/identity-token.response.ts
Normal file
67
libs/token-provider/src/identity-token.response.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
25
libs/token-provider/src/identity-two-factor.response.ts
Normal file
25
libs/token-provider/src/identity-two-factor.response.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
5
libs/token-provider/src/index.ts
Normal file
5
libs/token-provider/src/index.ts
Normal 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";
|
||||
45
libs/token-provider/src/password-token.request.ts
Normal file
45
libs/token-provider/src/password-token.request.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
35
libs/token-provider/src/sso-token.request.ts
Normal file
35
libs/token-provider/src/sso-token.request.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
21
libs/token-provider/src/token-api.service.ts
Normal file
21
libs/token-provider/src/token-api.service.ts
Normal 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>;
|
||||
}
|
||||
8
libs/token-provider/src/token-provider.spec.ts
Normal file
8
libs/token-provider/src/token-provider.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
11
libs/token-provider/src/token-two-factor.request.ts
Normal file
11
libs/token-provider/src/token-two-factor.request.ts
Normal 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,
|
||||
) {}
|
||||
}
|
||||
58
libs/token-provider/src/token.request.ts
Normal file
58
libs/token-provider/src/token.request.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
33
libs/token-provider/src/user-api-token.request.ts
Normal file
33
libs/token-provider/src/user-api-token.request.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
36
libs/token-provider/src/webauthn-login-token.request.ts
Normal file
36
libs/token-provider/src/webauthn-login-token.request.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
6
libs/token-provider/tsconfig.eslint.json
Normal file
6
libs/token-provider/tsconfig.eslint.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": ["src/**/*.ts", "src/**/*.js"],
|
||||
"exclude": ["**/build", "**/dist"]
|
||||
}
|
||||
13
libs/token-provider/tsconfig.json
Normal file
13
libs/token-provider/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
libs/token-provider/tsconfig.lib.json
Normal file
10
libs/token-provider/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
10
libs/token-provider/tsconfig.spec.json
Normal file
10
libs/token-provider/tsconfig.spec.json
Normal 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
9
package-lock.json
generated
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user