-
Notifications
You must be signed in to change notification settings - Fork 6
OAuth Support for Refresh tokens flow #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
cccdb6c
d28d716
08343c9
87126a2
a8950a9
52aa003
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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` | ||
|
|
@@ -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` | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
|
|
||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.