1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +00:00

feat(SSO): (Auth/[PM-22110] Remove Alternate Login Options when SSO Required (#16340)

If a user is part of an org that has the `RequireSso` policy, when that user successfully logs in we add their email to a local `ssoRequiredCache` on their device. The next time this user goes to the `/login` screen on this device, we will use that cache to determine that for this email we should only show the "Use single sign-on" button and disable the alternate login buttons.

These changes are behind the flag: `PM22110_DisableAlternateLoginMethods`
This commit is contained in:
rr-bw
2025-09-22 08:32:20 -07:00
committed by GitHub
parent b455cb5986
commit 3bbc6c564c
15 changed files with 539 additions and 19 deletions

View File

@@ -38,7 +38,14 @@
<div class="tw-grid tw-gap-3">
<!-- Continue button -->
<button type="button" bitButton block buttonType="primary" (click)="continuePressed()">
<button
type="button"
bitButton
block
buttonType="primary"
(click)="continuePressed()"
[disabled]="ssoRequired"
>
{{ "continue" | i18n }}
</button>
@@ -52,6 +59,7 @@
block
buttonType="secondary"
(click)="handleLoginWithPasskeyClick()"
[disabled]="ssoRequired"
>
<i class="bwi bwi-passkey tw-mr-1" aria-hidden="true"></i>
{{ "logInWithPasskey" | i18n }}

View File

@@ -1,5 +1,14 @@
import { CommonModule } from "@angular/common";
import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
import {
Component,
DestroyRef,
ElementRef,
NgZone,
OnDestroy,
OnInit,
ViewChild,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, Router, RouterModule } from "@angular/router";
import { firstValueFrom, Subject, take, takeUntil } from "rxjs";
@@ -17,9 +26,10 @@ import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.d
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ClientType, HttpStatusCode } from "@bitwarden/common/enums";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
@@ -83,6 +93,7 @@ export class LoginComponent implements OnInit, OnDestroy {
LoginUiState = LoginUiState;
isKnownDevice = false;
loginUiState: LoginUiState = LoginUiState.EMAIL_ENTRY;
ssoRequired = false;
formGroup = this.formBuilder.group(
{
@@ -108,6 +119,7 @@ export class LoginComponent implements OnInit, OnDestroy {
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
private appIdService: AppIdService,
private broadcasterService: BroadcasterService,
private destroyRef: DestroyRef,
private devicesApiService: DevicesApiServiceAbstraction,
private formBuilder: FormBuilder,
private i18nService: I18nService,
@@ -124,8 +136,8 @@ export class LoginComponent implements OnInit, OnDestroy {
private logService: LogService,
private validationService: ValidationService,
private loginSuccessHandlerService: LoginSuccessHandlerService,
private masterPasswordService: MasterPasswordServiceAbstraction,
private configService: ConfigService,
private ssoLoginService: SsoLoginServiceAbstraction,
) {
this.clientType = this.platformUtilsService.getClientType();
}
@@ -184,6 +196,15 @@ export class LoginComponent implements OnInit, OnDestroy {
if (!this.activatedRoute) {
await this.loadRememberedEmail();
}
const disableAlternateLoginMethodsFlagEnabled = await this.configService.getFeatureFlag(
FeatureFlag.PM22110_DisableAlternateLoginMethods,
);
if (disableAlternateLoginMethodsFlagEnabled) {
// This SSO required check should come after email has had a chance to be pre-filled (if it
// was found in query params or was the remembered email)
await this.determineIfSsoRequired();
}
}
private async desktopOnInit(): Promise<void> {
@@ -210,6 +231,40 @@ export class LoginComponent implements OnInit, OnDestroy {
this.messagingService.send("getWindowIsFocused");
}
private async determineIfSsoRequired() {
const ssoRequiredCache = await firstValueFrom(this.ssoLoginService.ssoRequiredCache$);
// Only perform initial update and setup a subscription if there is actually a populated ssoRequiredCache
if (ssoRequiredCache != null && ssoRequiredCache.size > 0) {
// If the pre-filled/remembered email field value exists in the cache, set to true
if (
this.emailFormControl.value &&
ssoRequiredCache.has(this.emailFormControl.value.toLowerCase())
) {
this.ssoRequired = true;
}
this.listenForEmailChanges(ssoRequiredCache);
}
}
private listenForEmailChanges(ssoRequiredCache: Set<string>) {
// On subsequent email field value changes, check and set again. This allows alternate login buttons
// to dynamically enable/disable depending on whether or not the entered email is in the ssoRequiredCache
this.formGroup.controls.email.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
if (
this.emailFormControl.value &&
ssoRequiredCache.has(this.emailFormControl.value.toLowerCase())
) {
this.ssoRequired = true;
} else {
this.ssoRequired = false;
}
});
}
submit = async (): Promise<void> => {
if (this.clientType === ClientType.Desktop) {
if (this.loginUiState !== LoginUiState.MASTER_PASSWORD_ENTRY) {

View File

@@ -8,7 +8,6 @@
<bit-label>{{ "ssoIdentifier" | i18n }}</bit-label>
<input bitInput type="text" formControlName="identifier" appAutofocus />
</bit-form-field>
<hr />
<div class="tw-flex tw-gap-2">
<button type="submit" bitButton bitFormButton buttonType="primary" [block]="true">
{{ "continue" | i18n }}

View File

@@ -290,6 +290,7 @@ export class SsoComponent implements OnInit {
this.identifier = this.identifierFormControl.value ?? "";
await this.ssoLoginService.setOrganizationSsoIdentifier(this.identifier);
this.ssoComponentService.setDocumentCookies?.();
try {
await this.submitSso();
} catch (error) {