diff --git a/README.md b/README.md index a3f91f5..c8467d4 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,8 @@ The repository includes runnable examples: Run with: `php examples/simple.php` - `examples/checkout.php` - create and process a checkout. Run with: `php examples/checkout.php` +- `examples/oauth2/oauth2-server.php` - OAuth 2.0 Authorization Code flow with PKCE. + Run with: `cd examples/oauth2 && php -S localhost:8080 oauth2-server.php` - `examples/custom-http-client.php` - wrap/customize HTTP behavior. Run with: `php examples/custom-http-client.php` - `examples/guzzle-http-client.php` - use the built-in Guzzle client. diff --git a/examples/oauth2/README.md b/examples/oauth2/README.md index 370f1f1..d68a28e 100644 --- a/examples/oauth2/README.md +++ b/examples/oauth2/README.md @@ -1,141 +1,38 @@ -# OAuth 2.0 Examples for SumUp PHP SDK +# OAuth2 Example for SumUp PHP SDK -This directory contains examples demonstrating how to implement OAuth 2.0 Authorization Code flow with the SumUp PHP SDK. +This example demonstrates the OAuth 2.0 Authorization Code flow with PKCE using `league/oauth2-client`, then uses the resulting access token with the SumUp PHP SDK. -## Prerequisites +## Requirements -Before running these examples, you need: +Set these environment variables before running the example: -1. **Client Credentials**: Create an OAuth2 application in the [SumUp Developer Settings](https://me.sumup.com/en-us/settings/oauth2-applications) -2. **Redirect URI**: Configure your application with the correct redirect URI: - - For web server example: `http://localhost:8080/callback` - -## Examples - -### 1. Web Server Example (`oauth2-server.php`) - -A complete web-based OAuth2 implementation with a built-in web server. - -**Setup:** ```bash export CLIENT_ID="your_client_id" export CLIENT_SECRET="your_client_secret" -export REDIRECT_URI="http://localhost:8080/callback" # Optional, defaults to localhost -``` - -**Run:** -```bash -cd examples/oauth2 -php -S localhost:8080 oauth2-server.php +export REDIRECT_URI="http://localhost:8080/callback" ``` -**Usage:** -1. Open http://localhost:8080 in your browser -2. Click "Start OAuth2 Flow" -3. Authorize the application on SumUp -4. View merchant information and token details - -**Features:** -- Complete OAuth2 Authorization Code flow with PKCE -- CSRF protection with state parameter -- Session-based state management -- Merchant information display -- Example API usage - -## OAuth 2.0 Flow Overview - -The example implements the Authorization Code flow with PKCE (Proof Key for Code Exchange): - -1. **Authorization Request**: User is redirected to SumUp's authorization server -2. **User Authorization**: User logs in and grants permissions -3. **Authorization Code**: SumUp redirects back with an authorization code -4. **Token Exchange**: Application exchanges the code for an access token -5. **API Access**: Use the access token with the SumUp SDK - -## Security Features - -- **PKCE (RFC 7636)**: Prevents authorization code interception attacks -- **State Parameter**: Prevents CSRF attacks -- **Secure Cookie Settings**: HttpOnly, SameSite protection (web example) -- **Session Management**: Proper cleanup of sensitive data - -## Scopes - -The examples request these scopes: -- `payments`: Create and manage payments/checkouts -- `transactions.history`: Access transaction history -- `user.profile_readonly`: Read user profile information -- `user.app-settings`: Access application settings +The redirect URI must match the one configured for your OAuth2 application in the SumUp Developer Settings. -Adjust the scopes based on your application's needs. Always request the minimum required permissions. - -## Integration with Your Application - -After obtaining an access token, use it with the SumUp SDK: - -```php - $accessToken, -]); - -// Option 2: Set token later -$sumup = new \SumUp\SumUp(); -$sumup->setDefaultAccessToken($accessToken); - -// Use the SDK normally -$request = new \SumUp\Types\CheckoutCreateRequest(); -$request->amount = 10.00; -$request->currency = \SumUp\Types\CheckoutCreateRequestCurrency::EUR; -$request->checkoutReference = 'order-123'; -$request->merchantCode = $merchantCode; - -$checkout = $sumup->checkouts()->create($request); -``` - -## Token Storage - -In production applications: - -1. **Store tokens securely** (encrypted database, secure session storage) -2. **Handle token expiration** (implement refresh token logic) -3. **Protect refresh tokens** (encrypt, rotate regularly) -4. **Implement proper logout** (revoke tokens on logout) - -## Dependencies - -These examples use the [League OAuth2 Client](https://oauth2-client.thephpleague.com/) library: +## Run ```bash -composer require league/oauth2-client +cd examples/oauth2 +php -S localhost:8080 oauth2-server.php ``` -## Production Considerations - -- Use HTTPS in production -- Set secure cookie flags -- Implement proper error handling -- Log security events -- Validate all input parameters -- Use environment variables for sensitive configuration -- Implement rate limiting -- Monitor for suspicious activity +Then open `http://localhost:8080/` and start the flow. -## Troubleshooting +## Flow -**Common Issues:** +The example exposes two routes: -1. **Invalid redirect URI**: Ensure your OAuth2 app is configured with the correct redirect URI -2. **Invalid client credentials**: Check your CLIENT_ID and CLIENT_SECRET -3. **Scope errors**: Verify your application has been granted the requested scopes -4. **Token expiration**: Implement refresh token logic for long-running applications -5. **CORS issues**: Ensure proper CORS configuration for web applications +- `/login` generates a state value and PKCE verifier, then redirects to SumUp authorization. +- `/callback` verifies the state, exchanges the code for an access token, and fetches the merchant identified by `merchant_code`. -**Debug Tips:** +## Notes -- Enable error reporting: `error_reporting(E_ALL);` -- Check SumUp API logs in your developer dashboard -- Validate OAuth2 parameters match your app configuration -- Test with different browsers/incognito mode +- The example relies on `league/oauth2-client` to generate state and PKCE parameters and to exchange the authorization code for a token. +- The example requests the `email profile` scope. +- The access token is passed directly into `new \SumUp\SumUp([ 'access_token' => ... ])`. +- The response includes the fetched merchant payload as JSON. diff --git a/examples/oauth2/SumUpProvider.php b/examples/oauth2/SumUpProvider.php deleted file mode 100644 index 33e39b5..0000000 --- a/examples/oauth2/SumUpProvider.php +++ /dev/null @@ -1,195 +0,0 @@ -baseUrl . '/v0.1/me/memberships'; - } - - protected function getDefaultScopes(): array - { - return self::DEFAULT_SCOPES; - } - - protected function checkResponse(ResponseInterface $response, $data): void - { - if ($response->getStatusCode() >= 400) { - $message = 'OAuth2 error'; - - if (isset($data['error'])) { - $message = $data['error']; - if (isset($data['error_description'])) { - $message .= ': ' . $data['error_description']; - } - } - - throw new IdentityProviderException( - $message, - $response->getStatusCode(), - $response - ); - } - } - - protected function createResourceOwner(array $response, AccessToken $token): SumUpResourceOwner - { - return new SumUpResourceOwner($response); - } - - /** - * Get authorization URL with PKCE enabled by default - */ - public function getAuthorizationUrl(array $options = []): string - { - // Enable PKCE by default for enhanced security - if (!isset($options['code_challenge'])) { - $verifier = $this->getRandomPKCEVerifier(); - $challenge = $this->getPKCEChallenge($verifier); - - $options['code_challenge'] = $challenge; - $options['code_challenge_method'] = 'S256'; - - // Store verifier for later use (you should store this securely) - $this->pkceVerifier = $verifier; - } - - return parent::getAuthorizationUrl($options); - } - - /** - * Generate a random PKCE code verifier - */ - public function getRandomPKCEVerifier(): string - { - return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); - } - - /** - * Generate PKCE code challenge from verifier - */ - public function getPKCEChallenge(string $verifier): string - { - return rtrim(strtr(base64_encode(hash('sha256', $verifier, true)), '+/', '-_'), '='); - } - - /** - * Get access token with PKCE verifier - */ - public function getAccessTokenWithPKCE(string $code, string $verifier): AccessToken - { - return $this->getAccessToken('authorization_code', [ - 'code' => $code, - 'code_verifier' => $verifier, - ]); - } -} - -/** - * Resource owner class for SumUp users - */ -class SumUpResourceOwner implements \League\OAuth2\Client\Provider\ResourceOwnerInterface -{ - protected $response; - - public function __construct(array $response = []) - { - $this->response = $response; - } - - public function getId() - { - // SumUp doesn't return a user ID in the memberships endpoint - // This would typically be available in a dedicated user info endpoint - return $this->response['id'] ?? null; - } - - public function toArray(): array - { - return $this->response; - } - - /** - * Get user's merchant memberships - */ - public function getMemberships(): array - { - return $this->response; - } - - /** - * Get the default merchant code - */ - public function getDefaultMerchantCode(): ?string - { - if (!empty($this->response) && is_array($this->response)) { - $firstMembership = reset($this->response); - return $firstMembership['merchant_code'] ?? null; - } - return null; - } -} diff --git a/examples/oauth2/oauth2-server.php b/examples/oauth2/oauth2-server.php index dacfc88..7acffd6 100644 --- a/examples/oauth2/oauth2-server.php +++ b/examples/oauth2/oauth2-server.php @@ -3,45 +3,46 @@ /** * OAuth 2.0 Authorization Code flow with SumUp * - * This example walks you through the steps necessary to implement - * OAuth 2.0 (https://oauth.net/) in case you are building a software - * for other people to use. + * This example shows the minimal flow required to authenticate a user, + * exchange the authorization code for an access token, and use that token + * with the SumUp PHP SDK. * - * To get started, you will need your client credentials. - * If you don't have any yet, you can create them in the - * [Developer Settings](https://me.sumup.com/en-us/settings/oauth2-applications). + * Required environment variables: + * - CLIENT_ID + * - CLIENT_SECRET + * - REDIRECT_URI * - * Your credentials need to be configured with the correct redirect URI, - * that's the URI the user will get redirected to once they authenticate - * and authorize your application. For development, you might want to - * use for example `http://localhost:8080/callback`. In production, you would - * redirect the user back to your host, e.g. `https://example.com/callback`. - * - * To run this example: - * 1. Set environment variables: CLIENT_ID, CLIENT_SECRET, REDIRECT_URI - * 2. Run: php -S localhost:8080 oauth2-server.php - * 3. Visit: http://localhost:8080/login + * Run: + * php -S localhost:8080 oauth2-server.php */ require_once __DIR__ . '/../../vendor/autoload.php'; use League\OAuth2\Client\Provider\GenericProvider; -use League\OAuth2\Client\Token\AccessToken; -const STATE_COOKIE_NAME = 'oauth_state'; -const PKCE_COOKIE_NAME = 'oauth_pkce'; +const STATE_SESSION_KEY = 'oauth_state'; +const PKCE_SESSION_KEY = 'oauth_pkce'; +const SCOPES = 'email profile'; +session_set_cookie_params([ + 'path' => '/', + 'httponly' => true, + 'secure' => false, + 'samesite' => 'Lax', +]); session_start(); $clientId = getenv('CLIENT_ID'); $clientSecret = getenv('CLIENT_SECRET'); -$redirectUri = getenv('REDIRECT_URI') ?: 'http://localhost:8080/callback'; +$redirectUri = getenv('REDIRECT_URI'); -if (!$clientId || !$clientSecret) { - die("Please set CLIENT_ID and CLIENT_SECRET environment variables\n"); +if (!$clientId || !$clientSecret || !$redirectUri) { + http_response_code(500); + header('Content-Type: text/plain; charset=utf-8'); + echo 'Missing CLIENT_ID, CLIENT_SECRET or REDIRECT_URI environment variables'; + exit; } -// Configure the OAuth2 provider for SumUp $provider = new GenericProvider([ 'clientId' => $clientId, 'clientSecret' => $clientSecret, @@ -49,20 +50,13 @@ 'urlAuthorize' => 'https://api.sumup.com/authorize', 'urlAccessToken' => 'https://api.sumup.com/token', 'urlResourceOwnerDetails' => '', - // Scope is a mechanism in OAuth 2.0 to limit an application's access to a user's account. - // You should always request the minimal set of scope that you need for your application to - // work. In this example we use "payments transactions.history" scope which gives you access - // to create payments and view transaction history. - 'scopes' => 'payments transactions.history user.profile_readonly user.app-settings', ]); -$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); +$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/'; -switch ($requestUri) { +switch ($path) { case '/': - echo '
This example demonstrates the OAuth2 Authorization Code flow with PKCE.
'; - echo 'Start OAuth2 Flow'; + renderHome(); break; case '/login': @@ -78,124 +72,89 @@ echo 'Not Found'; } +function renderHome(): void +{ + echo ''; + echo ''; + echo 'This example demonstrates the Authorization Code flow with PKCE.
'; + echo ''; + echo ''; + echo ''; +} + function handleLogin(GenericProvider $provider): void { - // Generate random state for security - $state = bin2hex(random_bytes(32)); - - // Generate PKCE challenge and verifier - $codeVerifier = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); - $codeChallenge = rtrim(strtr(base64_encode(hash('sha256', $codeVerifier, true)), '+/', '-_'), '='); - - // Store state and code verifier in session for later verification - $_SESSION[STATE_COOKIE_NAME] = $state; - $_SESSION[PKCE_COOKIE_NAME] = $codeVerifier; - - // Get authorization URL with state and PKCE parameters + $provider->pkceMethod = GenericProvider::PKCE_METHOD_S256; + $authorizationUrl = $provider->getAuthorizationUrl([ - 'state' => $state, - 'code_challenge' => $codeChallenge, - 'code_challenge_method' => 'S256', + 'scope' => SCOPES, ]); + $_SESSION[STATE_SESSION_KEY] = $provider->getState(); + $_SESSION[PKCE_SESSION_KEY] = $provider->getPkceCode(); - // Redirect the user to the authorization URL - header('Location: ' . $authorizationUrl); - exit(); + header('Location: ' . $authorizationUrl, true, 302); + exit; } function handleCallback(GenericProvider $provider): void { - // Verify state parameter to prevent CSRF attacks $state = $_GET['state'] ?? ''; - $sessionState = $_SESSION[STATE_COOKIE_NAME] ?? ''; - - if ($state !== $sessionState) { + $expectedState = $_SESSION[STATE_SESSION_KEY] ?? ''; + + if ($state === '' || !hash_equals($expectedState, $state)) { http_response_code(400); - die('Invalid OAuth state parameter'); + echo 'Invalid OAuth state parameter'; + return; } - // Get the authorization code from the callback $code = $_GET['code'] ?? ''; - if (!$code) { + if ($code === '') { http_response_code(400); - die('Missing authorization code'); + echo 'Missing authorization code'; + return; } - // Get the PKCE code verifier from session - $codeVerifier = $_SESSION[PKCE_COOKIE_NAME] ?? ''; - if (!$codeVerifier) { + $codeVerifier = $_SESSION[PKCE_SESSION_KEY] ?? ''; + if ($codeVerifier === '') { http_response_code(400); - die('Missing PKCE code verifier'); + echo 'Missing PKCE code verifier'; + return; + } + + $merchantCode = $_GET['merchant_code'] ?? ''; + if ($merchantCode === '') { + http_response_code(400); + echo 'Missing merchant_code query parameter'; + return; } try { - // Exchange the authorization code for an access token - /** @var AccessToken $accessToken */ + $provider->pkceMethod = GenericProvider::PKCE_METHOD_S256; + $provider->setPkceCode($codeVerifier); + $accessToken = $provider->getAccessToken('authorization_code', [ 'code' => $code, - 'code_verifier' => $codeVerifier, ]); - // Users might have access to multiple merchant accounts, the `merchant_code` parameter - // returned in the callback is the merchant code of their default merchant account. - // In production, you would want to let users pick which merchant they want to use - // using the memberships API. - $defaultMerchantCode = $_GET['merchant_code'] ?? ''; - - echo 'Successfully obtained access token.
'; - echo 'Merchant Code: ' . htmlspecialchars($defaultMerchantCode) . '
'; - - // Now use the access token with the SumUp SDK $sumup = new \SumUp\SumUp([ 'access_token' => $accessToken->getToken(), ]); - if ($defaultMerchantCode) { - echo '' . htmlspecialchars(json_encode($merchant, JSON_PRETTY_PRINT)) . ''; - } catch (Exception $e) { - echo '
Error fetching merchant: ' . htmlspecialchars($e->getMessage()) . '
'; - } - } - - // Display token information (in production, you would store this securely) - echo 'Token: ' . htmlspecialchars(substr($accessToken->getToken(), 0, 20)) . '...
'; - echo 'Expires: ' . ($accessToken->getExpires() ? date('Y-m-d H:i:s', $accessToken->getExpires()) : 'Never') . '
'; - if ($accessToken->getRefreshToken()) { - echo 'Refresh Token: Available
'; - } - - echo 'Here\'s how you would use the token to create a checkout:
'; - echo '';
- echo htmlspecialchars(' \'' . $accessToken->getToken() . '\',
-]);
-
-$checkout = $sumup->checkouts()->create([
- \'amount\' => 10.00,
- \'currency\' => \'EUR\',
- \'checkout_reference\' => \'my-order-123\',
- \'merchant_code\' => \'' . $defaultMerchantCode . '\',
- \'description\' => \'My product\',
-]);
-
-echo "Checkout ID: " . $checkout->id;');
- echo '';
+ $merchant = $sumup->merchants()->get($merchantCode);
- // Clean up session
- unset($_SESSION[STATE_COOKIE_NAME]);
- unset($_SESSION[PKCE_COOKIE_NAME]);
+ unset($_SESSION[STATE_SESSION_KEY], $_SESSION[PKCE_SESSION_KEY]);
+ header('Content-Type: application/json; charset=utf-8');
+ echo json_encode([
+ 'merchant_code' => $merchantCode,
+ 'merchant' => $merchant,
+ ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
} catch (Exception $e) {
http_response_code(500);
- echo 'Error: ' . htmlspecialchars($e->getMessage()) . '
'; - echo 'Try again'; + header('Content-Type: text/plain; charset=utf-8'); + echo 'OAuth2 error: ' . $e->getMessage(); } }