mirror of
https://github.com/bitwarden/browser
synced 2026-02-17 09:59:41 +00:00
Merge branch 'main' into km/replace-encstring-with-unsigned-shared-key
This commit is contained in:
@@ -33,19 +33,27 @@ export class DefaultLoginComponentService implements LoginComponentService {
|
||||
*/
|
||||
async redirectToSsoLogin(email: string): Promise<void | null> {
|
||||
// Set the state that we'll need to verify the SSO login when we get the code back
|
||||
const [state, codeChallenge] = await this.setSsoPreLoginState();
|
||||
|
||||
// Set the email address in state. This is used in 2 places:
|
||||
// 1. On the web client, on the SSO component we need the email address to look up
|
||||
// the org SSO identifier. The email address is passed via query param for the other clients.
|
||||
// 2. On all clients, after authentication on the originating client the SSO component
|
||||
// will need to look up 2FA Remember token by email.
|
||||
await this.ssoLoginService.setSsoEmail(email);
|
||||
const [state, codeChallenge] = await this.setSsoPreLoginState(email);
|
||||
|
||||
// Finally, we redirect to the SSO login page. This will be handled by each client implementation of this service.
|
||||
await this.redirectToSso(email, state, codeChallenge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects the user to the SSO login page, either via route or in a new browser window.
|
||||
* @param email The email address of the user attempting to log in
|
||||
*/
|
||||
async redirectToSsoLoginWithOrganizationSsoIdentifier(
|
||||
email: string,
|
||||
orgSsoIdentifier: string,
|
||||
): Promise<void | null> {
|
||||
// Set the state that we'll need to verify the SSO login when we get the code back
|
||||
const [state, codeChallenge] = await this.setSsoPreLoginState(email);
|
||||
|
||||
// Finally, we redirect to the SSO login page. This will be handled by each client implementation of this service.
|
||||
await this.redirectToSso(email, state, codeChallenge, orgSsoIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* No-op implementation of redirectToSso
|
||||
*/
|
||||
@@ -53,6 +61,7 @@ export class DefaultLoginComponentService implements LoginComponentService {
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
@@ -65,9 +74,9 @@ export class DefaultLoginComponentService implements LoginComponentService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state required for verifying SSO login after completion
|
||||
* Set the state that we'll need to verify the SSO login when we get the authorization code back
|
||||
*/
|
||||
private async setSsoPreLoginState(): Promise<[string, string]> {
|
||||
private async setSsoPreLoginState(email: string): Promise<[string, string]> {
|
||||
// Generate SSO params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
@@ -93,6 +102,13 @@ export class DefaultLoginComponentService implements LoginComponentService {
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
|
||||
// Set the email address in state. This is used in 2 places:
|
||||
// 1. On the web client, on the SSO component we need the email address to look up
|
||||
// the org SSO identifier. The email address is passed via query param for the other clients.
|
||||
// 2. On all clients, after authentication on the originating client the SSO component
|
||||
// will need to look up 2FA Remember token by email.
|
||||
await this.ssoLoginService.setSsoEmail(email);
|
||||
|
||||
return [state, codeChallenge];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ export abstract class LoginComponentService {
|
||||
*/
|
||||
redirectToSsoLogin: (email: string) => Promise<void | null>;
|
||||
|
||||
/**
|
||||
* Redirects the user to the SSO login page with organization SSO identifier, either via route or in a new browser window.
|
||||
*/
|
||||
redirectToSsoLoginWithOrganizationSsoIdentifier: (
|
||||
email: string,
|
||||
orgSsoIdentifier: string | null | undefined,
|
||||
) => Promise<void | null>;
|
||||
|
||||
/**
|
||||
* Shows the back button.
|
||||
*/
|
||||
|
||||
@@ -381,6 +381,24 @@ export class LoginComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect to SSO if ssoOrganizationIdentifier is present in token response
|
||||
if (authResult.requiresSso) {
|
||||
const email = this.formGroup?.value?.email;
|
||||
if (!email) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("emailRequiredForSsoLogin"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.loginComponentService.redirectToSsoLoginWithOrganizationSsoIdentifier(
|
||||
email,
|
||||
authResult.ssoOrganizationIdentifier,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// User logged in successfully so execute side effects
|
||||
await this.loginSuccessHandlerService.run(authResult.userId, authResult.masterPassword);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { NewDeviceVerificationComponentService } from "./new-device-verification-component.service";
|
||||
|
||||
export class DefaultNewDeviceVerificationComponentService
|
||||
implements NewDeviceVerificationComponentService
|
||||
{
|
||||
export class DefaultNewDeviceVerificationComponentService implements NewDeviceVerificationComponentService {
|
||||
showBackButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { TwoFactorAuthWebAuthnComponentService } from "./two-factor-auth-webauthn-component.service";
|
||||
|
||||
export class DefaultTwoFactorAuthWebAuthnComponentService
|
||||
implements TwoFactorAuthWebAuthnComponentService
|
||||
{
|
||||
export class DefaultTwoFactorAuthWebAuthnComponentService implements TwoFactorAuthWebAuthnComponentService {
|
||||
/**
|
||||
* Default implementation is to not open in a new tab.
|
||||
*/
|
||||
|
||||
@@ -421,6 +421,7 @@ describe("TwoFactorAuthComponent", () => {
|
||||
keyConnectorUrl:
|
||||
mockUserDecryptionOpts.noMasterPasswordWithKeyConnector.keyConnectorOption!
|
||||
.keyConnectorUrl,
|
||||
organizationSsoIdentifier: "test-sso-id",
|
||||
}),
|
||||
);
|
||||
const authResult = new AuthResult();
|
||||
|
||||
@@ -13,6 +13,7 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide
|
||||
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentitySsoRequiredResponse } from "@bitwarden/common/auth/models/response/identity-sso-required.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
|
||||
@@ -49,7 +50,8 @@ import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||
type IdentityResponse =
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse;
|
||||
| IdentityDeviceVerificationResponse
|
||||
| IdentitySsoRequiredResponse;
|
||||
|
||||
export abstract class LoginStrategyData {
|
||||
tokenRequest:
|
||||
@@ -128,6 +130,8 @@ export abstract class LoginStrategy {
|
||||
return [await this.processTokenResponse(response), response];
|
||||
} else if (response instanceof IdentityDeviceVerificationResponse) {
|
||||
return [await this.processDeviceVerificationResponse(response), response];
|
||||
} else if (response instanceof IdentitySsoRequiredResponse) {
|
||||
return [await this.processSsoRequiredResponse(response), response];
|
||||
}
|
||||
|
||||
throw new Error("Invalid response object.");
|
||||
@@ -185,6 +189,7 @@ export abstract class LoginStrategy {
|
||||
name: accountInformation.name,
|
||||
email: accountInformation.email ?? "",
|
||||
emailVerified: accountInformation.email_verified ?? false,
|
||||
creationDate: undefined, // We don't get a creation date in the token. See https://bitwarden.atlassian.net/browse/PM-29551 for consolidation plans.
|
||||
});
|
||||
|
||||
// User env must be seeded from currently set env before switching to the account
|
||||
@@ -398,4 +403,19 @@ export abstract class LoginStrategy {
|
||||
result.requiresDeviceVerification = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the response from the server when a SSO Authentication is required.
|
||||
* It hydrates the AuthResult with the SSO organization identifier.
|
||||
*
|
||||
* @param {IdentitySsoRequiredResponse} response - The response from the server indicating that SSO is required.
|
||||
* @returns {Promise<AuthResult>} - A promise that resolves to an AuthResult object
|
||||
*/
|
||||
protected async processSsoRequiredResponse(
|
||||
response: IdentitySsoRequiredResponse,
|
||||
): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.ssoOrganizationIdentifier = response.ssoOrganizationIdentifier;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for
|
||||
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
|
||||
import { IdentitySsoRequiredResponse } from "@bitwarden/common/auth/models/response/identity-sso-required.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
@@ -165,14 +166,20 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
identityResponse:
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse,
|
||||
| IdentityDeviceVerificationResponse
|
||||
| IdentitySsoRequiredResponse,
|
||||
credentials: PasswordLoginCredentials,
|
||||
authResult: AuthResult,
|
||||
): Promise<void> {
|
||||
// TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the
|
||||
// IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse
|
||||
// If the response is a device verification response, we don't need to evaluate the password
|
||||
if (identityResponse instanceof IdentityDeviceVerificationResponse) {
|
||||
// If SSO is required, we also do not evaluate the password here, since the user needs to first
|
||||
// authenticate with their SSO IdP Provider
|
||||
if (
|
||||
identityResponse instanceof IdentityDeviceVerificationResponse ||
|
||||
identityResponse instanceof IdentitySsoRequiredResponse
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { mockAccountServiceWith, mockAccountInfoWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -79,17 +79,21 @@ describe("DefaultLockService", () => {
|
||||
);
|
||||
|
||||
it("locks the active account last", async () => {
|
||||
await accountService.addAccount(mockUser2, {
|
||||
name: "name2",
|
||||
email: "email2@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
await accountService.addAccount(
|
||||
mockUser2,
|
||||
mockAccountInfoWith({
|
||||
name: "name2",
|
||||
email: "email2@example.com",
|
||||
}),
|
||||
);
|
||||
|
||||
await accountService.addAccount(mockUser3, {
|
||||
name: "name3",
|
||||
email: "name3@example.com",
|
||||
emailVerified: false,
|
||||
});
|
||||
await accountService.addAccount(
|
||||
mockUser3,
|
||||
mockAccountInfoWith({
|
||||
name: "name3",
|
||||
email: "name3@example.com",
|
||||
}),
|
||||
);
|
||||
|
||||
const lockSpy = jest.spyOn(sut, "lock").mockResolvedValue(undefined);
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ export class SsoUrlService {
|
||||
* @param webAppUrl The URL of the web app
|
||||
* @param clientType The client type that is initiating SSO, which will drive how the response is handled
|
||||
* @param redirectUri The redirect URI or callback that will receive the SSO code after authentication
|
||||
* @param state A state value that will be peristed through the SSO flow
|
||||
* @param state A state value that will be persisted through the SSO flow
|
||||
* @param codeChallenge A challenge value that will be used to verify the SSO code after authentication
|
||||
* @param email The optional email adddress of the user initiating SSO, which will be used to look up the org SSO identifier
|
||||
* @param email The optional email address of the user initiating SSO, which will be used to look up the org SSO identifier
|
||||
* @param orgSsoIdentifier The optional SSO identifier of the org that is initiating SSO
|
||||
* @returns The URL for redirecting users to the web app SSO component
|
||||
*/
|
||||
|
||||
@@ -19,9 +19,7 @@ export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition<UserDecryptionOptio
|
||||
},
|
||||
);
|
||||
|
||||
export class UserDecryptionOptionsService
|
||||
implements InternalUserDecryptionOptionsServiceAbstraction
|
||||
{
|
||||
export class UserDecryptionOptionsService implements InternalUserDecryptionOptionsServiceAbstraction {
|
||||
constructor(private singleUserStateProvider: SingleUserStateProvider) {}
|
||||
|
||||
userDecryptionOptionsById$(userId: UserId): Observable<UserDecryptionOptions> {
|
||||
|
||||
Reference in New Issue
Block a user