1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-17751] Store SSO email in state on web client (#13295)

* Moved saving of SSO email outside of browser/desktop code

* Clarified comments.

* Tests

* Refactored login component services to manage state

* Fixed input on login component

* Fixed tests

* Linting

* Moved web setting in state into web override

* updated tests

* Fixed typing.

* Fixed type safety issues.

* Added comments and renamed for clarity.

* Removed method parameters that weren't used

* Added clarifying comments

* Added more comments.

* Removed test that is not necessary on base

* Test cleanup

* More comments.

* Linting

* Fixed test.

* Fixed base URL

* Fixed typechecking.

* Type checking

* Moved setting of email state to default service

* Added comments.

* Consolidated SSO URL formatting

* Updated comment

* Fixed reference.

* Fixed missing parameter.

* Initialized service.

* Added comments

* Added initialization of new service

* Made email optional due to CLI.

* Fixed comment on handleSsoClick.

* Added SSO email persistence to v1 component.

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
Todd Martin
2025-02-21 17:09:50 -05:00
committed by GitHub
parent 9dd2033081
commit 077e0f89cc
26 changed files with 534 additions and 245 deletions

View File

@@ -68,50 +68,21 @@ describe("DefaultLoginComponentService", () => {
});
});
describe("launchSsoBrowserWindow", () => {
const email = "test@bitwarden.com";
let state = "testState";
const codeVerifier = "testCodeVerifier";
const codeChallenge = "testCodeChallenge";
const baseUrl = "https://webvault.bitwarden.com/#/sso";
beforeEach(() => {
state = "testState";
describe("redirectToSsoLogin", () => {
it("sets the pre-SSO state", async () => {
const email = "test@bitwarden.com";
const state = "testState";
const codeVerifier = "testCodeVerifier";
const codeChallenge = "testCodeChallenge";
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
await service.redirectToSsoLogin(email);
expect(ssoLoginService.setSsoEmail).toHaveBeenCalledWith(email);
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
});
it.each([
{
clientType: ClientType.Browser,
clientId: "browser",
expectedRedirectUri: "https://webvault.bitwarden.com/sso-connector.html",
},
{
clientType: ClientType.Desktop,
clientId: "desktop",
expectedRedirectUri: "bitwarden://sso-callback",
},
])(
"launches SSO browser window with correct URL for $clientId client",
async ({ clientType, clientId, expectedRedirectUri }) => {
service["clientType"] = clientType;
await service.launchSsoBrowserWindow(email, clientId as "browser" | "desktop");
if (clientType === ClientType.Browser) {
state += ":clientId=browser";
}
const expectedUrl = `${baseUrl}?clientId=${clientId}&redirectUri=${encodeURIComponent(expectedRedirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}`;
expect(ssoLoginService.setSsoEmail).toHaveBeenCalledWith(email);
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
expect(platformUtilsService.launchUri).toHaveBeenCalledWith(expectedUrl);
},
);
});
});

View File

@@ -1,7 +1,5 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { LoginComponentService } from "@bitwarden/auth/angular";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
@@ -21,19 +19,55 @@ export class DefaultLoginComponentService implements LoginComponentService {
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected platformUtilsService: PlatformUtilsService,
protected ssoLoginService: SsoLoginServiceAbstraction,
) {}
) {
this.clientType = this.platformUtilsService.getClientType();
}
isLoginWithPasskeySupported(): boolean {
return this.clientType === ClientType.Web;
}
async launchSsoBrowserWindow(
email: string,
clientId: "browser" | "desktop",
): Promise<void | null> {
// Save email for SSO
/**
* 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 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);
// 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);
}
/**
* No-op implementation of redirectToSso
*/
protected async redirectToSso(
email: string,
state: string,
codeChallenge: string,
): Promise<void> {
return;
}
/**
* No-op implementation of showBackButton
*/
showBackButton(showBackButton: boolean): void {
return;
}
/**
* Sets the state required for verifying SSO login after completion
*/
private async setSsoPreLoginState(): Promise<[string, string]> {
// Generate SSO params
const passwordOptions: any = {
type: "password",
@@ -46,8 +80,8 @@ export class DefaultLoginComponentService implements LoginComponentService {
let state = await this.passwordGenerationService.generatePassword(passwordOptions);
if (clientId === "browser") {
// Need to persist the clientId in the state for the extension
// For the browser extension, we persist the clientId on state so that it will be included after SSO in the callback
if (this.clientType === ClientType.Browser) {
state += ":clientId=browser";
}
@@ -59,35 +93,6 @@ export class DefaultLoginComponentService implements LoginComponentService {
await this.ssoLoginService.setSsoState(state);
await this.ssoLoginService.setCodeVerifier(codeVerifier);
// Build URL
const env = await firstValueFrom(this.environmentService.environment$);
const webVaultUrl = env.getWebVaultUrl();
const redirectUri =
clientId === "browser"
? webVaultUrl + "/sso-connector.html" // Browser
: "bitwarden://sso-callback"; // Desktop
// Launch browser window with URL
this.platformUtilsService.launchUri(
webVaultUrl +
"/#/sso?clientId=" +
clientId +
"&redirectUri=" +
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge +
"&email=" +
encodeURIComponent(email),
);
}
/**
* No-op implementation of showBackButton
*/
showBackButton(showBackButton: boolean): void {
return;
return [state, codeChallenge];
}
}

View File

@@ -31,10 +31,9 @@ export abstract class LoginComponentService {
isLoginWithPasskeySupported: () => boolean;
/**
* Launches the SSO flow in a new browser window.
* - Used by: Browser, Desktop
* Redirects the user to the SSO login page, either via route or in a new browser window.
*/
launchSsoBrowserWindow: (email: string, clientId: "browser" | "desktop") => Promise<void>;
redirectToSsoLogin: (email: string) => Promise<void | null>;
/**
* Shows the back button.

View File

@@ -318,15 +318,6 @@ export class LoginComponent implements OnInit, OnDestroy {
}
}
protected async launchSsoBrowserWindow(clientId: "browser" | "desktop"): Promise<void> {
const email = this.emailFormControl.value;
if (!email) {
this.logService.error("Email is required for SSO login");
return;
}
await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
}
/**
* Checks if the master password meets the enforced policy requirements
* and if the user is required to change their password.
@@ -636,26 +627,25 @@ export class LoginComponent implements OnInit, OnDestroy {
/**
* Handle the SSO button click.
* @param event - The event object.
*/
async handleSsoClick() {
const isEmailValid = await this.validateEmail();
// Make sure the email is not empty, for type safety
const email = this.formGroup.value.email;
if (!email) {
this.logService.error("Email is required for SSO");
return;
}
// Make sure the email is valid
const isEmailValid = await this.validateEmail();
if (!isEmailValid) {
return;
}
// Save the email configuration for the login component
await this.saveEmailSettings();
if (this.clientType === ClientType.Web) {
await this.router.navigate(["/sso"], {
queryParams: { email: this.formGroup.value.email },
});
return;
}
await this.launchSsoBrowserWindow(
this.clientType === ClientType.Browser ? "browser" : "desktop",
);
// Send the user to SSO, either through routing or through redirecting to the web app
await this.loginComponentService.redirectToSsoLogin(email);
}
}

View File

@@ -89,6 +89,7 @@ export class SsoComponent implements OnInit {
protected state: string | undefined;
protected codeChallenge: string | undefined;
protected clientId: SsoClientType | undefined;
protected email: string | null | undefined;
formPromise: Promise<AuthResult> | undefined;
initiateSsoFormPromise: Promise<SsoPreValidateResponse> | undefined;
@@ -129,38 +130,58 @@ export class SsoComponent implements OnInit {
}
}
/**
* Like several components in our app (e.g. our invite acceptance components), the SSO component is engaged both
* before and after the user authenticates.
* Flow 1: Initialize SSO state and redirect to IdP
* - We can get here several ways:
* - The user is on the web client and is routed here
* - The user is on a different client and is redirected by opening a new browser window, passing query params
* - A customer integration has been set up to direct users to the `/sso` route to initiate SSO with an identifier
* Flow 2: Handle callback from IdP and verify the state that was set pre-authentication
*/
async ngOnInit() {
const qParams: QueryParams = await firstValueFrom(this.route.queryParams);
// This if statement will pass on the second portion of the SSO flow
// SSO on web uses a service to provide the email via state that's set on login,
// but because we have clients that delegate SSO to web we have to accept the email in the query params as well.
// We also can't require the email, because it isn't provided in the CLI SSO flow.
this.email = qParams.email ?? (await this.ssoLoginService.getSsoEmail());
// Detect if we are on the second portion of the SSO flow,
// where the user has already authenticated with the identity provider
if (this.hasCodeOrStateParams(qParams)) {
await this.handleCodeAndStateParams(qParams);
if (this.userCompletedSsoAuthentication(qParams)) {
await this.handleTokenRequestForAuthenticatedUser(qParams);
return;
}
// This if statement will pass on the first portion of the SSO flow
if (this.hasRequiredSsoParams(qParams)) {
this.setRequiredSsoVariables(qParams);
// Detect if we are on the first portion of the SSO flow
// and have been sent here from another client with the info in query params
if (this.hasParametersFromOtherClientRedirect(qParams)) {
this.initializeFromRedirectFromOtherClient(qParams);
return;
}
// Detect if we have landed here but only have an SSO identifier in the URL.
// This is used by integrations that want to "short-circuit" the login to send users
// directly to their IdP to simulate IdP-initiated SSO, so we submit automatically.
if (qParams.identifier != null) {
// SSO Org Identifier in query params takes precedence over claimed domains
this.identifierFormControl.setValue(qParams.identifier);
this.loggingIn = true;
await this.submit();
return;
}
await this.initializeIdentifierFromEmailOrStorage(qParams);
// If we're routed here with no additional parameters, we'll try to determine the
// identifier using claimed domain or local state saved from their last attempt.
await this.initializeIdentifierFromEmailOrStorage();
}
/**
* Sets the required SSO variables from the query params
* @param qParams - The query params
*/
private setRequiredSsoVariables(qParams: QueryParams): void {
private initializeFromRedirectFromOtherClient(qParams: QueryParams): void {
this.redirectUri = qParams.redirectUri ?? "";
this.state = qParams.state ?? "";
this.codeChallenge = qParams.codeChallenge ?? "";
@@ -182,11 +203,16 @@ export class SsoComponent implements OnInit {
}
/**
* Checks if the query params have the required SSO params
* Checks if the query params have the required SSO params to initiate SSO
* * The query params presented here are:
* - clientId: The client type (e.g. web, browser, desktop)
* - redirectUri: The URI to redirect to after authentication
* - state: The state to verify on the client after authentication
* - codeChallenge: The PKCE code challenge that is sent up when authenticating with the IdP
* @param qParams - The query params
* @returns True if the query params have the required SSO params, false otherwise
*/
private hasRequiredSsoParams(qParams: QueryParams): boolean {
private hasParametersFromOtherClientRedirect(qParams: QueryParams): boolean {
return (
qParams.clientId != null &&
qParams.redirectUri != null &&
@@ -196,12 +222,18 @@ export class SsoComponent implements OnInit {
}
/**
* Handles the code and state params
* Handles the case in which the user has completed SSO authentication, has a code
* and has been redirected back to the SSO component to exchange the code for a token.
* This will be on the client originating the SSO request, not always the web client, as that
* is where the state and verifier are stored.
* @param qParams - The query params
*/
private async handleCodeAndStateParams(qParams: QueryParams): Promise<void> {
private async handleTokenRequestForAuthenticatedUser(qParams: QueryParams): Promise<void> {
// We set these in state prior to starting SSO, so we can retrieve them here
const codeVerifier = await this.ssoLoginService.getCodeVerifier();
const state = await this.ssoLoginService.getSsoState();
const stateFromPrelogin = await this.ssoLoginService.getSsoState();
// Reset the code verifier and state so we don't accidentally use them again
await this.ssoLoginService.setCodeVerifier("");
await this.ssoLoginService.setSsoState("");
@@ -209,11 +241,13 @@ export class SsoComponent implements OnInit {
this.redirectUri = qParams.redirectUri;
}
// Verify that the state matches the state we set prior to starting SSO.
// If it does, we can proceed with exchanging the code for a token.
if (
qParams.code != null &&
codeVerifier != null &&
state != null &&
this.checkState(state, qParams.state ?? "")
stateFromPrelogin != null &&
this.verifyStateMatches(stateFromPrelogin, qParams.state ?? "")
) {
const ssoOrganizationIdentifier = this.getOrgIdentifierFromState(qParams.state ?? "");
await this.logIn(qParams.code, codeVerifier, ssoOrganizationIdentifier);
@@ -221,11 +255,12 @@ export class SsoComponent implements OnInit {
}
/**
* Checks if the query params have a code or state
* Checks if the query params have a code and state, indicating that we've completed SSO authentication
* and have been redirected back to the SSO component on the originating client to complete login.
* @param qParams - The query params
* @returns True if the query params have a code or state, false otherwise
* @returns True if the query params have a code and state, false otherwise
*/
private hasCodeOrStateParams(qParams: QueryParams): boolean {
private userCompletedSsoAuthentication(qParams: QueryParams): boolean {
return qParams.code != null && qParams.state != null;
}
@@ -265,6 +300,11 @@ export class SsoComponent implements OnInit {
}
};
/**
* Redirects the user to `/connect/authorize` on IdentityServer to begin SSO.
* @param returnUri - The URI to redirect to after authentication (used to link user to SSO)
* @param includeUserIdentifier - Whether to include the user identifier in the request (used to link user to SSO)
*/
private async submitSso(returnUri?: string, includeUserIdentifier?: boolean) {
if (this.identifier == null || this.identifier === "") {
this.toastService.showToast({
@@ -307,6 +347,9 @@ export class SsoComponent implements OnInit {
special: false,
};
// Initialize the challenge and state if they aren't passed in. If we're performing SSO initiated on a
// different client, they'll be passed in, as they will need to be verified on that client and not the web.
// If they're not passed in, then we need to set them here on the web client to be verified here after SSO.
if (codeChallenge == null) {
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
@@ -316,15 +359,20 @@ export class SsoComponent implements OnInit {
if (state == null) {
state = await this.passwordGenerationService.generatePassword(passwordOptions);
if (returnUri) {
state += `_returnUri='${returnUri}'`;
}
}
// If we have a returnUri, add it to the state parameter. This will be used after SSO
// is complete, on the sso-connector, in order to route the user somewhere other than the SSO component.
if (returnUri) {
state += `_returnUri='${returnUri}'`;
}
// Add Organization Identifier to state
state += `_identifier=${this.identifier}`;
// Save state (regardless of new or existing)
// Save the pre-SSO state.
// We need to do this here as even if it was generated on the intiating client (e.g. browser, desktop),
// we need it on the web client to verify after the user authenticates with the identity provider and is redirected back.
await this.ssoLoginService.setSsoState(state);
const env = await firstValueFrom(this.environmentService.environment$);
@@ -349,6 +397,8 @@ export class SsoComponent implements OnInit {
"&ssoToken=" +
encodeURIComponent(token ?? "");
// If we're linking a user to SSO, we need to provide a user identifier that will be passed
// on to the SSO provider so that after SSO we can link the user to the SSO identity.
if (includeUserIdentifier) {
const userIdentifier = await this.apiService.getSsoUserIdentifier();
authorizeUrl += `&user_identifier=${encodeURIComponent(userIdentifier)}`;
@@ -357,17 +407,23 @@ export class SsoComponent implements OnInit {
return authorizeUrl;
}
/**
* We are using the Auth Code + PKCE flow.
* We have received the code from IdentityServer, which we will now present with the code verifier to get a token.
*/
private async logIn(code: string, codeVerifier: string, orgSsoIdentifier: string): Promise<void> {
this.loggingIn = true;
try {
const email = await this.ssoLoginService.getSsoEmail();
// The code verifier is used to ensure that the client presenting the code is the same one that initiated the authentication request.
// The redirect URI is also supplied on the request to the token endpoint, so the server can ensure it matches the original request
// for the code and prevent authorization code injection attacks.
const redirectUri = this.redirectUri ?? "";
const credentials = new SsoLoginCredentials(
code,
codeVerifier,
redirectUri,
orgSsoIdentifier,
email ?? undefined,
this.email ?? undefined,
);
this.formPromise = this.loginStrategyService.logIn(credentials);
const authResult = await this.formPromise;
@@ -524,16 +580,22 @@ export class SsoComponent implements OnInit {
return stateSplit.length > 1 ? stateSplit[1] : "";
}
private checkState(state: string, checkState: string): boolean {
if (state === null || state === undefined) {
/**
* Checks if the state matches the checkState
* @param originalStateValue - The state to check
* @param stateValueToCheck - The state to check against
* @returns True if the state matches the checkState, false otherwise
*/
private verifyStateMatches(originalStateValue: string, stateValueToCheck: string): boolean {
if (originalStateValue === null || originalStateValue === undefined) {
return false;
}
if (checkState === null || checkState === undefined) {
if (stateValueToCheck === null || stateValueToCheck === undefined) {
return false;
}
const stateSplit = state.split("_identifier=");
const checkStateSplit = checkState.split("_identifier=");
const stateSplit = originalStateValue.split("_identifier=");
const checkStateSplit = stateValueToCheck.split("_identifier=");
return stateSplit[0] === checkStateSplit[0];
}
@@ -541,17 +603,16 @@ export class SsoComponent implements OnInit {
* Attempts to initialize the SSO identifier from email or storage.
* Note: this flow is written for web but both browser and desktop
* redirect here on SSO button click.
* @param qParams - The query params
*/
private async initializeIdentifierFromEmailOrStorage(qParams: QueryParams): Promise<void> {
// Check if email matches any claimed domains
if (qParams.email) {
private async initializeIdentifierFromEmailOrStorage(): Promise<void> {
if (this.email) {
// show loading spinner
this.loggingIn = true;
try {
// Check if email matches any claimed domains
if (await this.configService.getFeatureFlag(FeatureFlag.VerifiedSsoDomainEndpoint)) {
const response: ListResponse<VerifiedOrganizationDomainSsoDetailsResponse> =
await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(qParams.email);
await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(this.email);
if (response.data.length > 0) {
this.identifierFormControl.setValue(response.data[0].organizationIdentifier);
@@ -560,7 +621,7 @@ export class SsoComponent implements OnInit {
}
} else {
const response: OrganizationDomainSsoDetailsResponse =
await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email);
await this.orgDomainApiService.getClaimedOrgDomainByEmail(this.email);
if (response?.ssoAvailable && response?.verifiedDate) {
this.identifierFormControl.setValue(response.organizationIdentifier);
@@ -575,7 +636,8 @@ export class SsoComponent implements OnInit {
this.loggingIn = false;
}
// Fallback to state svc if domain is unclaimed
// If we don't find a claimed domain, check to see if we stored an identifier in state
// from their last attrempt to login via SSO. If so, we'll populate the field, but not submit.
const storedIdentifier = await this.ssoLoginService.getOrganizationSsoIdentifier();
if (storedIdentifier != null) {
this.identifierFormControl.setValue(storedIdentifier);

View File

@@ -6,3 +6,4 @@ export * from "./auth-request/auth-request.service";
export * from "./auth-request/auth-request-api.service";
export * from "./accounts/lock.service";
export * from "./login-success-handler/default-login-success-handler.service";
export * from "./sso-redirect/sso-url.service";

View File

@@ -0,0 +1,95 @@
import { ClientType } from "@bitwarden/common/enums";
import { DESKTOP_SSO_CALLBACK, SsoUrlService } from "./sso-url.service";
describe("SsoUrlService", () => {
let service: SsoUrlService;
beforeEach(() => {
service = new SsoUrlService();
});
it("should build Desktop SSO URL correctly", () => {
const baseUrl = "https://web-vault.bitwarden.com";
const clientType = ClientType.Desktop;
const redirectUri = DESKTOP_SSO_CALLBACK;
const state = "abc123";
const codeChallenge = "xyz789";
const email = "test@bitwarden.com";
const expectedUrl = `${baseUrl}/#/sso?clientId=desktop&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}`;
const result = service.buildSsoUrl(
baseUrl,
clientType,
redirectUri,
state,
codeChallenge,
email,
);
expect(result).toBe(expectedUrl);
});
it("should build Desktop localhost callback SSO URL correctly", () => {
const baseUrl = "https://web-vault.bitwarden.com";
const clientType = ClientType.Desktop;
const redirectUri = `https://localhost:1000`;
const state = "abc123";
const codeChallenge = "xyz789";
const email = "test@bitwarden.com";
const expectedUrl = `${baseUrl}/#/sso?clientId=desktop&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}`;
const result = service.buildSsoUrl(
baseUrl,
clientType,
redirectUri,
state,
codeChallenge,
email,
);
expect(result).toBe(expectedUrl);
});
it("should build Extension SSO URL correctly", () => {
const baseUrl = "https://web-vault.bitwarden.com";
const clientType = ClientType.Browser;
const redirectUri = baseUrl + "/sso-connector.html";
const state = "abc123";
const codeChallenge = "xyz789";
const email = "test@bitwarden.com";
const expectedUrl = `${baseUrl}/#/sso?clientId=browser&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}`;
const result = service.buildSsoUrl(
baseUrl,
clientType,
redirectUri,
state,
codeChallenge,
email,
);
expect(result).toBe(expectedUrl);
});
it("should build CLI SSO URL correctly", () => {
const baseUrl = "https://web-vault.bitwarden.com";
const clientType = ClientType.Cli;
const redirectUri = "https://localhost:1000";
const state = "abc123";
const codeChallenge = "xyz789";
const email = "test@bitwarden.com";
const expectedUrl = `${baseUrl}/#/sso?clientId=cli&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}`;
const result = service.buildSsoUrl(
baseUrl,
clientType,
redirectUri,
state,
codeChallenge,
email,
);
expect(result).toBe(expectedUrl);
});
});

View File

@@ -0,0 +1,41 @@
import { ClientType } from "@bitwarden/common/enums";
export const DESKTOP_SSO_CALLBACK: string = "bitwarden://sso-callback";
export class SsoUrlService {
/**
* Builds a URL for redirecting users to the web app SSO component to complete SSO
* @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 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
* @returns The URL for redirecting users to the web app SSO component
*/
buildSsoUrl(
webAppUrl: string,
clientType: ClientType,
redirectUri: string,
state: string,
codeChallenge: string,
email?: string,
): string {
let url =
webAppUrl +
"/#/sso?clientId=" +
clientType +
"&redirectUri=" +
encodeURIComponent(redirectUri) +
"&state=" +
state +
"&codeChallenge=" +
codeChallenge;
if (email) {
url += "&email=" + encodeURIComponent(email);
}
return url;
}
}