mirror of
https://github.com/bitwarden/browser.git
synced 2025-12-05 09:14:28 +01:00
Merge c02de661a6 into d32365fbba
This commit is contained in:
commit
7f0e34ce2d
@ -102,6 +102,36 @@ describe("ExtensionLoginComponentService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
|
||||
it("launches SSO browser window with correct Url", async () => {
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const expectedState = "testState:clientId=browser";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const orgSsoIdentifier = "org-sso-identifier";
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
|
||||
|
||||
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(expectedState);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("showBackButton", () => {
|
||||
it("sets showBackButton in extensionAnonLayoutWrapperDataService", () => {
|
||||
service.showBackButton(true);
|
||||
|
||||
@ -47,6 +47,7 @@ export class ExtensionLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
@ -60,6 +61,7 @@ export class ExtensionLoginComponentService
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
this.platformUtilsService.launchUri(webAppSsoUrl);
|
||||
|
||||
@ -113,20 +113,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 +225,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 +307,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,6 +356,7 @@ 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.");
|
||||
}
|
||||
@ -692,6 +725,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,
|
||||
|
||||
@ -136,6 +136,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
undefined,
|
||||
);
|
||||
} else {
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
@ -145,4 +146,55 @@ describe("DesktopLoginComponentService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("redirectToSsoLoginWithOrganizationSsoIdentifier", () => {
|
||||
// Array of all permutations of isAppImage and isDev
|
||||
const permutations = [
|
||||
[true, false], // Case 1: isAppImage true
|
||||
[false, true], // Case 2: isDev true
|
||||
[true, true], // Case 3: all true
|
||||
[false, false], // Case 4: all false
|
||||
];
|
||||
|
||||
permutations.forEach(([isAppImage, isDev]) => {
|
||||
it("calls redirectToSso with orgSsoIdentifier", async () => {
|
||||
(global as any).ipc.platform.isAppImage = isAppImage;
|
||||
(global as any).ipc.platform.isDev = isDev;
|
||||
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const orgSsoIdentifier = "orgSsoId";
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.redirectToSsoLoginWithOrganizationSsoIdentifier(email, orgSsoIdentifier);
|
||||
|
||||
if (isAppImage || isDev) {
|
||||
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
} else {
|
||||
expect(ssoUrlService.buildSsoUrl).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
expect.any(String),
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -48,11 +48,12 @@ export class DesktopLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
// For platforms that cannot support a protocol-based (e.g. bitwarden://) callback, we use a localhost callback
|
||||
// Otherwise, we launch the SSO component in a browser window and wait for the callback
|
||||
if (ipc.platform.isAppImage || ipc.platform.isDev) {
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge);
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge, orgSsoIdentifier);
|
||||
} else {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
@ -66,6 +67,7 @@ export class DesktopLoginComponentService
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
this.platformUtilsService.launchUri(ssoWebAppUrl);
|
||||
@ -76,9 +78,15 @@ export class DesktopLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
challenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(challenge, state, email);
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(
|
||||
challenge,
|
||||
state,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
|
||||
@ -108,8 +108,13 @@ const ephemeralStore = {
|
||||
};
|
||||
|
||||
const localhostCallbackService = {
|
||||
openSsoPrompt: (codeChallenge: string, state: string, email: string): Promise<void> => {
|
||||
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email });
|
||||
openSsoPrompt: (
|
||||
codeChallenge: string,
|
||||
state: string,
|
||||
email: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> => {
|
||||
return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state, email, orgSsoIdentifier });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -25,20 +25,25 @@ export class SSOLocalhostCallbackService {
|
||||
private messagingService: MessageSender,
|
||||
private ssoUrlService: SsoUrlService,
|
||||
) {
|
||||
ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state, email }) => {
|
||||
// Close any existing server before starting new one
|
||||
if (this.currentServer) {
|
||||
await this.closeCurrentServer();
|
||||
}
|
||||
ipcMain.handle(
|
||||
"openSsoPrompt",
|
||||
async (event, { codeChallenge, state, email, orgSsoIdentifier }) => {
|
||||
// Close any existing server before starting new one
|
||||
if (this.currentServer) {
|
||||
await this.closeCurrentServer();
|
||||
}
|
||||
|
||||
return this.openSsoPrompt(codeChallenge, state, email).then(({ ssoCode, recvState }) => {
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
});
|
||||
});
|
||||
});
|
||||
return this.openSsoPrompt(codeChallenge, state, email, orgSsoIdentifier).then(
|
||||
({ ssoCode, recvState }) => {
|
||||
this.messagingService.send("ssoCallback", {
|
||||
code: ssoCode,
|
||||
state: recvState,
|
||||
redirectUri: this.ssoRedirectUri,
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async closeCurrentServer(): Promise<void> {
|
||||
@ -58,6 +63,7 @@ export class SSOLocalhostCallbackService {
|
||||
codeChallenge: string,
|
||||
state: string,
|
||||
email: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<{ ssoCode: string; recvState: string }> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
|
||||
@ -121,6 +127,7 @@ export class SSOLocalhostCallbackService {
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
orgSsoIdentifier,
|
||||
);
|
||||
|
||||
// Set up error handler before attempting to listen
|
||||
|
||||
@ -61,8 +61,11 @@ export class WebLoginComponentService
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
orgSsoIdentifier?: string,
|
||||
): Promise<void> {
|
||||
await this.router.navigate(["/sso"]);
|
||||
await this.router.navigate(["/sso"], {
|
||||
queryParams: { identifier: orgSsoIdentifier },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -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,20 @@ 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.logService.error("Email is required for SSO");
|
||||
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);
|
||||
|
||||
|
||||
@ -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.");
|
||||
@ -398,4 +402,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,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
|
||||
*/
|
||||
|
||||
@ -50,6 +50,7 @@ import { UpdateProfileRequest } from "../auth/models/request/update-profile.requ
|
||||
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentitySsoRequiredResponse } from "../auth/models/response/identity-sso-required.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
@ -140,7 +141,10 @@ export abstract class ApiService {
|
||||
| UserApiTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
): Promise<
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
| IdentitySsoRequiredResponse
|
||||
>;
|
||||
abstract refreshIdentityToken(userId?: UserId): Promise<any>;
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
||||
|
||||
@ -18,10 +20,16 @@ export class AuthResult {
|
||||
email: string;
|
||||
requiresEncryptionKeyMigration: boolean;
|
||||
requiresDeviceVerification: boolean;
|
||||
ssoOrganizationIdentifier?: string | null;
|
||||
// The master-password used in the authentication process
|
||||
masterPassword: string | null;
|
||||
|
||||
get requiresTwoFactor() {
|
||||
return this.twoFactorProviders != null;
|
||||
}
|
||||
|
||||
// This is not as extensible as an object-based approach. In the future we may need to adjust to an object based approach.
|
||||
get requiresSso() {
|
||||
return !Utils.isNullOrWhitespace(this.ssoOrganizationIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class IdentitySsoRequiredResponse extends BaseResponse {
|
||||
ssoOrganizationIdentifier: string | null;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.ssoOrganizationIdentifier = this.getResponseProperty("SsoOrganizationIdentifier");
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,7 @@ import { UpdateProfileRequest } from "../auth/models/request/update-profile.requ
|
||||
import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentitySsoRequiredResponse } from "../auth/models/response/identity-sso-required.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
@ -165,7 +166,10 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
): Promise<
|
||||
IdentityTokenResponse | IdentityTwoFactorResponse | IdentityDeviceVerificationResponse
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
| IdentitySsoRequiredResponse
|
||||
> {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
@ -212,6 +216,8 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE
|
||||
) {
|
||||
return new IdentityDeviceVerificationResponse(responseJson);
|
||||
} else if (response.status === 400 && responseJson?.SsoOrganizationIdentifier) {
|
||||
return new IdentitySsoRequiredResponse(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user