1: <?php
2: namespace Worldline\Acquiring\Sdk\Authentication;
3:
4: use InvalidArgumentException;
5: use Worldline\Acquiring\Sdk\Communication\Connection;
6: use Worldline\Acquiring\Sdk\Communication\DefaultConnection;
7: use Worldline\Acquiring\Sdk\Communication\ResponseBuilder;
8: use Worldline\Acquiring\Sdk\CommunicatorConfiguration;
9: use Worldline\Acquiring\Sdk\JSON\JSONUtil;
10:
11: /**
12: * Class OAuth2Authenticator
13: *
14: * @package Worldline\Acquiring\Sdk\Authentication
15: */
16: class OAuth2Authenticator implements Authenticator
17: {
18: // Only a limited amount of scopes may be sent in one request.
19: // While at the moment all scopes fit in one request, keep this code so we can easily add more token types if necessary.
20: // The empty path will ensure that all paths will match, as each full path ends with an empty string.
21: private const TOKEN_TYPES = [
22: '' => [
23: 'processing_payment', 'processing_refund', 'processing_credittransfer', 'processing_accountverification',
24: 'processing_operation_reverse', 'processing_dcc_rate', 'services_ping'
25: ],
26: ];
27:
28: /** @var string */
29: private $oauth2TokenUri;
30:
31: /** @var string */
32: private $oauth2ClientId;
33:
34: /** @var string */
35: private $oauth2ClientSecret;
36:
37: /** @var OAuth2TokenCache */
38: private $tokenCache;
39:
40: /** @var CommunicatorConfiguration */
41: private $communicatorConfiguration;
42:
43: /**
44: * @param CommunicatorConfiguration $communicatorConfiguration
45: * @param string $oauth2TokenUri
46: * @param OAuth2TokenCache|null $tokenCache
47: */
48: public function __construct(
49: CommunicatorConfiguration $communicatorConfiguration,
50: $oauth2TokenUri,
51: OAuth2TokenCache $tokenCache = null
52: ) {
53: $this->communicatorConfiguration = $communicatorConfiguration;
54: $this->oauth2TokenUri = $oauth2TokenUri;
55: $this->oauth2ClientId = $communicatorConfiguration->getOAuth2ClientId();
56: $this->oauth2ClientSecret = $communicatorConfiguration->getOAuth2ClientSecret();
57: $this->tokenCache = $tokenCache ?: new DefaultOAuth2TokenCache();
58: }
59:
60: /**
61: * @param string $httpMethod
62: * @param string $uriPath
63: * @param string[] $requestHeaders
64: * @return string
65: */
66: public function getAuthorization($httpMethod, $uriPath, $requestHeaders)
67: {
68: $tokenType = self::getTokenType($uriPath);
69: $oauth2AccessToken = $this->tokenCache->getOAuth2AccessToken($tokenType);
70: if ($oauth2AccessToken) {
71: return 'Bearer ' . $oauth2AccessToken;
72: }
73:
74: $startTime = time();
75:
76: $requestHeaders = array();
77: $requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
78:
79: $requestBody = sprintf('grant_type=client_credentials&client_id=%s&client_secret=%s&scope=%s', $this->oauth2ClientId, $this->oauth2ClientSecret, self::getScopes($tokenType));
80:
81: $responseBuilder = new ResponseBuilder();
82: $responseHandler = function ($httpStatusCode, $data, $headers) use ($responseBuilder) {
83: $responseBuilder->setHttpStatusCode($httpStatusCode);
84: $responseBuilder->setHeaders($headers);
85: $responseBuilder->appendBody($data);
86: };
87:
88: $connection = $this->createConnection();
89: $connection->post($this->oauth2TokenUri, $requestHeaders, $requestBody, $responseHandler);
90:
91: $response = $responseBuilder->getResponse();
92:
93: $responseObject = JSONUtil::decode($response->getBody());
94:
95: if ($response->getHttpStatusCode() !== 200) {
96: throw new OAuth2Exception(sprintf(
97: 'There was an error while retrieving the OAuth2 access token: %s - %s',
98: $responseObject->error,
99: $responseObject->error_description
100: ));
101: }
102: $oauth2AccessToken = $responseObject->access_token;
103: $expirationTimestamp = $startTime + $responseObject->expires_in;
104: $this->tokenCache->storeOAuth2AccessToken($tokenType, $oauth2AccessToken, $expirationTimestamp);
105:
106: return 'Bearer ' . $oauth2AccessToken;
107: }
108:
109: /**
110: * @return Connection
111: */
112: protected function createConnection()
113: {
114: return new DefaultConnection($this->communicatorConfiguration);
115: }
116:
117: private static function getTokenType($fullPath)
118: {
119: foreach (self::TOKEN_TYPES as $tokenType => $scopes) {
120: if (self::endsWith($fullPath, $tokenType) || self::contains($fullPath, $tokenType . '/')) {
121: return $tokenType;
122: }
123: }
124:
125: throw new InvalidArgumentException("Scope could not be found for path $fullPath");
126: }
127:
128: private static function getScopes($tokenType)
129: {
130: if (!array_key_exists($tokenType, self::TOKEN_TYPES)) {
131: throw new InvalidArgumentException("Token type $tokenType does not exist.");
132: }
133:
134: return join(" ", self::TOKEN_TYPES[$tokenType]);
135: }
136:
137: private static function endsWith($haystack, $needle)
138: {
139: return substr_compare($haystack, $needle, -strlen($needle)) === 0;
140: }
141:
142: private static function contains($haystack, $needle)
143: {
144: return strpos($haystack, $needle) !== false;
145: }
146: }
147: