-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathGOAuth2TokenServer.php
More file actions
520 lines (431 loc) · 19.5 KB
/
GOAuth2TokenServer.php
File metadata and controls
520 lines (431 loc) · 19.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
<?php
require_once 'GOAuth2.php';
/**
* OAuth2.0-compliant token endpoint.
* @package GOAuth2
*/
abstract class GOAuth2TokenServer {
// The type of token the token server hands out.
protected $token_type;
// The method the token server uses to authenticate clients.
protected $client_auth_method;
// The algorithm used to generate HMAC signature.
protected $hmac_algorithm;
// Whether SSL is enforced.
protected $enforce_ssl;
// Whether the token server supports refresh tokens.
protected $support_refresh;
// An array of URIs, indexed by error type, that may be provided to the client.
protected $error_uris = array();
/**
* Class constructor.
*
* @param String $token_type The type of token this server distributes.
* Defaults to MAC token type.
* @param String $client_auth_method The method that clients should use to authenticate
* themselves to the server. Defaults to client credentials,
* which requires client_id and client_secret to be passed
* with each request.
* @param Bool $enforce_ssl Whether to enforce SSL or not (highly recommended for
* production websites). Defaults to true.
* @param Bool $support_refresh Whether the token server should support refresh tokens.
* @param String $hmac_algorithm The HMAC algorithm the server should use when calculating
* comparison signature if the MAC token type is specified.
* Defaults to SHA1.
*
* @return GOAuth2TokenServer
*/
public function __construct($token_type = GOAuth2::TOKEN_TYPE_MAC, $client_auth_method = GOAuth2::SERVER_AUTH_TYPE_CREDENTIALS, $enforce_ssl = true, $support_refresh = true, $hmac_algorithm = GOAuth2::HMAC_SHA1) {
$this->token_type = $token_type;
$this->client_auth_method = $client_auth_method;
$this->enforce_ssl = $enforce_ssl;
$this->support_refresh = $support_refresh;
$this->hmac_algorithm = $hmac_algorithm;
}
/**
* Handle a request for a token to the token endpoint.
*
* @param array $post The POST fields of the request.
* @param String $authorization_header The Authorization header field.
*/
public function handleTokenRequest($post, $authorization_header) {
// Check SSL
if($this->enforce_ssl) {
if($_SERVER['HTTPS'] !== 'on') {
throw new GOAuth2SSLException('Attempted to connect to GOAuth2 token server over non-HTTPS channel.');
}
}
// Check for required parameters.
if(!isset($post['grant_type'])) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_REQUEST);
}
// Handle the token request depending on its type.
switch($post['grant_type']) {
case GOAuth2::GRANT_TYPE_CODE:
$this->handleTokenRequestWithAuthorizationCode($post, $authorization_header);
break;
case GOAuth2::GRANT_TYPE_CLIENT_CREDENTIALS:
$this->handleTokenRequestWithClientCredentials($post, $authorization_header);
break;
case GOAuth2::GRANT_TYPE_PASSWORD:
$this->handleTokenRequestWithPassword($post, $authorization_header);
break;
case GOAuth2::GRANT_TYPE_REFRESH_TOKEN:
// Only process if this server supports refresh tokens. Otherwise,
// the break is skipped and the unsupported grant type response is sent.
if($this->support_refresh) {
$this->handleTokenRefreshRequest($post, $authorization_header);
break;
}
default:
$this->sendErrorResponse(GOAuth2::ERROR_UNSUPPORTED_GRANT_TYPE);
}
}
/**
* Handle a request for an access token using a previously obtained
* authorization code. This is the flow used when a client would like
* to obtain access on behalf of an end-user.
*
* @param array $post The POST array given with the request.
* @param String $authorization_header The contents of the Authorization: header.
*/
private function handleTokenRequestWithAuthorizationCode($post, $authorization_header) {
// Get the authorization code and redirect URI from the POST
$client_id = isset($post['client_id']) ? $post['client_id'] : null;
$client_secret = isset($post['client_secret']) ? $post['client_secret'] : null;
$code = isset($post['code']) ? $post['code'] : null;
$redirect_uri = isset($post['redirect_uri']) ? $post['redirect_uri'] : null;
// Authenticate the client request
$this->authenticateClientRequest($client_id, $client_secret, $authorization_header);
// Check that a code and redirect_uri was passed
if(empty($code) || empty($redirect_uri)) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_REQUEST);
}
// @todo: There are issues here if not using the client credentials means of authorization.
// Validate the authorization code information
$this->validateAuthorizationCode($client_id, $code, $redirect_uri);
// Generate a token from the authorization code
$token = $this->generateAccessTokenFromAuthorizationCode($code);
// Send the generated token back to the client
$this->sendResponse(GOAuth2::HTTP_200, $token->toJSON(), GOAuth2::CONTENT_TYPE_JSON, $no_store = true);
}
/**
* Handle a request for an access token using just the client credentials.
* This flow is used when the client would like to obtain access on behalf
* of itself.
*
* @param array $post The POST array given with the request.
* @param String $authorization_header The contents of the Authorization: header.
*/
private function handleTokenRequestWithClientCredentials($post, $authorization_header) {
// Get the client_id, client_secret and scope parameters from the POST if present.
$client_id = isset($post['client_id']) ? $post['client_id'] : null;
$client_secret = isset($post['client_secret']) ? $post['client_secret'] : null;
$scope = isset($post['scope']) ? $post['scope'] : null;
// Authenticate the client request
$this->authenticateClientRequest($client_id, $client_secret, $authorization_header);
// Check that the scope requested is permissible
$this->checkTokenRequestScope($client_id, $user = null, $scope);
// Get a new access token for the client
$token = $this->generateAccessToken($client_id, $for_user = null, $scope);
// Send the generated token back to the client
$this->sendResponse(GOAuth2::HTTP_200, $token->toJSON(), GOAuth2::CONTENT_TYPE_JSON, $no_store = true);
}
/**
* Handle a request to obtain an access token with the resource owner username
* and password.
*
* @param array $post The POST array of the request.
* @param String $authorization_header The contents of the Authorization header.
*/
private function handleTokenRequestWithPassword($post, $authorization_header) {
// Get the client_id, client_secret, username, password and scope parameters from the POST if present.
$client_id = isset($post['client_id']) ? $post['client_id'] : null;
$client_secret = isset($post['client_secret']) ? $post['client_secret'] : null;
$username = isset($post['username']) ? $post['username'] : null;
$password = isset($post['password']) ? $post['password'] : null;
$scope = isset($post['scope']) ? $post['scope'] : null;
// Authenticate the client request
$this->authenticateClientRequest($client_id, $client_secret, $authorization_header);
// Check that a username and password was passed
if(empty($username) || empty($password)) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_REQUEST);
}
// Validate the resource owner credentials
$user = $this->validateResourceOwnerCredentials($username, $password);
// Check that the scope requested is permissible
$this->checkTokenRequestScope($client_id, $user, $scope);
// Get a new token
$token = $this->generateAccessToken($client_id, $user, $scope);
$this->sendResponse(GOAuth2::HTTP_200, $token->toJSON(), GOAuth2::CONTENT_TYPE_JSON, $no_store = true);
}
/**
* Handle a request to refresh an access token with the given refresh token.
*
* @param array $post The POST array of the request.
* @param String $authorization_header The contents of the Authorization header.
*/
private function handleTokenRefreshRequest($post, $authorization_header) {
// Get the client_id, client_secret, refresh token and scope parameters from the POST if present.
$client_id = isset($post['client_id']) ? $post['client_id'] : null;
$client_secret = isset($post['client_secret']) ? $post['client_secret'] : null;
$refresh_token = isset($post['refresh_token']) ? $post['refresh_token'] : null;
$scope = isset($post['scope']) ? $post['scope'] : null;
// Authenticate the client request
$this->authenticateClientRequest($client_id, $client_secret, $scope, $authorization_header);
// Check that a refresh token was passed
if(empty($refresh_token)) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_REQUEST);
}
// Refresh the access token
$token = $this->refreshAccessToken($client_id, $refresh_token, $scope);
// Send the generated token back to the client
$this->sendResponse(GOAuth2::HTTP_200, $token->toJSON(), GOAuth2::CONTENT_TYPE_JSON, $no_store = true);
}
/**
* Authenticate a request from a client using the authentication method
* specified by the server. This will most often be using the method
* specified in s3.1 of the OAuth specification, namely the presence of
* a client_id and client_secret POST field.
*
* However, as noted in the specification, other authentication methods
* (such as HTTP BASIC) or anonymous access may be permitted.
*
* @param String $client_id The POSTs client_id field, or null if not present.
* @param String $client_secret The POSTs client_secret field, or null if not present.
* @param String $authorization_header The Authorization: header of the request.
*/
private function authenticateClientRequest($client_id, $client_secret, $authorization_header) {
switch($this->client_auth_method) {
case GOAuth2::SERVER_AUTH_TYPE_ANONYMOUS:
// Anonymous access is permitted.
return;
case GOAuth2::SERVER_AUTH_TYPE_HTTP_BASIC:
// Extracted the base64-encoded string from the header.
if(!preg_match('/^Authorization:\s+Basic\s+(\w+==)$/', $authorization_header, $matches)) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_CLIENT);
}
// Decode the authorization information and check that it's in u:p form
try {
list($_, $authorization_string) = $matches;
list($client_id, $client_secret) = explode(':', base64_decode($authorization_string));
} catch(Exception $e) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_CLIENT);
}
// Authenticate the HTTP BASIC credentials.
// NB: Currently we assume that the credentials being passed in the BASIC header
// are the client_id and client_secret.
// @todo: Generalise this.
$this->authenticateClientCredentials($client_id, $client_secret);
// Authentication was successful.
return;
case GOAuth2::SERVER_AUTH_TYPE_CREDENTIALS:
// Using the (default) credentials method.
// Check for client_id and client_secret, required for request.
if(empty($client_id) || empty($client_secret)) {
$this->sendErrorResponse(GOAuth2::ERROR_INVALID_CLIENT);
}
// Authenticate the client id and client secret
$this->authenticateClientCredentials($client_id, $client_secret);
// Authentication was successful.
return;
default:
// Unknown authentication method.
throw new Exception('Unknown server authentication method specified in server configuration.');
return;
}
}
/**
* Send an error response from the server as specified by the OAuth 2.0
* specification. This requires a JSON response with an "error" field
* and optional description and URI fields.
*
* @param String $error A string representing one of the error types
* specified in s5.2 of the OAuth 2.0 spec, eg
* 'invalid_request' or 'invalid_scope'.
*/
protected function sendErrorResponse($error = GOAuth2::ERROR_INVALID_REQUEST) {
// Create the JSON response object
$error_object = array(
'error' => $error,
'error_description' => GOAuth2::getErrorDescription($error)
);
// Append the error URI if defined
if(isset($this->error_uris[$error])) {
$error_object['error_uri'] = $this->error_uris[$error];
}
// Encode the error into JSON
$error_json = json_encode($error_object);
// Get the appropriate HTTP response code for this type of error
$http_response_code = GOAuth2::getErrorHttpStatusCode($error);
// Send the HTTP response
$this->sendResponse($http_response_code, $error_json);
}
/**
* Send an HTTP response to the client.
*
* @param int $status The HTTP response status code to send.
* @param String $response The body of the response.
* @param String $content_type Optional .The content type of the response.
* Defaults to 'application/json'.
* @param bool $no_store Send Cache-control no-store header
*/
protected function sendResponse($status, $response, $content_type = GOAuth2::CONTENT_TYPE_JSON, $no_store = false) {
// Clean the output buffer to eliminate any whitespace.
@ob_end_clean();
// Set the response status code
header($status);
// Set the content type of the response
header("Content-Type: $content_type");
// Set the Cache-Control: no-store header if desired
if($no_store) {
header("Cache-Control: no-store");
}
// Send the response text
echo $response;
// Cease processing
exit;
}
/**
* FUNCTIONS WHICH REQUIRE REIMPLEMENTATION
*
* The following functions MUST be implemented in any inheriting subclass.
*/
/**
* Check that the specified client is permitted to obtain an access token
* of the specified scope for the specified user. Both the user and scope
* parameters are optional, as a client may request a token for themselves
* (not on behalf of a user) and the 'scope' parameter is an optional
* request parameter. There may be only one default scope or your server
* implementation may treat a lack of scope specificity as a request for
* the maximum permitted scope.
*
* This function MUST be reimplemented in the inheriting subclass.
*
* @param String $client_id The ID of the client who is requesting the token.
* @param mixed $user Optional. If given, represents the means of
* uniquely identifying the resource owner returned
* by validateResourceOwnerCredentials().
* @param String $scope Optional. If given, is a space-delimited string of
* requested scopes.
*/
protected function checkTokenRequestScope($client_id, $user = null, $scope = null) {
throw new Exception('checkTokenRequestScope() not implemented by server.');
}
/**
* Generate and store an access token for the given client with
* the given scope.
*
* This function MUST be reimplemented in the inheriting subclass.
*
* @param String $client_id The ID of the client to be given
* the token.
* @param String $user Optional. If given, represents
* the username of the resource
* owner on whose behalf the token
* is being generated.
* @param String $scope Optional. If given, a string of
* space-delimited scope names.
*/
protected function generateAccessToken($client_id, $user = null, $scope = null) {
throw new Exception('generateAccessToken() not implemented by server.');
}
/**
* FUNCTIONS THAT MUST BE REIMPLEMENTED IN SOME SCENARIOS
*
* The following functions MUST be reimplemented in any inheriting subclass
* IF the inheriting Token Server needs to support the relevant feature.
*/
/**
* Refresh and store an access token for the given client with
* the given scope.
*
* This function MUST be reimplemented in the inheriting subclass if
* the Token Server is to support refresh tokens.
*
* @param String $client_id The ID of the client to be given
* the token.
* @param String $refresh_token The refresh token provided by
* the client.
* @param String $scope Optional. If given, a string of
* space-delimited scope names.
*/
protected function refreshAccessToken($client_id, $refresh_token, $scope = null) {
throw new Exception('refreshAccessToken() not implemented by server.');
}
/**
* Generate and store an access token from the given authorization
* code. This function must only be called after the code has been
* validated.
*
* This function MUST be reimplemented if the server utilises the
* authorization code flow.
*
* @param String $code The authorization code.
* @return GOAuth2AccessToken
*/
protected function generateAccessTokenFromAuthorizationCode($code) {
throw new Exception('generateAccessTokenFromAuthorizationCode() not implemented by server.');
}
/**
* Authenticate the given client credentials. This function must be
* reimplemented in the inheriting server subclass if that server
* utilises the client credentials authentication method. The function
* implementation MUST call the sendErrorResponse() method on a failed
* authentication.
*
* @param String $client_id
* @param String $client_secret
*/
protected function authenticateClientCredentials($client_id, $client_secret) {
throw new Exception('authenticateClientCredentials() not implemented by server.');
}
/**
* Validate the given resource owner credentials, and return a value
* that the server uses to uniquely identify the specific resource
* owner (usually a user ID). This function must be reimplemented in
* the inheriting subclass if the server needs to support the resource
* owner credentials flow of access token grant.
*
* The OAuth specification notes that this flow should only be used
* where there is a high level of trust between the resource owner
* and the client, and should only be used where other flows aren't
* available. It is also used when migrating a client from a
* stored-password approach to an access token approach.
*
* @param String $username The username of the resource owner.
* @param String $password The (plaintext) password of the resource
* owner.
*
* @return mixed $user A means for the resource server to
* uniquely identify the resource owner.
* This will typically be a user ID, or
* perhaps a username. This return value
* is passed to the access token generating
* function.
*/
protected function validateResourceOwnerCredentials($username, $password) {
// By default, we don't support the password grant type.
$this->sendErrorResponse(GOAuth2::ERROR_UNSUPPORTED_GRANT_TYPE);
}
/**
* Validate the given authorization code details. This function must
* be reimplemented in the inheriting subclass if the server needs to
* support the authorization code flow of access token grant.
*
* @param String $client_id The ID of the client requesting the
* token. This MUST be checked against
* the ID of the client that was given
* the authorization code by the
* authorization server.
* @param String $code The authorization code.
* @param String $redirect_uri The redirect URI the client claims it
* obtained during the authorization
* process. This MUST be checked against
* the redirect URI logged by the
* authorization server.
*/
protected function validateAuthorizationCode($client_id, $code, $redirect_uri) {
throw new Exception('validateAuthorizationCode() not implemented by server.');
}
}