Skip to content

Commit 373a868

Browse files
committed
Auth updates - add client credentials flow, remove username password flow
1 parent 0fe0a73 commit 373a868

8 files changed

Lines changed: 251 additions & 228 deletions

File tree

README.md

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Use of this project with Salesforce is subject to the [TERMS OF USE](./TERMS_OF_
1212
- [Azul Zulu OpenJDK 17.x](https://www.azul.com/downloads/?version=java-17-lts&package=jdk#zulu)
1313
- Docker support like [Docker Desktop](https://docs.docker.com/desktop/)
1414
- A salesforce org with some DLOs or DMOs with data and this feature enabled (it is not GA)
15-
- A [connected app](#creating-a-connected-app)
15+
- An [External Client App](#creating-an-external-client-app)
1616

1717
## Installation
1818
The SDK can be downloaded directly from PyPI with `pip`:
@@ -58,7 +58,7 @@ This will yield all necessary files to get started:
5858
* `config.json` – This config defines permissions on the back and can be generated programmatically with `scan` CLI method.
5959
* `entrypoint.py` – The script that defines the data transformation logic.
6060

61-
A functional entrypoint.py is provided so you can run once you've configured your connected app:
61+
A functional entrypoint.py is provided so you can run once you've configured your External Client App:
6262
```zsh
6363
cd my_package
6464
datacustomcode configure
@@ -175,26 +175,26 @@ Display the current version of the package.
175175
Configure credentials for connecting to Data Cloud.
176176

177177
**Prerequisites:**
178-
- A [connected app](#creating-a-connected-app) with OAuth settings configured
178+
- An [External Client App](#creating-an-external-client-app) with OAuth settings configured
179179
- For OAuth Tokens authentication: [refresh token and core token](#obtaining-refresh-token-and-core-token)
180180

181181
Options:
182182
- `--profile TEXT`: Credential profile name (default: "default")
183-
- `--auth-type TEXT`: Authentication method (`oauth_tokens` or `username_password`, default: `oauth_tokens`)
183+
- `--auth-type TEXT`: Authentication method (default: `oauth_tokens`)
184+
- `oauth_tokens` - OAuth tokens with refresh_token
185+
- `client_credentials` - Server-to-server using client_id/secret only
184186
- `--login-url TEXT`: Salesforce login URL
185187

186-
For Username/Password authentication:
187-
- `--username TEXT`: Salesforce username
188-
- `--password TEXT`: Salesforce password
189-
- `--client-id TEXT`: Connected App Client ID
190-
- `--client-secret TEXT`: Connected App Client Secret
191-
192188
For OAuth Tokens authentication:
193-
- `--client-id TEXT`: Connected App Client ID
194-
- `--client-secret TEXT`: Connected App Client Secret
189+
- `--client-id TEXT`: External Client App Client ID
190+
- `--client-secret TEXT`: External Client App Client Secret
195191
- `--refresh-token TEXT`: OAuth refresh token (see [Obtaining Refresh Token](#obtaining-refresh-token-and-core-token))
196192
- `--core-token TEXT`: (Optional) OAuth core/access token - if not provided, it will be obtained using the refresh token
197193

194+
For Client Credentials authentication (server-to-server):
195+
- `--client-id TEXT`: External Client App Client ID
196+
- `--client-secret TEXT`: External Client App Client Secret
197+
198198
##### Using Environment Variables (Alternative)
199199

200200
Instead of using `datacustomcode configure`, you can also set credentials via environment variables.
@@ -207,7 +207,7 @@ Instead of using `datacustomcode configure`, you can also set credentials via en
207207
|----------|-------------|
208208
| `SFDC_LOGIN_URL` | Salesforce login URL (e.g., `https://login.salesforce.com`) |
209209
| `SFDC_CLIENT_ID` | External Client App Client ID |
210-
| `SFDC_AUTH_TYPE` | Authentication type: `oauth_tokens` (default) or `username_password` |
210+
| `SFDC_AUTH_TYPE` | Authentication type: `oauth_tokens` (default) or `client_credentials` |
211211

212212
**For OAuth Tokens authentication (`SFDC_AUTH_TYPE=oauth_tokens`):**
213213
| Variable | Description |
@@ -216,13 +216,6 @@ Instead of using `datacustomcode configure`, you can also set credentials via en
216216
| `SFDC_REFRESH_TOKEN` | OAuth refresh token |
217217
| `SFDC_CORE_TOKEN` | (Optional) OAuth core/access token |
218218

219-
**For Username/Password authentication (`SFDC_AUTH_TYPE=username_password`):**
220-
| Variable | Description |
221-
|----------|-------------|
222-
| `SFDC_USERNAME` | Salesforce username |
223-
| `SFDC_PASSWORD` | Salesforce password |
224-
| `SFDC_CLIENT_SECRET` | External Client App Client Secret |
225-
226219
Example usage:
227220
```bash
228221
export SFDC_LOGIN_URL="https://login.salesforce.com"
@@ -376,9 +369,9 @@ You now have all fields necessary for the `datacustomcode configure` command.
376369

377370
If you're using OAuth Tokens authentication (instead of Username/Password), follow these steps to obtain your refresh token and core token (access token).
378371

379-
#### Step 1: Note Connected App Details
372+
#### Step 1: Note External Client App Details
380373

381-
From your connected app, note down the following:
374+
From your External Client App, note down the following:
382375
- **Client ID**
383376
- **Client Secret**
384377
- **Callback URL** (e.g., `http://localhost:55555/callback`)

src/datacustomcode/cli.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,60 +43,56 @@ def version():
4343
click.echo("Version information not available")
4444

4545

46-
def _configure_username_password(
46+
def _configure_oauth_tokens(
4747
login_url: str,
4848
client_id: str,
4949
profile: str,
5050
) -> None:
51-
"""Configure credentials for Username/Password authentication."""
51+
"""Configure credentials for OAuth Tokens authentication."""
5252
from datacustomcode.credentials import AuthType, Credentials
5353

54-
username = click.prompt("Username")
55-
password = click.prompt("Password", hide_input=True)
5654
client_secret = click.prompt("Client Secret")
55+
refresh_token = click.prompt("Refresh Token")
56+
core_token = click.prompt(
57+
"Core Token (optional, press Enter to skip)",
58+
default="",
59+
show_default=False,
60+
)
5761

5862
credentials = Credentials(
5963
login_url=login_url,
6064
client_id=client_id,
61-
auth_type=AuthType.USERNAME_PASSWORD,
62-
username=username,
63-
password=password,
65+
auth_type=AuthType.OAUTH_TOKENS,
6466
client_secret=client_secret,
67+
refresh_token=refresh_token,
68+
core_token=core_token if core_token else None,
6569
)
6670
credentials.update_ini(profile=profile)
6771
click.secho(
68-
f"Username/Password credentials saved to profile '{profile}' successfully",
72+
f"OAuth Tokens credentials saved to profile '{profile}' successfully",
6973
fg="green",
7074
)
7175

7276

73-
def _configure_oauth_tokens(
77+
def _configure_client_credentials(
7478
login_url: str,
7579
client_id: str,
7680
profile: str,
7781
) -> None:
78-
"""Configure credentials for OAuth Tokens authentication."""
82+
"""Configure credentials for Client Credentials authentication."""
7983
from datacustomcode.credentials import AuthType, Credentials
8084

8185
client_secret = click.prompt("Client Secret")
82-
refresh_token = click.prompt("Refresh Token")
83-
core_token = click.prompt(
84-
"Core Token (optional, press Enter to skip)",
85-
default="",
86-
show_default=False,
87-
)
8886

8987
credentials = Credentials(
9088
login_url=login_url,
9189
client_id=client_id,
92-
auth_type=AuthType.OAUTH_TOKENS,
90+
auth_type=AuthType.CLIENT_CREDENTIALS,
9391
client_secret=client_secret,
94-
refresh_token=refresh_token,
95-
core_token=core_token if core_token else None,
9692
)
9793
credentials.update_ini(profile=profile)
9894
click.secho(
99-
f"OAuth Tokens credentials saved to profile '{profile}' successfully",
95+
f"Client Credentials saved to profile '{profile}' successfully",
10096
fg="green",
10197
)
10298

@@ -105,13 +101,13 @@ def _configure_oauth_tokens(
105101
@click.option("--profile", default="default", help="Credential profile name")
106102
@click.option(
107103
"--auth-type",
108-
type=click.Choice(["oauth_tokens", "username_password"]),
104+
type=click.Choice(["oauth_tokens", "client_credentials"]),
109105
default="oauth_tokens",
110106
help="""Authentication method to use.
111107
112108
\b
113-
oauth_tokens - OAuth tokens (refresh_token/core_token) authentication [DEFAULT]
114-
username_password - Traditional username/password OAuth flow
109+
oauth_tokens - OAuth tokens (refresh_token) authentication (default)
110+
client_credentials - Server-to-server using client_id/secret only
115111
""",
116112
)
117113
def configure(profile: str, auth_type: str) -> None:
@@ -124,10 +120,10 @@ def configure(profile: str, auth_type: str) -> None:
124120
client_id = click.prompt("Client ID")
125121

126122
# Route to appropriate handler based on auth type
127-
if auth_type == AuthType.USERNAME_PASSWORD.value:
128-
_configure_username_password(login_url, client_id, profile)
129-
elif auth_type == AuthType.OAUTH_TOKENS.value:
123+
if auth_type == AuthType.OAUTH_TOKENS.value:
130124
_configure_oauth_tokens(login_url, client_id, profile)
125+
elif auth_type == AuthType.CLIENT_CREDENTIALS.value:
126+
_configure_client_credentials(login_url, client_id, profile)
131127

132128

133129
@cli.command()

src/datacustomcode/credentials.py

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
class AuthType(str, Enum):
2929
"""Supported authentication methods for Salesforce Data Cloud."""
3030

31-
USERNAME_PASSWORD = "username_password"
3231
OAUTH_TOKENS = "oauth_tokens"
32+
CLIENT_CREDENTIALS = "client_credentials"
3333

3434

3535
# Environment variable mappings for each auth type
@@ -38,37 +38,31 @@ class AuthType(str, Enum):
3838
"client_id": "SFDC_CLIENT_ID",
3939
}
4040

41-
ENV_CREDENTIALS_USERNAME_PASSWORD = {
42-
"username": "SFDC_USERNAME",
43-
"password": "SFDC_PASSWORD",
44-
"client_secret": "SFDC_CLIENT_SECRET",
45-
}
46-
4741
ENV_CREDENTIALS_OAUTH_TOKENS = {
4842
"client_secret": "SFDC_CLIENT_SECRET",
4943
"refresh_token": "SFDC_REFRESH_TOKEN",
5044
"core_token": "SFDC_CORE_TOKEN",
5145
}
5246

47+
ENV_CREDENTIALS_CLIENT_CREDENTIALS = {
48+
"client_secret": "SFDC_CLIENT_SECRET",
49+
}
50+
5351

5452
@dataclass
5553
class Credentials:
5654
"""Flexible credentials supporting multiple authentication methods.
5755
5856
Supports two authentication methods:
59-
- OAUTH_TOKENS: OAuth tokens (core_token and refresh_token) authentication
60-
- USERNAME_PASSWORD: Traditional username/password OAuth flow
57+
- OAUTH_TOKENS: OAuth tokens (refresh_token) authentication (default)
58+
- CLIENT_CREDENTIALS: Server-to-server integration using client_id/secret only
6159
"""
6260

6361
# Required for all auth types
6462
login_url: str
6563
client_id: str
6664
auth_type: AuthType = field(default=AuthType.OAUTH_TOKENS)
6765

68-
# Username/Password flow fields
69-
username: Optional[str] = None
70-
password: Optional[str] = None
71-
7266
# Common field
7367
client_secret: Optional[str] = None
7468

@@ -82,20 +76,7 @@ def __post_init__(self):
8276

8377
def _validate(self) -> None:
8478
"""Validate that required fields are present for the auth type."""
85-
if self.auth_type == AuthType.USERNAME_PASSWORD:
86-
missing = []
87-
if not self.username:
88-
missing.append("username")
89-
if not self.password:
90-
missing.append("password")
91-
if not self.client_secret:
92-
missing.append("client_secret")
93-
if missing:
94-
raise ValueError(
95-
f"Username/Password auth requires: {', '.join(missing)}"
96-
)
97-
98-
elif self.auth_type == AuthType.OAUTH_TOKENS:
79+
if self.auth_type == AuthType.OAUTH_TOKENS:
9980
missing = []
10081
if not self.refresh_token:
10182
missing.append("refresh_token")
@@ -104,6 +85,10 @@ def _validate(self) -> None:
10485
if missing:
10586
raise ValueError(f"OAuth Tokens auth requires: {', '.join(missing)}")
10687

88+
elif self.auth_type == AuthType.CLIENT_CREDENTIALS:
89+
if not self.client_secret:
90+
raise ValueError("Client Credentials auth requires: client_secret")
91+
10792
@classmethod
10893
def from_ini(
10994
cls,
@@ -150,9 +135,6 @@ def from_ini(
150135
login_url=section["login_url"],
151136
client_id=section["client_id"],
152137
auth_type=auth_type,
153-
# Username/Password fields
154-
username=section.get("username"),
155-
password=section.get("password"),
156138
client_secret=section.get("client_secret"),
157139
# OAuth Tokens fields
158140
core_token=section.get("core_token"),
@@ -166,19 +148,14 @@ def from_env(cls) -> Credentials:
166148
Environment variables:
167149
Common (required):
168150
SFDC_LOGIN_URL: Salesforce login URL
169-
SFDC_CLIENT_ID: Connected App client ID
151+
SFDC_CLIENT_ID: External Client App client ID
170152
SFDC_AUTH_TYPE: Authentication type (optional, defaults to oauth_tokens)
171153
172154
For oauth_tokens (default):
173-
SFDC_CLIENT_SECRET: Connected App client secret
155+
SFDC_CLIENT_SECRET: External Client App client secret
174156
SFDC_REFRESH_TOKEN: OAuth refresh token
175157
SFDC_CORE_TOKEN: OAuth core/access token (optional)
176158
177-
For username_password:
178-
SFDC_USERNAME: Salesforce username
179-
SFDC_PASSWORD: Salesforce password
180-
SFDC_CLIENT_SECRET: Connected App client secret
181-
182159
Returns:
183160
Credentials instance loaded from environment variables
184161
@@ -208,9 +185,6 @@ def from_env(cls) -> Credentials:
208185
login_url=login_url,
209186
client_id=client_id,
210187
auth_type=auth_type,
211-
# Username/Password fields
212-
username=os.environ.get("SFDC_USERNAME"),
213-
password=os.environ.get("SFDC_PASSWORD"),
214188
client_secret=os.environ.get("SFDC_CLIENT_SECRET"),
215189
# OAuth Tokens fields
216190
core_token=os.environ.get("SFDC_CORE_TOKEN"),
@@ -273,15 +247,7 @@ def update_ini(self, profile: str = "default", ini_file: str = INI_FILE) -> None
273247
config[profile]["client_id"] = self.client_id
274248

275249
# Save fields based on auth type
276-
if self.auth_type == AuthType.USERNAME_PASSWORD:
277-
config[profile]["username"] = self.username or ""
278-
config[profile]["password"] = self.password or ""
279-
config[profile]["client_secret"] = self.client_secret or ""
280-
# Remove fields from other auth types
281-
for key in ["refresh_token", "core_token"]:
282-
config[profile].pop(key, None)
283-
284-
elif self.auth_type == AuthType.OAUTH_TOKENS:
250+
if self.auth_type == AuthType.OAUTH_TOKENS:
285251
config[profile]["client_secret"] = self.client_secret or ""
286252
config[profile]["refresh_token"] = self.refresh_token or ""
287253
if self.core_token:
@@ -290,6 +256,12 @@ def update_ini(self, profile: str = "default", ini_file: str = INI_FILE) -> None
290256
for key in ["username", "password"]:
291257
config[profile].pop(key, None)
292258

259+
elif self.auth_type == AuthType.CLIENT_CREDENTIALS:
260+
config[profile]["client_secret"] = self.client_secret or ""
261+
# Remove fields from other auth types
262+
for key in ["username", "password", "refresh_token", "core_token"]:
263+
config[profile].pop(key, None)
264+
293265
with open(expanded_ini_file, "w") as f:
294266
config.write(f)
295267

0 commit comments

Comments
 (0)