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;
26
27/**
28 * Authenticates requests using Google's Service Account credentials via
29 * JWT Access.
30 *
31 * This class allows authorizing requests for service accounts directly
32 * from credentials from a json key file downloaded from the developer
33 * console (via 'Generate new Json Key').  It is not part of any OAuth2
34 * flow, rather it creates a JWT and sends that as a credential.
35 */
36class ServiceAccountJwtAccessCredentials extends CredentialsLoader implements
37    GetQuotaProjectInterface,
38    SignBlobInterface,
39    ProjectIdProviderInterface
40{
41    use ServiceAccountSignerTrait;
42
43    /**
44     * The OAuth2 instance used to conduct authorization.
45     *
46     * @var OAuth2
47     */
48    protected $auth;
49
50    /**
51     * The quota project associated with the JSON credentials
52     *
53     * @var string
54     */
55    protected $quotaProject;
56
57    /**
58     * @var string
59     */
60    public $projectId;
61
62    /**
63     * Create a new ServiceAccountJwtAccessCredentials.
64     *
65     * @param string|array<mixed> $jsonKey JSON credential file path or JSON credentials
66     *   as an associative array
67     * @param string|string[] $scope the scope of the access request, expressed
68     *   either as an Array or as a space-delimited String.
69     */
70    public function __construct($jsonKey, $scope = null)
71    {
72        if (is_string($jsonKey)) {
73            if (!file_exists($jsonKey)) {
74                throw new \InvalidArgumentException('file does not exist');
75            }
76            $jsonKeyStream = file_get_contents($jsonKey);
77            if (!$jsonKey = json_decode((string) $jsonKeyStream, true)) {
78                throw new \LogicException('invalid json for auth config');
79            }
80        }
81        if (!array_key_exists('client_email', $jsonKey)) {
82            throw new \InvalidArgumentException(
83                'json key is missing the client_email field'
84            );
85        }
86        if (!array_key_exists('private_key', $jsonKey)) {
87            throw new \InvalidArgumentException(
88                'json key is missing the private_key field'
89            );
90        }
91        if (array_key_exists('quota_project_id', $jsonKey)) {
92            $this->quotaProject = (string) $jsonKey['quota_project_id'];
93        }
94        $this->auth = new OAuth2([
95            'issuer' => $jsonKey['client_email'],
96            'sub' => $jsonKey['client_email'],
97            'signingAlgorithm' => 'RS256',
98            'signingKey' => $jsonKey['private_key'],
99            'scope' => $scope,
100        ]);
101
102        $this->projectId = isset($jsonKey['project_id'])
103            ? $jsonKey['project_id']
104            : null;
105    }
106
107    /**
108     * Updates metadata with the authorization token.
109     *
110     * @param array<mixed> $metadata metadata hashmap
111     * @param string $authUri optional auth uri
112     * @param callable $httpHandler callback which delivers psr7 request
113     * @return array<mixed> updated metadata hashmap
114     */
115    public function updateMetadata(
116        $metadata,
117        $authUri = null,
118        callable $httpHandler = null
119    ) {
120        $scope = $this->auth->getScope();
121        if (empty($authUri) && empty($scope)) {
122            return $metadata;
123        }
124
125        $this->auth->setAudience($authUri);
126
127        return parent::updateMetadata($metadata, $authUri, $httpHandler);
128    }
129
130    /**
131     * Implements FetchAuthTokenInterface#fetchAuthToken.
132     *
133     * @param callable $httpHandler
134     *
135     * @return null|array{access_token:string} A set of auth related metadata
136     */
137    public function fetchAuthToken(callable $httpHandler = null)
138    {
139        $audience = $this->auth->getAudience();
140        $scope = $this->auth->getScope();
141        if (empty($audience) && empty($scope)) {
142            return null;
143        }
144
145        if (!empty($audience) && !empty($scope)) {
146            throw new \UnexpectedValueException(
147                'Cannot sign both audience and scope in JwtAccess'
148            );
149        }
150
151        $access_token = $this->auth->toJwt();
152
153        // Set the self-signed access token in OAuth2 for getLastReceivedToken
154        $this->auth->setAccessToken($access_token);
155
156        return ['access_token' => $access_token];
157    }
158
159    /**
160     * @return string
161     */
162    public function getCacheKey()
163    {
164        return $this->auth->getCacheKey();
165    }
166
167    /**
168     * @return array<mixed>
169     */
170    public function getLastReceivedToken()
171    {
172        return $this->auth->getLastReceivedToken();
173    }
174
175    /**
176     * Get the project ID from the service account keyfile.
177     *
178     * Returns null if the project ID does not exist in the keyfile.
179     *
180     * @param callable $httpHandler Not used by this credentials type.
181     * @return string|null
182     */
183    public function getProjectId(callable $httpHandler = null)
184    {
185        return $this->projectId;
186    }
187
188    /**
189     * Get the client name from the keyfile.
190     *
191     * In this case, it returns the keyfile's client_email key.
192     *
193     * @param callable $httpHandler Not used by this credentials type.
194     * @return string
195     */
196    public function getClientName(callable $httpHandler = null)
197    {
198        return $this->auth->getIssuer();
199    }
200
201    /**
202     * Get the quota project used for this API request
203     *
204     * @return string|null
205     */
206    public function getQuotaProject()
207    {
208        return $this->quotaProject;
209    }
210}
211