mirror of
https://github.com/bitwarden/browser
synced 2026-02-22 20:34:04 +00:00
Merge branch 'main' into km/replace-encstring-with-unsigned-shared-key
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
SsoLoginCredentials,
|
||||
SsoUrlService,
|
||||
UserApiLoginCredentials,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@@ -82,6 +83,7 @@ export class LoginCommand {
|
||||
protected ssoUrlService: SsoUrlService,
|
||||
protected i18nService: I18nService,
|
||||
protected masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
protected encryptedMigrator: EncryptedMigrator,
|
||||
) {}
|
||||
|
||||
@@ -113,20 +115,14 @@ export class LoginCommand {
|
||||
} else if (options.sso != null && this.canInteract) {
|
||||
// If the optional Org SSO Identifier isn't provided, the option value is `true`.
|
||||
const orgSsoIdentifier = options.sso === true ? null : options.sso;
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
const ssoPromptData = await this.makeSsoPromptData();
|
||||
ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
|
||||
try {
|
||||
const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier);
|
||||
const ssoParams = await this.openSsoPrompt(
|
||||
ssoPromptData.codeChallenge,
|
||||
ssoPromptData.state,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
ssoCode = ssoParams.ssoCode;
|
||||
orgIdentifier = ssoParams.orgIdentifier;
|
||||
} catch {
|
||||
@@ -231,9 +227,43 @@ export class LoginCommand {
|
||||
new PasswordLoginCredentials(email, password, twoFactor),
|
||||
);
|
||||
}
|
||||
|
||||
// Begin Acting on initial AuthResult
|
||||
|
||||
if (response.requiresEncryptionKeyMigration) {
|
||||
return Response.error(this.i18nService.t("legacyEncryptionUnsupported"));
|
||||
}
|
||||
|
||||
// Opting for not checking feature flag since the server will not respond with
|
||||
// SsoOrganizationIdentifier if the feature flag is not enabled.
|
||||
if (response.requiresSso && this.canInteract) {
|
||||
const ssoPromptData = await this.makeSsoPromptData();
|
||||
ssoCodeVerifier = ssoPromptData.ssoCodeVerifier;
|
||||
try {
|
||||
const ssoParams = await this.openSsoPrompt(
|
||||
ssoPromptData.codeChallenge,
|
||||
ssoPromptData.state,
|
||||
response.ssoOrganizationIdentifier,
|
||||
);
|
||||
ssoCode = ssoParams.ssoCode;
|
||||
orgIdentifier = ssoParams.orgIdentifier;
|
||||
if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.loginStrategyService.logIn(
|
||||
new SsoLoginCredentials(
|
||||
ssoCode,
|
||||
ssoCodeVerifier,
|
||||
this.ssoRedirectUri,
|
||||
orgIdentifier,
|
||||
undefined, // email to look up 2FA token not required as CLI can't remember 2FA token
|
||||
twoFactor,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
return Response.badRequest("Something went wrong. Try again.");
|
||||
}
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
@@ -279,6 +309,10 @@ export class LoginCommand {
|
||||
if (twoFactorToken == null && selectedProvider.type === TwoFactorProviderType.Email) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = await this.loginStrategyService.getEmail();
|
||||
// if the user was logging in with SSO, we need to include the SSO session token
|
||||
if (response.ssoEmail2FaSessionToken != null) {
|
||||
emailReq.ssoEmail2FaSessionToken = response.ssoEmail2FaSessionToken;
|
||||
}
|
||||
emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||
await this.twoFactorApiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
@@ -324,15 +358,18 @@ export class LoginCommand {
|
||||
response = await this.loginStrategyService.logInNewDeviceVerification(newDeviceToken);
|
||||
}
|
||||
|
||||
// We check response two factor again here since MFA could fail based on the logic on ln 226
|
||||
if (response.requiresTwoFactor) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
|
||||
if (response.resetMasterPassword) {
|
||||
return Response.error(
|
||||
"In order to log in with SSO from the CLI, you must first log in" +
|
||||
" through the web vault to set your master password.",
|
||||
);
|
||||
// If we are in the SSO flow and we got a successful login response (we are past rejection scenarios
|
||||
// and should always have a userId here), validate that SSO user in MP encryption org has MP set
|
||||
// This must be done here b/c we have 2 places we try to login with SSO above and neither has a
|
||||
// common handleSsoAuthnResult method to consoldiate this logic into (1. the normal SSO flow and
|
||||
// 2. the requiresSso automatic authentication flow)
|
||||
if (ssoCode != null && ssoCodeVerifier != null && response.userId) {
|
||||
await this.validateSsoUserInMpEncryptionOrgHasMp(response.userId);
|
||||
}
|
||||
|
||||
// Check if Key Connector domain confirmation is required
|
||||
@@ -692,6 +729,27 @@ export class LoginCommand {
|
||||
};
|
||||
}
|
||||
|
||||
/// Generate SSO prompt data: code verifier, code challenge, and state
|
||||
private async makeSsoPromptData(): Promise<{
|
||||
ssoCodeVerifier: string;
|
||||
codeChallenge: string;
|
||||
state: string;
|
||||
}> {
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
return { ssoCodeVerifier, codeChallenge, state };
|
||||
}
|
||||
|
||||
private async openSsoPrompt(
|
||||
codeChallenge: string,
|
||||
state: string,
|
||||
@@ -782,4 +840,35 @@ export class LoginCommand {
|
||||
const checkStateSplit = checkState.split("_identifier=");
|
||||
return stateSplit[0] === checkStateSplit[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a user logging in with SSO that is in an org using MP encryption
|
||||
* has a MP set. If not, they cannot set a MP in the CLI and must use another client.
|
||||
* @param userId
|
||||
* @returns void
|
||||
*/
|
||||
private async validateSsoUserInMpEncryptionOrgHasMp(userId: UserId): Promise<void> {
|
||||
const userDecryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptionsById$(userId),
|
||||
);
|
||||
|
||||
// device trust isn't supported in the CLI as we don't have persistent device key storage.
|
||||
const notUsingTrustedDeviceEncryption = !userDecryptionOptions.trustedDeviceOption;
|
||||
const notUsingKeyConnector = !userDecryptionOptions.keyConnectorOption;
|
||||
|
||||
if (
|
||||
notUsingTrustedDeviceEncryption &&
|
||||
notUsingKeyConnector &&
|
||||
!userDecryptionOptions.hasMasterPassword
|
||||
) {
|
||||
// If user is in an org that is using MP encryption and they JIT provisioned but
|
||||
// have not yet set a MP and come to the CLI to login, they won't be able to unlock
|
||||
// or set a MP in the CLI as it isn't supported.
|
||||
await this.logoutCallback();
|
||||
throw Response.error(
|
||||
"In order to log in with SSO from the CLI, you must first log in" +
|
||||
" through the web vault, the desktop, or the extension to set your master password.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
import { TemplateResponse } from "../models/response/template.response";
|
||||
@@ -17,16 +17,17 @@ export class StatusCommand {
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private userAutoUnlockKeyService: UserAutoUnlockKeyService,
|
||||
) {}
|
||||
|
||||
async run(): Promise<Response> {
|
||||
try {
|
||||
const baseUrl = await this.baseUrl();
|
||||
const status = await this.status();
|
||||
const lastSync = await this.syncService.getLastSync();
|
||||
const [userId, email] = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),
|
||||
);
|
||||
const status = await this.status(userId);
|
||||
|
||||
return Response.success(
|
||||
new TemplateResponse({
|
||||
@@ -42,12 +43,18 @@ export class StatusCommand {
|
||||
}
|
||||
}
|
||||
|
||||
private async baseUrl(): Promise<string> {
|
||||
private async baseUrl(): Promise<string | undefined> {
|
||||
const env = await firstValueFrom(this.envService.environment$);
|
||||
return env.getUrls().base;
|
||||
}
|
||||
|
||||
private async status(): Promise<"unauthenticated" | "locked" | "unlocked"> {
|
||||
private async status(
|
||||
userId: UserId | undefined,
|
||||
): Promise<"unauthenticated" | "locked" | "unlocked"> {
|
||||
if (userId != null) {
|
||||
await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId);
|
||||
}
|
||||
|
||||
const authStatus = await this.authService.getAuthStatus();
|
||||
if (authStatus === AuthenticationStatus.Unlocked) {
|
||||
return "unlocked";
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -48,9 +49,10 @@ describe("UnlockCommand", () => {
|
||||
const mockMasterPassword = "testExample";
|
||||
const activeAccount: Account = {
|
||||
id: "user-id" as UserId,
|
||||
email: "user@example.com",
|
||||
emailVerified: true,
|
||||
name: "User",
|
||||
...mockAccountInfoWith({
|
||||
email: "user@example.com",
|
||||
name: "User",
|
||||
}),
|
||||
};
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockSessionKey = new Uint8Array(64) as CsprngArray;
|
||||
|
||||
@@ -122,6 +122,7 @@ export class OssServeConfigurator {
|
||||
this.serviceContainer.syncService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.authService,
|
||||
this.serviceContainer.userAutoUnlockKeyService,
|
||||
);
|
||||
this.deleteCommand = new DeleteCommand(
|
||||
this.serviceContainer.cipherService,
|
||||
|
||||
@@ -195,6 +195,7 @@ export class Program extends BaseProgram {
|
||||
this.serviceContainer.ssoUrlService,
|
||||
this.serviceContainer.i18nService,
|
||||
this.serviceContainer.masterPasswordService,
|
||||
this.serviceContainer.userDecryptionOptionsService,
|
||||
this.serviceContainer.encryptedMigrator,
|
||||
);
|
||||
const response = await command.run(email, password, options);
|
||||
@@ -524,6 +525,7 @@ export class Program extends BaseProgram {
|
||||
this.serviceContainer.syncService,
|
||||
this.serviceContainer.accountService,
|
||||
this.serviceContainer.authService,
|
||||
this.serviceContainer.userAutoUnlockKeyService,
|
||||
);
|
||||
const response = await command.run();
|
||||
this.processResponse(response);
|
||||
|
||||
@@ -69,6 +69,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs
|
||||
import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service";
|
||||
import { HibpApiService } from "@bitwarden/common/dirt/services/hibp-api.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { DefaultAccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/default-account-cryptographic-state.service";
|
||||
import {
|
||||
DefaultKeyGenerationService,
|
||||
KeyGenerationService,
|
||||
@@ -103,6 +104,7 @@ import {
|
||||
EnvironmentService,
|
||||
RegionConfig,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { LogLevelType } from "@bitwarden/common/platform/enums";
|
||||
@@ -123,6 +125,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/default-sdk.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { DefaultRegisterSdkService } from "@bitwarden/common/platform/services/sdk/register-sdk.service";
|
||||
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
@@ -322,6 +325,7 @@ export class ServiceContainer {
|
||||
kdfConfigService: KdfConfigService;
|
||||
taskSchedulerService: TaskSchedulerService;
|
||||
sdkService: SdkService;
|
||||
registerSdkService: RegisterSdkService;
|
||||
sdkLoadService: SdkLoadService;
|
||||
cipherAuthorizationService: CipherAuthorizationService;
|
||||
ssoUrlService: SsoUrlService;
|
||||
@@ -334,6 +338,7 @@ export class ServiceContainer {
|
||||
masterPasswordUnlockService: MasterPasswordUnlockService;
|
||||
cipherArchiveService: CipherArchiveService;
|
||||
lockService: LockService;
|
||||
private accountCryptographicStateService: DefaultAccountCryptographicStateService;
|
||||
|
||||
constructor() {
|
||||
let p = null;
|
||||
@@ -492,10 +497,7 @@ export class ServiceContainer {
|
||||
|
||||
const pinStateService = new PinStateService(this.stateProvider);
|
||||
this.pinService = new PinService(
|
||||
this.accountService,
|
||||
this.encryptService,
|
||||
this.kdfConfigService,
|
||||
this.keyGenerationService,
|
||||
this.logService,
|
||||
this.keyService,
|
||||
this.sdkService,
|
||||
@@ -633,26 +635,10 @@ export class ServiceContainer {
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.accountCryptographicStateService = new DefaultAccountCryptographicStateService(
|
||||
this.stateProvider,
|
||||
);
|
||||
|
||||
this.twoFactorService = new DefaultTwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
this.twoFactorApiService,
|
||||
);
|
||||
|
||||
const sdkClientFactory = flagEnabled("sdk")
|
||||
? new DefaultSdkClientFactory()
|
||||
: new NoopSdkClientFactory();
|
||||
@@ -671,6 +657,41 @@ export class ServiceContainer {
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.registerSdkService = new DefaultRegisterSdkService(
|
||||
sdkClientFactory,
|
||||
this.environmentService,
|
||||
this.platformUtilsService,
|
||||
this.accountService,
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
customUserAgent,
|
||||
);
|
||||
|
||||
this.keyConnectorService = new KeyConnectorService(
|
||||
this.accountService,
|
||||
this.masterPasswordService,
|
||||
this.keyService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.logService,
|
||||
this.organizationService,
|
||||
this.keyGenerationService,
|
||||
logoutCallback,
|
||||
this.stateProvider,
|
||||
this.configService,
|
||||
this.registerSdkService,
|
||||
this.securityStateService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.twoFactorService = new DefaultTwoFactorService(
|
||||
this.i18nService,
|
||||
this.platformUtilsService,
|
||||
this.globalStateProvider,
|
||||
this.twoFactorApiService,
|
||||
);
|
||||
|
||||
this.passwordStrengthService = new PasswordStrengthService();
|
||||
|
||||
this.passwordGenerationService = legacyPasswordGenerationServiceFactory(
|
||||
@@ -747,6 +768,7 @@ export class ServiceContainer {
|
||||
this.kdfConfigService,
|
||||
this.taskSchedulerService,
|
||||
this.configService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.restrictedItemTypesService = new RestrictedItemTypesService(
|
||||
@@ -882,6 +904,7 @@ export class ServiceContainer {
|
||||
this.stateProvider,
|
||||
this.securityStateService,
|
||||
this.kdfConfigService,
|
||||
this.accountCryptographicStateService,
|
||||
);
|
||||
|
||||
this.totpService = new TotpService(this.sdkService);
|
||||
@@ -908,7 +931,7 @@ export class ServiceContainer {
|
||||
this.collectionService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.accountService,
|
||||
this.restrictedItemTypesService,
|
||||
);
|
||||
@@ -916,7 +939,7 @@ export class ServiceContainer {
|
||||
this.individualExportService = new IndividualVaultExportService(
|
||||
this.folderService,
|
||||
this.cipherService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
@@ -930,7 +953,7 @@ export class ServiceContainer {
|
||||
this.organizationExportService = new OrganizationVaultExportService(
|
||||
this.cipherService,
|
||||
this.vaultExportApiService,
|
||||
this.pinService,
|
||||
this.keyGenerationService,
|
||||
this.keyService,
|
||||
this.encryptService,
|
||||
this.cryptoFunctionService,
|
||||
@@ -985,7 +1008,12 @@ export class ServiceContainer {
|
||||
|
||||
this.masterPasswordApiService = new MasterPasswordApiService(this.apiService, this.logService);
|
||||
const changeKdfApiService = new DefaultChangeKdfApiService(this.apiService);
|
||||
const changeKdfService = new DefaultChangeKdfService(changeKdfApiService, this.sdkService);
|
||||
const changeKdfService = new DefaultChangeKdfService(
|
||||
changeKdfApiService,
|
||||
this.sdkService,
|
||||
this.keyService,
|
||||
this.masterPasswordService,
|
||||
);
|
||||
this.encryptedMigrator = new DefaultEncryptedMigrator(
|
||||
this.kdfConfigService,
|
||||
changeKdfService,
|
||||
|
||||
Reference in New Issue
Block a user