psf
oauth.php
1 <?php
2 
3 // Part of php simple framework (psf)
4 
5 // This program is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 
15 // Copyright (c) Petr Bena <petr@bena.rocks> 2015 - 2018
16 
17 if (!defined("PSF_ENTRY_POINT"))
18  die("Not a valid psf entry point");
19 
20 define("OAUTH_OK", 0);
21 define("OAUTH_NOT_AUTH", 2);
22 
23 class PsfOAuth
24 {
25  public $ConsumerKey = null;
26  public $UserAgent = "psf";
27  public $AuthURL = null;
28  public $TokenSecret = null;
29  public $URL = null;
30  public $TokenKey = null;
31  public $ConsumerSecret = null;
32 
33  public function __construct($url)
34  {
35  $this->URL = $url;
36  session_start();
37  if (isset( $_SESSION['tokenKey']))
38  {
39  $o->TokenKey = $_SESSION['tokenKey'];
40  $o->TokenSecret = $_SESSION['tokenSecret'];
41  }
42  session_write_close();
43  }
44 
45  private function SignRequest($method, $url, $params = array())
46  {
47  $parts = parse_url( $url );
48 
49  // We need to normalize the endpoint URL
50  $scheme = isset( $parts['scheme'] ) ? $parts['scheme'] : 'http';
51  $host = isset( $parts['host'] ) ? $parts['host'] : '';
52  $port = isset( $parts['port'] ) ? $parts['port'] : ( $scheme == 'https' ? '443' : '80' );
53  $path = isset( $parts['path'] ) ? $parts['path'] : '';
54  if (( $scheme == 'https' && $port != '443' ) ||
55  ( $scheme == 'http' && $port != '80' ))
56  {
57  // Only include the port if it's not the default
58  $host = "$host:$port";
59  }
60 
61  // Also the parameters
62  $pairs = array();
63  parse_str( isset( $parts['query'] ) ? $parts['query'] : '', $query );
64  $query += $params;
65  unset( $query['oauth_signature'] );
66  if ( $query )
67  {
68  $query = array_combine(
69  // rawurlencode follows RFC 3986 since PHP 5.3
70  array_map( 'rawurlencode', array_keys( $query ) ),
71  array_map( 'rawurlencode', array_values( $query ) )
72  );
73  ksort( $query, SORT_STRING );
74  foreach ( $query as $k => $v ) {
75  $pairs[] = "$k=$v";
76  }
77  }
78 
79  $toSign = rawurlencode( strtoupper( $method ) ) . '&' .
80  rawurlencode( "$scheme://$host$path" ) . '&' .
81  rawurlencode( join( '&', $pairs ) );
82  $key = rawurlencode( $this->ConsumerSecret ) . '&' . rawurlencode( $this->TokenSecret );
83  return base64_encode( hash_hmac( 'sha1', $toSign, $key, true ) );
84  }
85 
86  public function ProcessToken()
87  {
88  $url = $this->URL . '/token';
89  $url .= strpos( $url, '?' ) ? '&' : '?';
90  $url .= http_build_query(array(
91  'format' => 'json',
92  'oauth_verifier' => $_GET['oauth_verifier'],
93 
94  // OAuth information
95  'oauth_consumer_key' => $this->ConsumerKey,
96  'oauth_token' => $this->TokenKey,
97  'oauth_version' => '1.0',
98  'oauth_nonce' => md5( microtime() . mt_rand() ),
99  'oauth_timestamp' => time(),
100 
101  // We're using secret key signatures here.
102  'oauth_signature_method' => 'HMAC-SHA1',
103  ));
104  $signature = $this->SignRequest( 'GET', $url );
105  $url .= "&oauth_signature=" . urlencode( $signature );
106  $ch = curl_init();
107  curl_setopt( $ch, CURLOPT_URL, $url );
108  //curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
109  curl_setopt( $ch, CURLOPT_USERAGENT, $this->UserAgent );
110  curl_setopt( $ch, CURLOPT_HEADER, 0 );
111  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
112  $data = curl_exec( $ch );
113  if ( !$data ) {
114  header( "HTTP/1.1 500 Internal Server Error" );
115  echo 'Curl error: ' . htmlspecialchars( curl_error( $ch ) );
116  exit(0);
117  }
118  curl_close( $ch );
119  $token = json_decode( $data );
120  if ( is_object( $token ) && isset( $token->error ) ) {
121  header( "HTTP/1.1 500 Internal Server Error" );
122  echo 'Error retrieving token: ' . htmlspecialchars( $token->error );
123  exit(0);
124  }
125  if ( !is_object( $token ) || !isset( $token->key ) || !isset( $token->secret ) ) {
126  header( "HTTP/1.1 $errorCode Internal Server Error" );
127  echo 'Invalid response from token request';
128  exit(0);
129  }
130 
131  // Save the access token
132  session_start();
133  $_SESSION['tokenKey'] = $this->TokenKey = $token->key;
134  $_SESSION['tokenSecret'] = $this->TokenSecret = $token->secret;
135  session_write_close();
136  }
137 
138  public function AuthorizationRedirect()
139  {
140  $this->TokenSecret = '';
141  $url = $this->URL . '/initiate';
142  $url .= strpos( $url, '?' ) ? '&' : '?';
143  $url .= http_build_query( array(
144  'format' => 'json',
145  // OAuth information
146  'oauth_callback' => 'oob', // Must be "oob" for MWOAuth
147  'oauth_consumer_key' => $this->ConsumerKey,
148  'oauth_version' => '1.0',
149  'oauth_nonce' => md5( microtime() . mt_rand() ),
150  'oauth_timestamp' => time(),
151  // We're using secret key signatures here.
152  'oauth_signature_method' => 'HMAC-SHA1',
153  ) );
154  $signature = $this->SignRequest( 'GET', $url );
155  $url .= "&oauth_signature=" . urlencode( $signature );
156  $ch = curl_init();
157  curl_setopt( $ch, CURLOPT_URL, $url );
158  SystemLog::Write($url);
159  curl_setopt( $ch, CURLOPT_USERAGENT, $this->UserAgent );
160  curl_setopt( $ch, CURLOPT_HEADER, 0 );
161  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
162  $data = curl_exec( $ch );
163  if ( !$data ) {
164  header( "HTTP/1.1 $errorCode Internal Server Error" );
165  echo 'Curl error: ' . htmlspecialchars( curl_error( $ch ) );
166  exit(0);
167  }
168  curl_close( $ch );
169  echo $data;
170  $token = json_decode( $data );
171  if ( is_object( $token ) && isset( $token->error ) ) {
172  header( "HTTP/1.1 $errorCode Internal Server Error" );
173  echo 'Error retrieving token: ' . htmlspecialchars( $token->error );
174  exit(0);
175  }
176  if ( !is_object( $token ) || !isset( $token->key ) || !isset( $token->secret ) ) {
177  header( "HTTP/1.1 $errorCode Internal Server Error" );
178  echo 'Invalid response from token request';
179  exit(0);
180  }
181 
182  // Now we have the request token, we need to save it for later.
183  session_start();
184  $_SESSION['tokenKey'] = $token->key;
185  $_SESSION['tokenSecret'] = $token->secret;
186  session_write_close();
187 
188  // Then we send the user off to authorize
189  $url = $this->AuthURL;
190  $url .= strpos( $url, '?' ) ? '&' : '?';
191  $url .= http_build_query( array(
192  'oauth_token' => $token->key,
193  'oauth_consumer_key' => $this->ConsumerKey,
194  ) );
195  header( "Location: $url" );
196  echo 'Please see <a href="' . htmlspecialchars( $url ) . '">' . htmlspecialchars( $url ) . '</a>';
197  return true;
198  }
199 
200  public function fetchAccessToken() {
201  $url = $this->OAuth_URL . '/token';
202  $url .= strpos( $url, '?' ) ? '&' : '?';
203  $url .= http_build_query( array(
204  'format' => 'json',
205  'oauth_verifier' => $_GET['oauth_verifier'],
206 
207  // OAuth information
208  'oauth_consumer_key' => $gConsumerKey,
209  'oauth_token' => $gTokenKey,
210  'oauth_version' => '1.0',
211  'oauth_nonce' => md5( microtime() . mt_rand() ),
212  'oauth_timestamp' => time(),
213 
214  // We're using secret key signatures here.
215  'oauth_signature_method' => 'HMAC-SHA1',
216  ) );
217  $signature = $this->SignRequest( 'GET', $url );
218  $url .= "&oauth_signature=" . urlencode( $signature );
219  $ch = curl_init();
220  curl_setopt( $ch, CURLOPT_URL, $url );
221  //curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, false );
222  curl_setopt( $ch, CURLOPT_USERAGENT, $gUserAgent );
223  curl_setopt( $ch, CURLOPT_HEADER, 0 );
224  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
225  $data = curl_exec( $ch );
226  if ( !$data )
227  {
228  header( "HTTP/1.1 500 Internal Server Error" );
229  echo 'Curl error: ' . htmlspecialchars( curl_error( $ch ) );
230  }
231  curl_close( $ch );
232  $token = json_decode( $data );
233  if (is_object( $token ) && isset( $token->error ))
234  {
235  header( "HTTP/1.1 500 Internal Server Error" );
236  throw new Exception('Error retrieving token: ' . htmlspecialchars( $token->error ));
237  }
238  if ( !is_object( $token ) || !isset( $token->key ) || !isset( $token->secret)) {
239  header( "HTTP/1.1 500 Internal Server Error" );
240  throw new Exception('Invalid response from token request');
241  }
242 
243  // Save the access token
244  session_start();
245  $_SESSION['tokenKey'] = $gTokenKey = $token->key;
246  $_SESSION['tokenSecret'] = $gTokenSecret = $token->secret;
247  session_write_close();
248  }
249 
250  public function Identify()
251  {
252  $url = $this->URL . '/identify';
253  $headerArr = array(
254  // OAuth information
255  'oauth_consumer_key' => $this->ConsumerKey,
256  'oauth_token' => $this->TokenKey,
257  'oauth_version' => '1.0',
258  'oauth_nonce' => md5( microtime() . mt_rand() ),
259  'oauth_timestamp' => time(),
260 
261  // We're using secret key signatures here.
262  'oauth_signature_method' => 'HMAC-SHA1',
263  );
264  $signature = $this->SignRequest( 'GET', $url, $headerArr );
265  $headerArr['oauth_signature'] = $signature;
266 
267  $header = array();
268  foreach ( $headerArr as $k => $v )
269  {
270  $header[] = rawurlencode( $k ) . '="' . rawurlencode( $v ) . '"';
271  }
272  $header = 'Authorization: OAuth ' . join( ', ', $header );
273 
274  $ch = curl_init();
275  curl_setopt( $ch, CURLOPT_URL, $url );
276  curl_setopt( $ch, CURLOPT_HTTPHEADER, array( $header ) );
277  curl_setopt( $ch, CURLOPT_USERAGENT, $gUserAgent );
278  curl_setopt( $ch, CURLOPT_HEADER, 0 );
279  curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
280  $data = curl_exec( $ch );
281  if ( !$data ) {
282  header( "HTTP/1.1 $errorCode Internal Server Error" );
283  throw new Exception('Curl error: ' . htmlspecialchars( curl_error( $ch )));
284  }
285  $err = json_decode( $data );
286  if ( is_object( $err ) && isset( $err->error ) && $err->error === 'mwoauthdatastore-access-token-not-found' ) {
287  // We're not authorized!
288  return OAUTH_NOT_AUTH;
289  }
290 
291  // There are three fields in the response
292  $fields = explode( '.', $data );
293  if ( count( $fields ) !== 3 )
294  {
295  header( "HTTP/1.1 $errorCode Internal Server Error" );
296  throw new Exception('Invalid identify response: ' . htmlspecialchars( $data ));
297  }
298 
299  // Validate the header. MWOAuth always returns alg "HS256".
300  $header = base64_decode( strtr( $fields[0], '-_', '+/' ), true );
301  if ( $header !== false )
302  {
303  $header = json_decode( $header );
304  }
305  if ( !is_object( $header ) || $header->typ !== 'JWT' || $header->alg !== 'HS256' )
306  {
307  header( "HTTP/1.1 $errorCode Internal Server Error" );
308  throw new Exception('Invalid header in identify response: ' . htmlspecialchars( $data ));
309  }
310 
311  // Verify the signature
312  $sig = base64_decode( strtr( $fields[2], '-_', '+/' ), true );
313  $check = hash_hmac( 'sha256', $fields[0] . '.' . $fields[1], $gConsumerSecret, true );
314  if ( $sig !== $check )
315  {
316  header( "HTTP/1.1 $errorCode Internal Server Error" );
317  throw new Exception('JWT signature validation failed: ' . htmlspecialchars( $data ) . var_dump( base64_encode($sig), base64_encode($check) ));
318  }
319 
320  // Decode the payload
321  $payload = base64_decode( strtr( $fields[1], '-_', '+/' ), true );
322  if ( $payload !== false )
323  {
324  $payload = json_decode( $payload );
325  }
326  if ( !is_object( $payload ) )
327  {
328  header( "HTTP/1.1 $errorCode Internal Server Error" );
329  throw new Exception('Invalid payload in identify response: ' . htmlspecialchars( $data ));
330  }
331  SystemLog::Write( 'JWT payload: <pre>' . htmlspecialchars( var_export( $payload, 1 ) ) . '</pre>');
332  return OAUTH_OK;
333  }
334 }
335 
336 function OAuthFromIniFile($file)
337 {
338  $r = parse_ini_file($file);
339  if ($r === false)
340  throw new Exception("Unable to read: " . $file);
341 
342  $o = new OAuth($r["url"]);
343  $o->ConsumerSecret = $r["consumerSecret"];
344  $o->ConsumerKey = $r["consumerKey"];
345  return $o;
346 }