Skip to content

Commit a77b038

Browse files
dan-redcupitclaude
andcommitted
feat: add multi-profile support for managing multiple Okta organizations
This feature allows MSPs, MSSPs, and developers to manage credentials for multiple Okta organizations using named profiles, similar to AWS CLI profiles. New features: - `okta login --profile-name <name>` to create/update named profiles - `okta profiles list` to list all configured profiles - `okta profiles use <name>` to switch the active profile - `okta profiles show [name]` to display profile details - `okta profiles delete <name>` to remove a profile - `okta --profile <name> <command>` for one-off commands with a specific profile - `OKTA_CLI_PROFILE` environment variable support Configuration format: ```yaml okta: profiles: default: orgUrl: https://dev-123456.okta.com token: 00abc... acme-corp: orgUrl: https://acme.okta.com token: 00xyz... activeProfile: default ``` Backward compatibility: - Automatically migrates legacy single-profile format on first use - Legacy format continues to be readable without migration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6ef680c commit a77b038

13 files changed

Lines changed: 1218 additions & 15 deletions

File tree

cli/src/main/java/com/okta/cli/Environment.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@
1515
*/
1616
package com.okta.cli;
1717

18+
import com.okta.cli.common.model.OktaProfile;
19+
import com.okta.cli.common.service.DefaultProfileConfigurationService;
20+
import com.okta.cli.common.service.ProfileConfigurationService;
1821
import com.okta.cli.console.ConsoleOutput;
1922
import com.okta.cli.console.DefaultPrompter;
2023
import com.okta.cli.console.DisabledPrompter;
2124
import com.okta.cli.console.Prompter;
2225

2326
import java.io.File;
27+
import java.io.IOException;
28+
import java.util.Optional;
2429

