PluginProbe
Gianism / 1.1
Gianism v1.1
4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.4.0 5.0.0 5.0.1 5.0.2 5.1.0 5.2.1 5.2.2 5.3.0 6.0.0 6.0.1 trunk 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 All 65 releases
gianism / sdks / google / auth / apiOAuth2.php

apiOAuth2.php in Gianism 1.1, at sdks/google/auth/apiOAuth2.php

346 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * Copyright 2008 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
18 require_once "apiVerifier.php";
19 require_once "apiLoginTicket.php";
20 require_once "service/apiUtils.php";
21
22 /**
23 * Authentication class that deals with the OAuth 2 web-server authentication flow
24 *
25 * @author Chris Chabot <chabotc@google.com>
26 * @author Chirag Shah <chirags@google.com>
27 *
28 */
29 class apiOAuth2 extends apiAuth {
30 public $clientId;
31 public $clientSecret;
32 public $developerKey;
33 public $accessToken;
34 public $redirectUri;
35 public $state;
36 public $accessType = 'offline';
37 public $approvalPrompt = 'force';
38
39 const OAUTH2_TOKEN_URI = "https://accounts.google.com/o/oauth2/token";
40 const OAUTH2_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
41 const OAUTH2_FEDERATED_SIGNON_CERTS_URL = "https://www.googleapis.com/oauth2/v1/certs";
42 const CLOCK_SKEW_SECS = 300; // five minutes in seconds
43 const AUTH_TOKEN_LIFETIME_SECS = 300; // five minutes in seconds
44 const MAX_TOKEN_LIFETIME_SECS = 86400; // one day in seconds
45
46 /**
47 * Instantiates the class, but does not initiate the login flow, leaving it
48 * to the discretion of the caller (which is done by calling authenticate()).
49 */
50 public function __construct() {
51 global $apiConfig;
52
53 if (! empty($apiConfig['developer_key'])) {
54 $this->developerKey = $apiConfig['developer_key'];
55 }
56
57 if (! empty($apiConfig['oauth2_client_id'])) {
58 $this->clientId = $apiConfig['oauth2_client_id'];
59 }
60
61 if (! empty($apiConfig['oauth2_client_secret'])) {
62 $this->clientSecret = $apiConfig['oauth2_client_secret'];
63 }
64
65 if (! empty($apiConfig['oauth2_redirect_uri'])) {
66 $this->redirectUri = $apiConfig['oauth2_redirect_uri'];
67 }
68
69 if (! empty($apiConfig['oauth2_access_type'])) {
70 $this->accessType = $apiConfig['oauth2_access_type'];
71 }
72
73 if (! empty($apiConfig['oauth2_approval_prompt'])) {
74 $this->approvalPrompt = $apiConfig['oauth2_approval_prompt'];
75 }
76 }
77
78 public function authenticate($service) {
79 if (isset($_GET['code'])) {
80 // We got here from the redirect from a successful authorization grant, fetch the access token
81 $request = apiClient::$io->makeRequest(new apiHttpRequest(self::OAUTH2_TOKEN_URI, 'POST', array(), array(
82 'code' => $_GET['code'],
83 'grant_type' => 'authorization_code',
84 'redirect_uri' => $this->redirectUri,
85 'client_id' => $this->clientId,
86 'client_secret' => $this->clientSecret
87 )));
88 if ((int)$request->getResponseHttpCode() == 200) {
89 $this->setAccessToken($request->getResponseBody());
90 $this->accessToken['created'] = time();
91 return $this->getAccessToken();
92 } else {
93 $response = $request->getResponseBody();
94 $decodedResponse = json_decode($response, true);
95 if ($decodedResponse != $response && $decodedResponse != null && $decodedResponse['error']) {
96 $response = $decodedResponse['error'];
97 }
98 throw new apiAuthException("Error fetching OAuth2 access token, message: '$response'", $request->getResponseHttpCode());
99 }
100 }
101
102 $authUrl = $this->createAuthUrl($service['scope']);
103 header('Location: ' . $authUrl);
104 }
105
106 public function createAuthUrl($scope) {
107 $params = array(
108 'response_type=code',
109 'redirect_uri=' . urlencode($this->redirectUri),
110 'client_id=' . urlencode($this->clientId),
111 'scope=' . urlencode($scope),
112 'access_type=' . urlencode($this->accessType),
113 'approval_prompt=' . urlencode($this->approvalPrompt)
114 );
115
116 if (isset($this->state)) {
117 $params[] = 'state=' . urlencode($this->state);
118 }
119 $params = implode('&', $params);
120 return self::OAUTH2_AUTH_URL . "?$params";
121 }
122
123 public function setAccessToken($accessToken) {
124 $accessToken = json_decode($accessToken, true);
125 if ($accessToken == null) {
126 throw new apiAuthException("Could not json decode the access token");
127 }
128 if (! isset($accessToken['access_token'])) {
129 throw new apiAuthException("Invalid token format");
130 }
131 $this->accessToken = $accessToken;
132 }
133
134 public function getAccessToken() {
135 return json_encode($this->accessToken);
136 }
137
138 public function setDeveloperKey($developerKey) {
139 $this->developerKey = $developerKey;
140 }
141
142 public function setState($state) {
143 $this->state = $state;
144 }
145
146 public function setAccessType($accessType) {
147 $this->accessType = $accessType;
148 }
149
150 public function setApprovalPrompt($approvalPrompt) {
151 $this->approvalPrompt = $approvalPrompt;
152 }
153
154 public function sign(apiHttpRequest $request) {
155 // add the developer key to the request before signing it
156 if ($this->developerKey) {
157 $request->setUrl($request->getUrl() . ((strpos($request->getUrl(), '?') === false) ? '?' : '&') . 'key=' . urlencode($this->developerKey));
158 }
159
160 // Cannot sign the request without an OAuth access token.
161 if (null == $this->accessToken) {
162 return $request;
163 }
164
165 // If the token is set to expire in the next 30 seconds (or has already
166 // expired), refresh it and set the new token.
167 $expired = ($this->accessToken['created'] + ($this->accessToken['expires_in'] - 30)) < time();
168 if ($expired) {
169 if (! array_key_exists('refresh_token', $this->accessToken)) {
170 throw new apiAuthException("The OAuth 2.0 access token has expired, "
171 . "and a refresh token is not available. Refresh tokens are not "
172 . "returned for responses that were auto-approved.");
173 }
174 $this->refreshToken($this->accessToken['refresh_token']);
175 }
176
177 // Add the OAuth2 header to the request
178 $headers = $request->getHeaders();
179 $headers[] = "Authorization: OAuth " . $this->accessToken['access_token'];
180 $request->setHeaders($headers);
181
182 return $request;
183 }
184
185 /**
186 * @param string $refreshToken
187 * @return void
188 */
189 private function refreshToken($refreshToken) {
190 $params = array(
191 'client_id' => $this->clientId,
192 'client_secret' => $this->clientSecret,
193 'refresh_token' => $refreshToken,
194 'grant_type' => 'refresh_token'
195 );
196 $request = apiClient::$io->makeRequest(
197 new apiHttpRequest(self::OAUTH2_TOKEN_URI, 'POST', array(), $params));
198
199 if ((int)$request->getResponseHttpCode() == 200) {
200 $token = json_decode($request->getResponseBody(), true);
201 if ($token == null) {
202 throw new apiAuthException("Could not json decode the access token");
203 }
204
205 if (! isset($token['access_token']) || ! isset($token['expires_in'])) {
206 throw new apiAuthException("Invalid token format");
207 }
208
209 $this->accessToken['access_token'] = $token['access_token'];
210 $this->accessToken['expires_in'] = $token['expires_in'];
211 $this->accessToken['created'] = time();
212 } else {
213 $response = $request->getResponseBody();
214 $code = $request->getResponseHttpCode();
215 $decodedResponse = json_decode($response, true);
216
217 if ($decodedResponse != null && $decodedResponse['error']) {
218 $response = $decodedResponse['error'];
219 }
220 throw new apiAuthException("Error refreshing the OAuth2 token, message: '$response'", $code);
221 }
222
223 }
224
225 // Gets federated sign-on certificates to use for verifying identity tokens.
226 // Returns certs as array structure, where keys are key ids, and values
227 // are PEM encoded certificates.
228 private function getFederatedSignOnCerts() {
229 // This relies on makeRequest caching certificate responses.
230 $request = apiClient::$io->makeRequest(new apiHttpRequest(
231 self::OAUTH2_FEDERATED_SIGNON_CERTS_URL));
232 if ((int)$request->getResponseHttpCode() == 200) {
233 $certs = json_decode($request->getResponseBody(), true);
234 if ($certs) {
235 return $certs;
236 }
237 }
238 throw new apiAuthException(
239 "Failed to retrieve verification certificates: '" .
240 $request->getResponseBody() . "'.",
241 $request->getResponseHttpCode());
242 }
243
244 /**
245 * Verifies an id token and returns the authenticated apiLoginTicket.
246 * Throws an exception if the id token is not valid.
247 * The audience parameter can be used to control which id tokens are
248 * accepted. By default, the id token must have been issued to this OAuth2 client.
249 *
250 * @param $id_token
251 * @param $audience
252 * @return apiLoginTicket
253 */
254 function verifyIdToken($id_token, $audience = null) {
255 $certs = $this->getFederatedSignonCerts();
256 if (!$audience) {
257 $audience = $this->clientId;
258 }
259 return $this->verifySignedJwtWithCerts($id_token, $certs, $audience);
260 }
261
262 // Verifies the id token, returns the verified token contents.
263 //
264 // Visible for testing.
265 function verifySignedJwtWithCerts($jwt, $certs, $required_audience) {
266 $segments = explode(".", $jwt);
267 if (count($segments) != 3) {
268 throw new apiAuthException("Wrong number of segments in token: $jwt");
269 }
270 $signed = $segments[0] . "." . $segments[1];
271 $signature = apiUtils::urlSafeB64Decode($segments[2]);
272
273 // Parse envelope.
274 $envelope = json_decode(apiUtils::urlSafeB64Decode($segments[0]), true);
275 if (!$envelope) {
276 throw new apiAuthException("Can't parse token envelope: " . $segments[0]);
277 }
278
279 // Parse token
280 $json_body = apiUtils::urlSafeB64Decode($segments[1]);
281 $payload = json_decode($json_body, true);
282 if (!$payload) {
283 throw new apiAuthException("Can't parse token payload: " . $segments[1]);
284 }
285
286 // Check signature
287 $verified = false;
288 foreach ($certs as $keyName => $pem) {
289 $public_key = new apiPemVerifier($pem);
290 if ($public_key->verify($signed, $signature)) {
291 $verified = true;
292 break;
293 }
294 }
295
296 if (!$verified) {
297 throw new apiAuthException("Invalid token signature: $jwt");
298 }
299
300 // Check issued-at timestamp
301 $iat = 0;
302 if (array_key_exists("iat", $payload)) {
303 $iat = $payload["iat"];
304 }
305 if (!$iat) {
306 throw new apiAuthException("No issue time in token: $json_body");
307 }
308 $earliest = $iat - self::CLOCK_SKEW_SECS;
309
310 // Check expiration timestamp
311 $now = time();
312 $exp = 0;
313 if (array_key_exists("exp", $payload)) {
314 $exp = $payload["exp"];
315 }
316 if (!$exp) {
317 throw new apiAuthException("No expiration time in token: $json_body");
318 }
319 if ($exp >= $now + self::MAX_TOKEN_LIFETIME_SECS) {
320 throw new apiAuthException(
321 "Expiration time too far in future: $json_body");
322 }
323
324 $latest = $exp + self::CLOCK_SKEW_SECS;
325 if ($now < $earliest) {
326 throw new apiAuthException(
327 "Token used too early, $now < $earliest: $json_body");
328 }
329 if ($now > $latest) {
330 throw new apiAuthException(
331 "Token used too late, $now > $latest: $json_body");
332 }
333
334 // TODO(beaton): check issuer field?
335
336 // Check audience
337 $aud = $payload["aud"];
338 if ($aud != $required_audience) {
339 throw new apiAuthException("Wrong recipient, $aud != $required_audience: $json_body");
340 }
341
342 // All good.
343 return new apiLoginTicket($envelope, $payload);
344 }
345 }
346