1<?php
2/*
3 * Copyright 2015 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18namespace Google\Auth\Credentials;
19
20use Google\Auth\CredentialsLoader;
21use Google\Auth\GetQuotaProjectInterface;
22use Google\Auth\OAuth2;
23use Google\Auth\ProjectIdProviderInterface;
24use Google\Auth\ServiceAccountSignerTrait;
25use Google\Auth\SignBlobInterface;
26use InvalidArgumentException;
27
28/**
29 * ServiceAccountCredentials supports authorization using a Google service
30 * account.
31 *
32 * (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount)
33 *
34 * It's initialized using the json key file that's downloadable from developer
35 * console, which should contain a private_key and client_email fields that it
36 * uses.
37 *
38 * Use it with AuthTokenMiddleware to authorize http requests:
39 *
40 *   use Google\Auth\Credentials\ServiceAccountCredentials;
41 *   use Google\Auth\Middleware\AuthTokenMiddleware;
42 *   use GuzzleHttp\Client;
43 *   use GuzzleHttp\HandlerStack;
44 *
45 *   $sa = new ServiceAccountCredentials(
46 *       'https://www.googleapis.com/auth/taskqueue',
47 *       '/path/to/your/json/key_file.json'
48 *   );
49 *   $middleware = new AuthTokenMiddleware($sa);
50 *   $stack = HandlerStack::create();
51 *   $stack->push($middleware);
52 *
53 *   $client = new Client([
54 *       'handler' => $stack,
55 *       'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
56 *       'auth' => 'google_auth' // authorize all requests
57 *   ]);
58 *
59 *   $res = $client->get('myproject/taskqueues/myqueue');
60 */
61class ServiceAccountCredentials extends CredentialsLoader implements
62    GetQuotaProjectInterface,
63    SignBlobInterface,
64    ProjectIdProviderInterface
65{
66    use ServiceAccountSignerTrait;
67
68    /**
69     * The OAuth2 instance used to conduct authorization.
70     *
71     * @var OAuth2
72     */
73    protected $auth;
74
75    /**
76     * The quota project associated with the JSON credentials
77     *
78     * @var string
79     */
80    protected $quotaProject;
81
82    /*
83     * @var string|null
84     */
85    protected $projectId;
86
87    /*
88     * @var array|null
89     */
90    private $lastReceivedJwtAccessToken;
91
92    /**
93     * Create a new ServiceAccountCredentials.
94     *
95     * @param string|array $scope the scope of the access request, expressed
96     *   either as an Array or as a space-delimited String.
97     * @param string|array $jsonKey JSON credential file path or JSON credentials
98     *   as an associative array
99     * @param string $sub an email address account to impersonate, in situations when
100     *   the service account has been delegated domain wide access.
101     * @param string $targetAudience The audience for the ID token.
102     */
103    public function __construct(
104        $scope,
105        $jsonKey,
106        $sub = null,
107        $targetAudience = null
108    ) {
109        if (is_string($jsonKey)) {
110            if (!file_exists($jsonKey)) {
111                throw new \InvalidArgumentException('file does not exist');
112            }
113            $jsonKeyStream = file_get_contents($jsonKey);
114            if (!$jsonKey = json_decode($jsonKeyStream, true)) {
115                throw new \LogicException('invalid json for auth config');
116            }
117        }
118        if (!array_key_exists('client_email', $jsonKey)) {
119            throw new \InvalidArgumentException(
120                'json key is missing the client_email field'
121            );
122        }
123        if (!array_key_exists('private_key', $jsonKey)) {
124            throw new \InvalidArgumentException(
125                'json key is missing the private_key field'
126            );
127        }
128        if (array_key_exists('quota_project_id', $jsonKey)) {
129            $this->quotaProject = (string) $jsonKey['quota_project_id'];
130        }
131        if ($scope && $targetAudience) {
132            throw new InvalidArgumentException(
133                'Scope and targetAudience cannot both be supplied'
134            );
135        }
136        $additionalClaims = [];
137        if ($targetAudience) {
138            $additionalClaims = ['target_audience' => $targetAudience];
139        }
140        $this->auth = new OAuth2([
141            'audience' => self::TOKEN_CREDENTIAL_URI,
142            'issuer' => $jsonKey['client_email'],
143            'scope' => $scope,
144            'signingAlgorithm' => 'RS256',
145            'signingKey' => $jsonKey['private_key'],
146            'sub' => $sub,
147            'tokenCredentialUri' => self::TOKEN_CREDENTIAL_URI,
148            'additionalClaims' => $additionalClaims,
149        ]);
150
151        $this->projectId = isset($jsonKey['project_id'])
152            ? $jsonKey['project_id']
153            : null;
154    }
155
156    /**
157     * @param callable $httpHandler
158     *
159     * @return array A set of auth related metadata, containing the following
160     * keys:
161     *   - access_token (string)
162     *   - expires_in (int)
163     *   - token_type (string)
164     */
165    public function fetchAuthToken(callable $httpHandler = null)
166    {
167        return $this->auth->fetchAuthToken($httpHandler);
168    }
169
170    /**
171     * @return string
172     */
173    public function getCacheKey()
174    {
175        $key = $this->auth->getIssuer() . ':' . $this->auth->getCacheKey();
176        if ($sub = $this->auth->getSub()) {
177            $key .= ':' . $sub;
178        }
179
180        return $key;
181    }
182
183    /**
184     * @return array
185     */
186    public function getLastReceivedToken()
187    {
188        // If self-signed JWTs are being used, fetch the last received token
189        // from memory. Else, fetch it from OAuth2
190        return $this->useSelfSignedJwt()
191            ? $this->lastReceivedJwtAccessToken
192            : $this->auth->getLastReceivedToken();
193    }
194
195    /**
196     * Get the project ID from the service account keyfile.
197     *
198     * Returns null if the project ID does not exist in the keyfile.
199     *
200     * @param callable $httpHandler Not used by this credentials type.
201     * @return string|null
202     */
203    public function getProjectId(callable $httpHandler = null)
204    {
205        return $this->projectId;
206    }
207
208    /**
209     * Updates metadata with the authorization token.
210     *
211     * @param array $metadata metadata hashmap
212     * @param string $authUri optional auth uri
213     * @param callable $httpHandler callback which delivers psr7 request
214     * @return array updated metadata hashmap
215     */
216    public function updateMetadata(
217        $metadata,
218        $authUri = null,
219        callable $httpHandler = null
220    ) {
221        // scope exists. use oauth implementation
222        if (!$this->useSelfSignedJwt()) {
223            return parent::updateMetadata($metadata, $authUri, $httpHandler);
224        }
225
226        // no scope found. create jwt with the auth uri
227        $credJson = array(
228            'private_key' => $this->auth->getSigningKey(),
229            'client_email' => $this->auth->getIssuer(),
230        );
231        $jwtCreds = new ServiceAccountJwtAccessCredentials($credJson);
232
233        $updatedMetadata = $jwtCreds->updateMetadata($metadata, $authUri, $httpHandler);
234
235        if ($lastReceivedToken = $jwtCreds->getLastReceivedToken()) {
236            // Keep self-signed JWTs in memory as the last received token
237            $this->lastReceivedJwtAccessToken = $lastReceivedToken;
238        }
239
240        return $updatedMetadata;
241    }
242
243    /**
244     * @param string $sub an email address account to impersonate, in situations when
245     *   the service account has been delegated domain wide access.
246     */
247    public function setSub($sub)
248    {
249        $this->auth->setSub($sub);
250    }
251
252    /**
253     * Get the client name from the keyfile.
254     *
255     * In this case, it returns the keyfile's client_email key.
256     *
257     * @param callable $httpHandler Not used by this credentials type.
258     * @return string
259     */
260    public function getClientName(callable $httpHandler = null)
261    {
262        return $this->auth->getIssuer();
263    }
264
265    /**
266     * Get the quota project used for this API request
267     *
268     * @return string|null
269     */
270    public function getQuotaProject()
271    {
272        return $this->quotaProject;
273    }
274
275    private function useSelfSignedJwt()
276    {
277        return is_null($this->auth->getScope());
278    }
279}
280