Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 103 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,26 @@ Display the current version of the package.
#### `datacustomcode configure`
Configure credentials for connecting to Data Cloud.

**Prerequisites:**
- A [connected app](#creating-a-connected-app) with OAuth settings configured
- For OAuth Tokens authentication: [refresh token and core token](#obtaining-refresh-token-and-core-token)

Options:
- `--profile TEXT`: Credential profile name (default: "default")
- `--auth-type TEXT`: Authentication method (`oauth_tokens` or `username_password`, default: `oauth_tokens`)
- `--login-url TEXT`: Salesforce login URL

For Username/Password authentication:
- `--username TEXT`: Salesforce username
- `--password TEXT`: Salesforce password
- `--client-id TEXT`: Connected App Client ID
- `--client-secret TEXT`: Connected App Client Secret
- `--login-url TEXT`: Salesforce login URL

For OAuth Tokens authentication:
- `--client-id TEXT`: Connected App Client ID
- `--client-secret TEXT`: Connected App Client Secret
- `--refresh-token TEXT`: OAuth refresh token (see [Obtaining Refresh Token](#obtaining-refresh-token-and-core-token))
- `--core-token TEXT`: (Optional) OAuth core/access token - if not provided, it will be obtained using the refresh token


#### `datacustomcode init`
Expand Down Expand Up @@ -295,35 +308,98 @@ You can read more about Jupyter Notebooks here: https://jupyter.org/

## Prerequisite details

### Creating a connected app

1. Log in to salesforce as an admin. In the top right corner, click on the gear icon and go to `Setup`
2. In the left hand column search for `oauth`
3. Select `OAuth and OpenID Connect Settings`
4. Toggle on `Allow OAuth Username-Password Flows` and accept the dialog box that pops up
5. Clear the search bar
6. Expand `Apps`, expand `External Client Apps`, click `Settings`
7. Toggle on `Allow access to External Client App consumer secrets via REST API`
8. Toggle on `Allow creation of connected apps`
9. Click `Enable` in the warning box
10. Click `New Connected App` button
11. Fill in the required fields within the `Basic Information` section
12. Under the `API (Enable OAuth Settings)` section:
a. Click on the checkbox to Enable OAuth Settings.
b. Provide a callback URL like http://localhost:55555/callback
c. In the Selected OAuth Scopes, make sure that `refresh_token`, `api`, `cdp_query_api`, `cdp_profile_api` is selected.
d. Click on Save to save the connected app
13. From the detail page that opens up afterwards, click the `Manage Consumer Details` button to find your client id and client secret
14. Click `Cancel` button once complete
15. Click `Manage` button
16. Click `Edit Policies`
17. Under `IP Relaxation` select `Relax IP restrictions`
18. Click `Save`
19. Logout
20. Use the URL of the login page as the `login_url` value when setting up the SDK
### Creating an External Client app

1. Log in to Salesforce as an admin. In the top right corner, click on the gear icon and go to `Setup`
2. On the left sidebar, expand `Apps`, expand `External Client Apps`, click `Settings`
3. Toggle on `Allow access to External Client App consumer secrets via REST API`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure this step 3 is required?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on my recent test, it seems that refresh token flow works with this toggle turned off. Going to exclude this step for now.

4. Expand `Apps`, expand `External Client Apps`, click `External Client App Manager`
5. Click `New External Client App` button
6. Fill in the required fields within the `Basic Information` section
7. Under the `API (Enable OAuth Settings)` section:
1. Click on the checkbox to Enable OAuth Settings
2. Provide a callback URL like `http://localhost:5555/callback`
3. In the Selected OAuth Scopes, make sure that `refresh_token`, `api`, `cdp_query_api`, `cdp_profile_api` is selected
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-pick- "is selected" -> "are selected"

4. Check the following:
- Enable Authorization Code and Credentials Flow
- Require user credentials in the POST body for Authorization Code and Credentials Flow
5. Uncheck `Require Proof Key for Code Exchange (PKCE) extension for Supported Authorization Flows`
6. Click on `Create` button
8. On your newly created External Client App page, on the `Policies` tab:
1. In the `App Authorization` section, choose an appropriate Refresh Token Policy as per your expected usage and preference.
2. Under `App Authorization`, set IP Relaxation to `Relax IP restrictions` unless otherwise needed
9. Click `Save`
10. Go to the `Settings` tab, under `OAuth Settings`. There, you can use the `Consumer Key and Secret` button to obtain the `client_id` and `client_secret` used during configuring credentials using this SDK
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be more explicit about this instruction: Click this button, then copy those values.

11. Logout
12. Use the URL of the login page as the `login_url` value when setting up the SDK

You now have all fields necessary for the `datacustomcode configure` command.

### Obtaining Refresh Token and Core Token

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

#### Step 1: Note Connected App Details

From your connected app, note down the following:
- **Client ID**
- **Client Secret**
- **Callback URL** (e.g., `http://localhost:55555/callback`)

#### Step 2: Obtain Authorization Code

1. Open a browser and navigate to the following URL (replace placeholders with your values):

```
<LOGIN_URL>/services/oauth2/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>
```

2. After authenticating, you'll be redirected to your callback URL. The redirected URL will be in the form:
```
<CALLBACK_URL>?code=<CODE>
```

3. Extract the `<CODE>` from the address bar. If the address bar doesn't show it, check the **Network tab** in your browser's developer tools.

#### Step 3: Exchange Code for Tokens

Make a POST request to exchange the authorization code for tokens. You can use `curl` or Postman:

```bash
curl --location --request POST '<LOGIN_URL>/services/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=<CODE>' \
--data-urlencode 'client_id=<CLIENT_ID>' \
--data-urlencode 'client_secret=<CLIENT_SECRET>' \
--data-urlencode 'redirect_uri=<CALLBACK_URL>'
```

The response will be a JSON object containing:

```json
{
"access_token": "<access_token>",
"refresh_token": "<refresh_token>",
"signature": "<signature>",
"scope": "refresh_token cdp_query_api api cdp_profile_api cdp_api full",
"id_token": "<id_token>",
"instance_url": "https://your-instance.my.salesforce.com",
"id": "https://login.salesforce.com/id/00DSB.../005SB...",
"token_type": "Bearer",
"issued_at": "1767743916187"
}
```

The key fields you need are:
| Field | Description |
|-------|-------------|
| `access_token` | The **core token** (also called access token) |
| `refresh_token` | The **refresh token** for obtaining new access tokens |
| `instance_url` | Your Salesforce instance URL |

Use the `refresh_token` value when running `datacustomcode configure` with OAuth Tokens authentication.

## Other docs

- [Troubleshooting](./docs/troubleshooting.md)
Expand Down
9 changes: 8 additions & 1 deletion src/datacustomcode/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@
# limitations under the License.

from datacustomcode.client import Client
from datacustomcode.credentials import AuthType, Credentials
from datacustomcode.io.reader.query_api import QueryAPIDataCloudReader
from datacustomcode.io.writer.print import PrintDataCloudWriter

__all__ = ["Client", "QueryAPIDataCloudReader", "PrintDataCloudWriter"]
__all__ = [
"AuthType",
"Client",
"Credentials",
"PrintDataCloudWriter",
"QueryAPIDataCloudReader",
]
109 changes: 93 additions & 16 deletions src/datacustomcode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,30 +43,107 @@ def version():
click.echo("Version information not available")


@cli.command()
@click.option("--profile", default="default")
@click.option("--username", prompt=True)
@click.option("--password", prompt=True, hide_input=True)
@click.option("--client-id", prompt=True)
@click.option("--client-secret", prompt=True)
@click.option("--login-url", prompt=True)
def configure(
username: str,
password: str,
client_id: str,
client_secret: str,
def _configure_username_password(
login_url: str,
client_id: str,
profile: str,
) -> None:
from datacustomcode.credentials import Credentials
"""Configure credentials for Username/Password authentication."""
from datacustomcode.credentials import AuthType, Credentials

username = click.prompt("Username")
password = click.prompt("Password", hide_input=True)
client_secret = click.prompt("Client Secret")

Credentials(
credentials = Credentials(
login_url=login_url,
client_id=client_id,
auth_type=AuthType.USERNAME_PASSWORD,
username=username,
password=password,
client_id=client_id,
client_secret=client_secret,
)
credentials.update_ini(profile=profile)
click.secho(
f"Username/Password credentials saved to profile '{profile}' successfully",
fg="green",
)


def _configure_oauth_tokens(
login_url: str,
client_id: str,
profile: str,
) -> None:
"""Configure credentials for OAuth Tokens authentication."""
from datacustomcode.credentials import AuthType, Credentials

client_secret = click.prompt("Client Secret")
refresh_token = click.prompt("Refresh Token")
core_token = click.prompt(
"Core Token (optional, press Enter to skip)",
default="",
show_default=False,
)

credentials = Credentials(
login_url=login_url,
).update_ini(profile=profile)
client_id=client_id,
auth_type=AuthType.OAUTH_TOKENS,
client_secret=client_secret,
refresh_token=refresh_token,
core_token=core_token if core_token else None,
)
credentials.update_ini(profile=profile)
click.secho(
f"OAuth Tokens credentials saved to profile '{profile}' successfully",
fg="green",
)


@cli.command()
@click.option("--profile", default="default", help="Credential profile name")
@click.option(
"--auth-type",
type=click.Choice(["oauth_tokens", "username_password"]),
default=None,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we default to OAuth? Similarly, maybe we note this in the README, that users know we'd recommend OAuth over username/password for security reasons.

help="""Authentication method to use.

\b
oauth_tokens - OAuth tokens (refresh_token/core_token) authentication
username_password - Traditional username/password OAuth flow
""",
)
def configure(profile: str, auth_type: str) -> None:
"""Configure credentials for connecting to Data Cloud."""
from datacustomcode.credentials import AuthType

# If auth_type not specified, prompt for it
if auth_type is None:
click.echo("\nSelect authentication method:")
click.echo(" 1. OAuth Tokens [default]")
click.echo(" 2. Username/Password")
choice = click.prompt(
"Enter choice",
type=click.Choice(["1", "2"]),
default="1",
)
auth_type_map = {
"1": "oauth_tokens",
"2": "username_password",
}
auth_type = auth_type_map[choice]

# Common fields for all auth types
click.echo(f"\nConfiguring {auth_type} authentication for profile '{profile}':\n")
login_url = click.prompt("Login URL")
client_id = click.prompt("Client ID")

# Route to appropriate handler based on auth type
if auth_type == AuthType.USERNAME_PASSWORD.value:
_configure_username_password(login_url, client_id, profile)
elif auth_type == AuthType.OAUTH_TOKENS.value:
_configure_oauth_tokens(login_url, client_id, profile)


@cli.command()
Expand Down
Loading