Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ There are many out of the box features ready for you to build upon.

- Authentication
- OAuth
- OAuth + PKCE
- Username \ Password Flow (API Users)
- Basic Operations
- Query record(s)
Expand Down Expand Up @@ -85,6 +86,21 @@ $authenticator = $salesforceApi->completeOAuthLogin($oauthConfig, $code, $state)
$serialized = $authenticator->serialize();
```

#### OAuth + PKCE
Pass in a `code_verifier` parameter into the oAuth configuration and it will kick off the flow for this. You'll need to store the
`code_verifier` parameter on your own end - make it random of course. Library will handle the oddities of SHA256 + base64url encoding for you.

```php
$oauthConfig = OAuthConfiguration::create([
'client_id' => 'SALESFORCE_CONSUMER_KEY',
'client_secret' => 'SALESFORCE_CONSUMER_SECRET',
'redirect_uri' => 'REDIRECT_URI',
'code_verifier' => 'code-verifier-challenge-make-sure-this-is-random-and-not-shown-to-user',
]);
```

It's a little hacky, but this is not implemented upstream and I haven't updated Saloon so it is what it is.

#### Password Authentication
Please visit `YOUR_DOMAIN.com/_ui/system/security/ResetApiTokenEdit` to get a security token reset. It will email the user. This must be
appended to the back of the password when authenticating with the password flow **unless** you are using a whitelisted IP address range.
Expand Down
6 changes: 5 additions & 1 deletion index.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'client_id' => $_ENV['SALESFORCE_CONSUMER_KEY'],
'client_secret' => $_ENV['SALESFORCE_CONSUMER_SECRET'],
'redirect_uri' => $_ENV['REDIRECT_URI'],
'code_verifier' => 'code-verifier-challenge-make-sure-this-is-random-and-not-shown-to-user',
]);
$salesforceApi = new SalesforceApi();
$salesforceApi->setInstanceUrl($_ENV['SALESFORCE_INSTANCE_URL']);
Expand All @@ -28,10 +29,13 @@
echo "<a class='text-center' href='$url'>Click here to login via OAuth</a>";
} else {
$state = file_get_contents(__DIR__.'/.state');
$authenticator = $salesforceApi->completeOAuthLogin($oauthConfig, $_GET['code'], $state);
$authenticator = $salesforceApi->completeOAuthLogin($oauthConfig, $_GET['code'], $state, 'code-verifier-challenge-make-sure-this-is-random-and-not-shown-to-user');
$token = $authenticator->getAccessToken();
$refresh = $authenticator->getRefreshToken();

echo "<p>Access Token: <code>$token</code></p>";
echo "<p>Refresh Token: <code>$refresh</code></p>";

file_put_contents('.authenticator', $authenticator->serialize());

echo '<p>Token is ready, you can use the authenticator by deserializing .authenticator in the root or boot tinkerwell and just use $api.</p>';
Expand Down
56 changes: 55 additions & 1 deletion src/Connectors/SalesforceOAuthLoginConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,75 @@
namespace myoutdeskllc\SalesforcePhp\Connectors;

use myoutdeskllc\SalesforcePhp\OAuth\OAuthConfiguration;
use myoutdeskllc\SalesforcePhp\Requests\OAuth\GetAccessTokenWithPKCERequest;
use myoutdeskllc\SalesforcePhp\SalesforceApi;
use Saloon\Contracts\OAuthAuthenticator;
use Saloon\Contracts\Response;
use Saloon\Exceptions\InvalidStateException;
use Saloon\Http\Connector;
use Saloon\Http\OAuth2\GetAccessTokenRequest;
use Saloon\Traits\OAuth2\AuthorizationCodeGrant;

class SalesforceOAuthLoginConnector extends Connector
{
use AuthorizationCodeGrant;
protected string $codeVerifier = '';

public function setOauthConfiguration(OAuthConfiguration $configuration): void
public function setOauthConfiguration(OAuthConfiguration $configuration, string $codeVerifier = ''): void
{
$this->oauthConfig()->setClientId($configuration->getClientId());
$this->oauthConfig()->setClientSecret($configuration->getClientSecret());
$this->oauthConfig()->setRedirectUri($configuration->getRedirectUri());
$this->oauthConfig()->setAuthorizeEndpoint($this->resolveBaseUrl().'/services/oauth2/authorize');
$this->oauthConfig()->setTokenEndpoint($this->resolveBaseUrl().'/services/oauth2/token');
$this->codeVerifier = $codeVerifier;
}

public function getAccessToken(string $code, ?string $state = null, ?string $expectedState = null, bool $returnResponse = false): OAuthAuthenticator|Response
{
if (empty($this->codeVerifier)) {
return $this->authorizeWithoutPkce($code, $state, $expectedState, $returnResponse);
}

$this->oauthConfig()->validate();

if (!empty($state) && !empty($expectedState) && $state !== $expectedState) {
throw new InvalidStateException();
}

$oauthPKCERequest = new GetAccessTokenWithPKCERequest($code, $this->oauthConfig());
$defaultBody = $oauthPKCERequest->defaultBody();
$defaultBody['code_verifier'] = $this->codeVerifier;

$oauthPKCERequest->body()->set($defaultBody);
$response = $this->send($oauthPKCERequest);

if ($returnResponse === true) {
return $response;
}

$response->throw();

return $this->createOAuthAuthenticatorFromResponse($response, '');
}

public function authorizeWithoutPkce(string $code, ?string $state = null, ?string $expectedState = null, bool $returnResponse = false): OAuthAuthenticator|Response
{
$this->oauthConfig()->validate();

if (!empty($state) && !empty($expectedState) && $state !== $expectedState) {
throw new InvalidStateException();
}

$response = $this->send(new GetAccessTokenRequest($code, $this->oauthConfig()));

if ($returnResponse === true) {
return $response;
}

$response->throw();

return $this->createOAuthAuthenticatorFromResponse($response);
}

public function resolveBaseUrl(): string
Expand Down
14 changes: 14 additions & 0 deletions src/OAuth/OAuthConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@ class OAuthConfiguration
protected string $clientId;
protected string $clientSecret;
protected string $redirectUri;
protected string $codeChallenge;

public static function create(array $oAuthConfiguration): self
{
$oauthConfig = new self();
$oauthConfig->setClientId($oAuthConfiguration['client_id']);
$oauthConfig->setClientSecret($oAuthConfiguration['client_secret']);
$oauthConfig->setRedirectUri($oAuthConfiguration['redirect_uri']);
$oauthConfig->setCodeChallenge($oAuthConfiguration['code_verifier'] ?? '');

return $oauthConfig;
}

public function setCodeChallenge(string $codeVerifier): string
{
$this->codeChallenge = hash('sha256', $codeVerifier);

return $this->codeChallenge;
}

public function getClientId(): string
{
return $this->clientId;
Expand Down Expand Up @@ -56,4 +65,9 @@ public function setRedirectUri(string $redirectUri): void
{
$this->redirectUri = $redirectUri;
}

public function getCodeChallenge(): string
{
return $this->codeChallenge;
}
}
66 changes: 66 additions & 0 deletions src/Requests/OAuth/GetAccessTokenWithPKCERequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace myoutdeskllc\SalesforcePhp\Requests\OAuth;

use Saloon\Contracts\Body\HasBody;
use Saloon\Enums\Method;
use Saloon\Helpers\OAuth2\OAuthConfig;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasFormBody;
use Saloon\Traits\Plugins\AcceptsJson;

class GetAccessTokenWithPKCERequest extends Request implements HasBody
{
use HasFormBody;
use AcceptsJson;

/**
* Define the method that the request will use.
*
* @var \Saloon\Enums\Method
*/
protected Method $method = Method::POST;

/**
* Define the endpoint for the request.
*
* @return string
*/
public function resolveEndpoint(): string
{
return $this->oauthConfig->getTokenEndpoint();
}

/**
* Requires the authorization code and OAuth 2 config.
*
* @param string $code
* @param \Saloon\Helpers\OAuth2\OAuthConfig $oauthConfig
*/
public function __construct(protected string $code, protected OAuthConfig $oauthConfig)
{
//
}

/**
* Register the default data.
*
* @return array{
* grant_type: string,
* code: string,
* client_id: string,
* client_secret: string,
* redirect_uri: string,
* }
*/
public function defaultBody(): array
{
return [
'grant_type' => 'authorization_code',
'code' => $this->code,
'client_id' => $this->oauthConfig->getClientId(),
'client_secret' => $this->oauthConfig->getClientSecret(),
'redirect_uri' => $this->oauthConfig->getRedirectUri(),
];
}
}
14 changes: 11 additions & 3 deletions src/SalesforceApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,24 @@ public function startOAuthLogin(OAuthConfiguration $configuration): array
$connector = new Connectors\SalesforceOAuthLoginConnector();
$connector->setOauthConfiguration($configuration);

$authorizationUrl = $connector->getAuthorizationUrl();
// If we have a code challenge, we need to include it here
if (!empty($configuration->getCodeChallenge())) {
$base64Challenge = base64_encode(hex2bin($configuration->getCodeChallenge()));
$base64Challenge = str_replace(['+', '/', '='], ['-', '_', ''], $base64Challenge);
$authorizationUrl .= '&code_challenge='.$base64Challenge.'&code_challenge_method=S256';
}

return [
'url' => $connector->getAuthorizationUrl(),
'url' => $authorizationUrl,
'state' => $connector->getState(),
];
}

public function completeOAuthLogin(OAuthConfiguration $configuration, string $code, string $state): OAuthAuthenticator
public function completeOAuthLogin(OAuthConfiguration $configuration, string $code, string $state, string $codeVerifier = ''): OAuthAuthenticator
{
$connector = new Connectors\SalesforceOAuthLoginConnector();
$connector->setOauthConfiguration($configuration);
$connector->setOauthConfiguration($configuration, $codeVerifier);
$authenticator = $connector->getAccessToken($code, $state);

$this->connector = new SalesforceApiConnector();
Expand Down