2530
public class Environment {
2631

@@ -38,6 +43,10 @@ public class Environment {
3843

3944
private boolean consoleColors = true;
4045

46+
private String profile = null;
47+
48+
private boolean profileActivated = false;
49+
4150
public boolean isInteractive() {
4251
return interactive;
4352
}
@@ -68,6 +77,81 @@ public File getOktaPropsFile() {
6877
return oktaPropsFile;
6978
}
7079

80+
/**
81+
* Gets the currently selected profile name.
82+
* Returns the profile set via --profile flag, or the active profile from config,
83+
* or "default" if no profile is configured.
84+
*
85+
* @return the profile name
86+
*/
87+
public String getProfile() {
88+
if (profile != null) {
89+
return profile;
90+
}
91+
92+
// Check environment variable
93+
String envProfile = System.getenv("OKTA_CLI_PROFILE");
94+
if (envProfile != null && !envProfile.isEmpty()) {
95+
return envProfile;
96+
}
97+
98+
// Get active profile from config file
99+
try {
100+
ProfileConfigurationService profileService = new DefaultProfileConfigurationService();
101+
return profileService.getActiveProfileName(oktaPropsFile);
102+
} catch (IOException e) {
103+
return ProfileConfigurationService.DEFAULT_PROFILE_NAME;
104+
}
105+
}
106+
107+
/**
108+
* Sets the profile to use for this session.
109+
* This overrides the active profile from configuration.
110+
*
111+
* @param profile the profile name
112+
* @return this Environment for chaining
113+
*/
114+
public Environment setProfile(String profile) {
115+
this.profile = profile;
116+
this.profileActivated = false; // Reset so profile will be activated on next SDK call
117+
return this;
118+
}
119+
120+
/**
121+
* Activates the current profile by setting system properties for the Okta SDK.
122+
* This method is idempotent - calling it multiple times has no additional effect.
123+
*
124+
* @throws IllegalStateException if the profile does not exist or cannot be loaded
125+
*/
126+
public void activateProfile() {
127+
if (profileActivated) {
128+
return;
129+
}
130+
131+
ProfileConfigurationService profileService = new DefaultProfileConfigurationService();
132+
133+
try {
134+
// Migrate legacy format if needed
135+
if (((DefaultProfileConfigurationService) profileService).isLegacyFormat(oktaPropsFile)) {
136+
((DefaultProfileConfigurationService) profileService).migrateFromLegacyFormat(oktaPropsFile);
137+
}
138+
139+
String profileName = getProfile();
140+
Optional<OktaProfile> profileOpt = profileService.getProfile(oktaPropsFile, profileName);
141+
142+
if (profileOpt.isPresent()) {
143+
profileService.activateProfileForSdk(profileOpt.get());
144+
profileActivated = true;
145+
}
146+
// If profile doesn't exist, let the SDK handle the error (might be using env vars)
147+
} catch (IOException e) {
148+
// If we can't read the config, let the SDK try its default behavior
149+
if (verbose) {
150+
System.err.println("Warning: Could not load profile configuration: " + e.getMessage());
151+
}
152+
}
153+
}
154+
71155
public boolean isDemo() {
72156
return Boolean.parseBoolean(System.getenv("OKTA_CLI_DEMO"));
73157
}

cli/src/main/java/com/okta/cli/OktaCli.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.okta.cli.commands.Register;
2222
import com.okta.cli.commands.Start;
2323
import com.okta.cli.commands.apps.Apps;
24+
import com.okta.cli.commands.profiles.Profiles;
2425
import com.okta.commons.lang.ApplicationInfo;
2526
import com.okta.sdk.resource.ResourceException;
2627
import picocli.AutoComplete;
@@ -43,6 +44,7 @@
4344
Register.class,
4445
Login.class,
4546
Apps.class,
47+
Profiles.class,
4648
Start.class,
4749
Logs.class,
4850
DumpCommand.class,
@@ -166,6 +168,11 @@ public void setSystemProperties(List<String> props) {
166168
}
167169
}
168170

171+
@Option(names = {"-p", "--profile"}, description = "Use a specific Okta profile. Overrides the active profile from configuration.")
172+
public void setProfile(String profile) {
173+
environment.setProfile(profile);
174+
}
175+
169176
public Environment getEnvironment() {
170177
return environment;
171178
}

cli/src/main/java/com/okta/cli/commands/BaseCommand.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ public BaseCommand(OktaCli.StandardOptions standardOptions) {
3838

3939
@Override
4040
public Integer call() throws Exception {
41+
// Activate the selected profile before running the command
42+
// This sets system properties so the Okta SDK uses the correct credentials
43+
getEnvironment().activateProfile();
4144
return runCommand();
4245
}
4346

cli/src/main/java/com/okta/cli/commands/Login.java

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,42 +15,87 @@
1515
*/
1616
package com.okta.cli.commands;
1717

18-
import com.okta.cli.common.service.DefaultSdkConfigurationService;
19-
import com.okta.cli.common.service.SdkConfigurationService;
18+
import com.okta.cli.common.model.OktaProfile;
19+
import com.okta.cli.common.service.DefaultProfileConfigurationService;
20+
import com.okta.cli.common.service.ProfileConfigurationService;
2021
import com.okta.cli.console.ConsoleOutput;
2122
import com.okta.commons.configcheck.ConfigurationValidator;
22-
import com.okta.commons.lang.Strings;
23-
import com.okta.sdk.impl.config.ClientConfiguration;
2423
import picocli.CommandLine;
2524

25+
import java.io.File;
26+
import java.util.Optional;
27+
2628
@CommandLine.Command(name = "login",
2729
description = "Authorizes the Okta CLI tool")
2830
public class Login extends BaseCommand {
2931

32+
@CommandLine.Option(names = {"--profile-name"}, description = "Name for this profile (e.g., 'acme-corp', 'dev-tenant')")
33+
private String profileName;
34+
3035
@Override
3136
public int runCommand() throws Exception {
3237

33-
// check if okta client config exists?
34-
SdkConfigurationService sdkConfigurationService = new DefaultSdkConfigurationService();
35-
ClientConfiguration clientConfiguration = sdkConfigurationService.loadUnvalidatedConfiguration();
36-
String orgUrl = clientConfiguration.getBaseUrl();
38+
ProfileConfigurationService profileService = new DefaultProfileConfigurationService();
39+
File configFile = getEnvironment().getOktaPropsFile();
40+
41+
// Migrate legacy format if needed
42+
if (((DefaultProfileConfigurationService) profileService).isLegacyFormat(configFile)) {
43+
((DefaultProfileConfigurationService) profileService).migrateFromLegacyFormat(configFile);
44+
}
45+
46+
// Determine profile name: --profile-name flag > --profile flag > prompt
47+
String targetProfile = profileName;
48+
if (targetProfile == null) {
49+
String envProfile = getEnvironment().getProfile();
50+
if (!ProfileConfigurationService.DEFAULT_PROFILE_NAME.equals(envProfile)) {
51+
targetProfile = envProfile;
52+
}
53+
}
3754

3855
try (ConsoleOutput out = getConsoleOutput()) {
39-
// prompt user to overwrite config file
40-
if (Strings.hasText(orgUrl)
41-
&& !configQuestions().isOverwriteExistingConfig(orgUrl, getEnvironment().getOktaPropsFile().getAbsolutePath())) {
42-
return 0;
56+
// Prompt for profile name if not provided
57+
if (targetProfile == null) {
58+
targetProfile = getPrompter().promptUntilIfEmpty(null, "Profile name", ProfileConfigurationService.DEFAULT_PROFILE_NAME);
4359
}
4460

45-
// prompt for Base URL
46-
orgUrl = getPrompter().promptUntilValue("Okta Org URL");
61+
// Check if profile already exists
62+
Optional<OktaProfile> existingProfile = profileService.getProfile(configFile, targetProfile);
63+
if (existingProfile.isPresent()) {
64+
out.writeLine("Profile '" + targetProfile + "' already exists with org: " + existingProfile.get().getOrgUrl());
65+
if (!getPrompter().promptYesNo("Overwrite this profile?")) {
66+
return 0;
67+
}
68+
}
69+
70+
// Prompt for Okta Org URL
71+
String orgUrl = getPrompter().promptUntilValue("Okta Org URL");
4772
ConfigurationValidator.assertOrgUrl(orgUrl);
4873

74+
// Prompt for API token
4975
out.writeLine("Enter your Okta API token, for more information see: https://bit.ly/get-okta-api-token");
5076
String apiToken = getPrompter().promptUntilValue(null, "Okta API token");
5177
ConfigurationValidator.assertApiToken(apiToken);
5278

53-
sdkConfigurationService.writeOktaYaml(orgUrl, apiToken, getEnvironment().getOktaPropsFile());
79+
// Determine if this should be the active profile
80+
boolean setAsActive = true;
81+
String currentActive = profileService.getActiveProfileName(configFile);
82+
if (!currentActive.equals(targetProfile) && profileService.profileExists(configFile, currentActive)) {
83+
setAsActive = getPrompter().promptYesNo("Set '" + targetProfile + "' as the active profile?", true);
84+
}
85+
86+
// Save the profile
87+
OktaProfile newProfile = new OktaProfile(targetProfile, orgUrl, apiToken);
88+
profileService.saveProfile(configFile, newProfile, setAsActive);
89+
90+
out.writeLine("");
91+
out.bold("Profile '" + targetProfile + "' saved successfully!");
92+
out.writeLine("");
93+
out.writeLine("Org URL: " + orgUrl);
94+
if (setAsActive) {
95+
out.writeLine("This profile is now active.");
96+
} else {
97+
out.writeLine("Use 'okta --profile " + targetProfile + " <command>' or 'okta profiles use " + targetProfile + "' to switch.");
98+
}
5499
}
55100

56101
return 0;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2020-Present Okta, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.okta.cli.commands.profiles;
17+
18+
import picocli.CommandLine;
19+
20+
@CommandLine.Command(name = "profiles",
21+
description = "Manage Okta CLI profiles for multiple organizations",
22+
subcommands = {
23+
ProfilesList.class,
24+
ProfilesUse.class,
25+
ProfilesShow.class,
26+
ProfilesDelete.class,
27+
CommandLine.HelpCommand.class
28+
})
29+
public class Profiles implements Runnable {
30+
31+
@CommandLine.Spec
32+
private CommandLine.Model.CommandSpec spec;
33+
34+
@Override
35+
public void run() {
36+
// Default action: list profiles
37+
spec.commandLine().execute("list");
38+
}
39+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2020-Present Okta, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.okta.cli.commands.profiles;
17+
18+
import com.okta.cli.commands.BaseCommand;
19+
import com.okta.cli.common.model.OktaProfile;
20+
import com.okta.cli.common.service.DefaultProfileConfigurationService;
21+
import com.okta.cli.common.service.ProfileConfigurationService;
22+
import com.okta.cli.console.ConsoleOutput;
23+
import picocli.CommandLine;
24+
25+
import java.io.File;
26+
import java.util.Optional;
27+
28+
@CommandLine.Command(name = "delete",
29+
description = "Delete a profile")
30+
public class ProfilesDelete extends BaseCommand {
31+
32+
@CommandLine.Parameters(index = "0", description = "The profile name to delete")
33+
private String profileName;
34+
35+
@CommandLine.Option(names = {"-f", "--force"}, description = "Skip confirmation prompt")
36+
private boolean force;
37+
38+
@Override
39+
public int runCommand() throws Exception {
40+
ProfileConfigurationService profileService = new DefaultProfileConfigurationService();
41+
File configFile = getEnvironment().getOktaPropsFile();
42+
43+
try (ConsoleOutput out = getConsoleOutput()) {
44+
// Check if profile exists
45+
Optional<OktaProfile> profile = profileService.getProfile(configFile, profileName);
46+
if (profile.isEmpty()) {
47+
out.writeError("Profile '" + profileName + "' not found.");
48+
return 1;
49+
}
50+
51+
// Check if it's the active profile
52+
String activeProfile = profileService.getActiveProfileName(configFile);
53+
if (profileName.equals(activeProfile)) {
54+
out.writeError("Cannot delete the active profile '" + profileName + "'.");
55+
out.writeLine("Switch to another profile first using 'okta profiles use <name>'.");
56+
return 1;
57+
}
58+
59+
// Confirm deletion
60+
if (!force) {
61+
out.writeLine("Profile: " + profileName);
62+
out.writeLine("Org URL: " + profile.get().getOrgUrl());
63+
if (!getPrompter().promptYesNo("Delete this profile?", false)) {
64+
out.writeLine("Cancelled.");
65+
return 0;
66+
}
67+
}
68+
69+
// Delete the profile
70+
boolean deleted = profileService.deleteProfile(configFile, profileName);
71+
if (deleted) {
72+
out.writeLine("Profile '" + profileName + "' deleted.");
73+
} else {
74+
out.writeError("Failed to delete profile '" + profileName + "'.");
75+
return 1;
76+
}
77+
}
78+
79+
return 0;
80+
}
81+
}

0 commit comments

Comments
 (0)