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