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