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

feat(CLI-SSO-Login): [Auth/PM-21116] CLI - SSO Login - Add SSO Org Identifier option (#14605)

* Add --identifier option for SSO on CLI

* Add option for identifier

* Moved auto-submit after the setting of client arguments

* Adjusted comment

* Changed to pass in as SSO option

* Renamed to orgSsoIdentifier for clarity

* Added more changes to orgSsoIdentifier.
This commit is contained in:
Todd Martin
2025-05-13 10:58:48 -04:00
committed by GitHub
parent 0b0397c3f0
commit 4c68f61d47
5 changed files with 47 additions and 10 deletions

View File

@@ -106,6 +106,8 @@ export class LoginCommand {
return Response.badRequest("client_secret is required."); return Response.badRequest("client_secret is required.");
} }
} else if (options.sso != null && this.canInteract) { } 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 = { const passwordOptions: any = {
type: "password", type: "password",
length: 64, length: 64,
@@ -119,7 +121,7 @@ export class LoginCommand {
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
try { try {
const ssoParams = await this.openSsoPrompt(codeChallenge, state); const ssoParams = await this.openSsoPrompt(codeChallenge, state, orgSsoIdentifier);
ssoCode = ssoParams.ssoCode; ssoCode = ssoParams.ssoCode;
orgIdentifier = ssoParams.orgIdentifier; orgIdentifier = ssoParams.orgIdentifier;
} catch { } catch {
@@ -664,6 +666,7 @@ export class LoginCommand {
private async openSsoPrompt( private async openSsoPrompt(
codeChallenge: string, codeChallenge: string,
state: string, state: string,
orgSsoIdentifier: string,
): Promise<{ ssoCode: string; orgIdentifier: string }> { ): Promise<{ ssoCode: string; orgIdentifier: string }> {
const env = await firstValueFrom(this.environmentService.environment$); const env = await firstValueFrom(this.environmentService.environment$);
@@ -712,6 +715,8 @@ export class LoginCommand {
this.ssoRedirectUri, this.ssoRedirectUri,
state, state,
codeChallenge, codeChallenge,
null,
orgSsoIdentifier,
); );
this.platformUtilsService.launchUri(webAppSsoUrl); this.platformUtilsService.launchUri(webAppSsoUrl);
}); });

View File

@@ -118,7 +118,10 @@ export class Program extends BaseProgram {
.description("Log into a user account.") .description("Log into a user account.")
.option("--method <method>", "Two-step login method.") .option("--method <method>", "Two-step login method.")
.option("--code <code>", "Two-step login code.") .option("--code <code>", "Two-step login code.")
.option("--sso", "Log in with Single-Sign On.") .option(
"--sso [identifier]",
"Log in with Single-Sign On with optional organization identifier.",
)
.option("--apikey", "Log in with an Api Key.") .option("--apikey", "Log in with an Api Key.")
.option("--passwordenv <passwordenv>", "Environment variable storing your password") .option("--passwordenv <passwordenv>", "Environment variable storing your password")
.option( .option(

View File

@@ -155,7 +155,14 @@ export class SsoComponent implements OnInit {
return; return;
} }
// Detect if we have landed here but only have an SSO identifier in the URL. // 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 so, we want to initialize the SSO flow with those values.
if (this.hasParametersFromOtherClientRedirect(qParams)) {
this.initializeFromRedirectFromOtherClient(qParams);
}
// Detect if we have landed here with an SSO identifier in the URL.
// This is used by integrations that want to "short-circuit" the login to send users // 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. // directly to their IdP to simulate IdP-initiated SSO, so we submit automatically.
if (qParams.identifier != null) { if (qParams.identifier != null) {
@@ -165,13 +172,6 @@ export class SsoComponent implements OnInit {
return; return;
} }
// 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 so, we want to initialize the SSO flow with those values.
if (this.hasParametersFromOtherClientRedirect(qParams)) {
this.initializeFromRedirectFromOtherClient(qParams);
}
// Try to determine the identifier using claimed domain or local state // Try to determine the identifier using claimed domain or local state
// persisted from the user's last login attempt. // persisted from the user's last login attempt.
await this.initializeIdentifierFromEmailOrStorage(); await this.initializeIdentifierFromEmailOrStorage();

View File

@@ -92,4 +92,27 @@ describe("SsoUrlService", () => {
); );
expect(result).toBe(expectedUrl); expect(result).toBe(expectedUrl);
}); });
it("should build CLI SSO URL with Org SSO Identifier 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 orgSsoIdentifier = "test-org";
const expectedUrl = `${baseUrl}/#/sso?clientId=cli&redirectUri=${encodeURIComponent(redirectUri)}&state=${state}&codeChallenge=${codeChallenge}&email=${encodeURIComponent(email)}&identifier=${encodeURIComponent(orgSsoIdentifier)}`;
const result = service.buildSsoUrl(
baseUrl,
clientType,
redirectUri,
state,
codeChallenge,
email,
orgSsoIdentifier,
);
expect(result).toBe(expectedUrl);
});
}); });

View File

@@ -11,6 +11,7 @@ export class SsoUrlService {
* @param state A state value that will be peristed through the SSO flow * @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 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 adddress 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 * @returns The URL for redirecting users to the web app SSO component
*/ */
buildSsoUrl( buildSsoUrl(
@@ -20,6 +21,7 @@ export class SsoUrlService {
state: string, state: string,
codeChallenge: string, codeChallenge: string,
email?: string, email?: string,
orgSsoIdentifier?: string,
): string { ): string {
let url = let url =
webAppUrl + webAppUrl +
@@ -36,6 +38,10 @@ export class SsoUrlService {
url += "&email=" + encodeURIComponent(email); url += "&email=" + encodeURIComponent(email);
} }
if (orgSsoIdentifier) {
url += "&identifier=" + encodeURIComponent(orgSsoIdentifier);
}
return url; return url;
} }
} }