oauth1.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <?php
  2. /**
  3. * Spec OAuth1 implementation for services using OAuth for authentication.
  4. * You will want to define request, access and authorize endpoints. Keyring
  5. * will walk the user through the OAuth dance. Once an access token is
  6. * obtained, it's considered verified. You may still want to do an additional
  7. * request to get some details or verify something specific. To do that, hook
  8. * something to 'keyring_SERVICE_post_verification' (see Keyring_Service::verified())
  9. *
  10. * @package Keyring
  11. */
  12. class Keyring_Service_OAuth1 extends Keyring_Service {
  13. protected $request_token_url = ''; // @see ::set_endpoint()
  14. protected $request_token_method = 'GET';
  15. protected $access_token_url = '';
  16. protected $access_token_method = 'GET';
  17. protected $authorize_url = '';
  18. protected $authorize_method = 'GET';
  19. protected $consumer = null;
  20. protected $signature_method = null;
  21. protected $callback_url = null;
  22. var $app_id = null;
  23. var $key = null;
  24. var $secret = null;
  25. var $token = null;
  26. var $authorization_header = false;
  27. var $authorization_realm = '';
  28. function __construct() {
  29. parent::__construct();
  30. // Nonces for the callback URL, which is used during the verify step
  31. $kr_nonce = wp_create_nonce( 'keyring-verify' );
  32. $nonce = wp_create_nonce( 'keyring-verify-' . $this->get_name() );
  33. $this->callback_url = Keyring_Util::admin_url( $this->get_name(), array( 'action' => 'verify', 'kr_nonce' => $kr_nonce, 'nonce' => $nonce ) );
  34. if ( !class_exists( 'OAuthRequest' ) )
  35. require dirname( dirname( dirname( __FILE__ ) ) ) . '/oauth-php/OAuth.php';
  36. }
  37. /**
  38. * OAuth services always require a key and a secret
  39. */
  40. function is_configured() {
  41. $creds = $this->get_credentials();
  42. return !empty( $creds['key'] ) && !empty( $creds['secret'] );
  43. }
  44. function request_token() {
  45. Keyring_Util::debug( 'Keyring_Service_OAuth1::request_token()' );
  46. if ( !isset( $_REQUEST['nonce'] ) || !wp_verify_nonce( $_REQUEST['nonce'], 'keyring-request-' . $this->get_name() ) ) {
  47. Keyring::error( __( 'Invalid/missing request nonce.', 'keyring' ) );
  48. exit;
  49. }
  50. // Need to create a request token now, so that we have a state to pass
  51. $request_token = new Keyring_Request_Token(
  52. $this->get_name(),
  53. array(),
  54. apply_filters(
  55. 'keyring_request_token_meta',
  56. array(
  57. 'for' => isset( $_REQUEST['for'] ) ? (string) $_REQUEST['for'] : false,
  58. 'type' => 'request',
  59. 'user_id' => get_current_user_id(),
  60. 'blog_id' => get_current_blog_id(),
  61. ),
  62. $this->get_name(),
  63. array(), // no token
  64. $this
  65. )
  66. );
  67. $request_token = apply_filters( 'keyring_request_token', $request_token, $this );
  68. $request_token_id = $this->store_token( $request_token );
  69. $scope = apply_filters( 'keyring_' . $this->get_name() . '_request_scope', false );
  70. Keyring_Util::debug( 'OAuth1 Stored Request token ' . $request_token_id );
  71. $request_token_url = add_query_arg( array(
  72. 'oauth_callback' =>
  73. urlencode(
  74. add_query_arg(
  75. array(
  76. 'state' => $request_token_id,
  77. ),
  78. $this->callback_url
  79. )
  80. ),
  81. 'scope' => $scope,
  82. ),
  83. $this->request_token_url
  84. );
  85. // Set up OAuth request
  86. $req = $this->prepare_request( null, $this->request_token_method, $request_token_url, false );
  87. $query = '';
  88. $parsed = parse_url( (string) $req );
  89. if ( !empty( $parsed['query'] ) && 'POST' == strtoupper( $this->request_token_method ) ) {
  90. $request_token_url = str_replace( '?' . $parsed['query'], '', (string) $req );
  91. $query = $parsed['query'];
  92. } else {
  93. $request_token_url = (string) $req;
  94. }
  95. // Go and get a request token
  96. switch ( strtoupper( $this->request_token_method ) ) {
  97. case 'GET':
  98. Keyring_Util::debug( "OAuth1 GET Request Token URL: $request_token_url" );
  99. $res = wp_remote_get( $request_token_url );
  100. break;
  101. case 'POST':
  102. Keyring_Util::debug( "OAuth1 POST Request Token URL: $request_token_url" );
  103. Keyring_Util::debug( $query );
  104. $res = wp_remote_post( $request_token_url, array( 'body' => $query, 'sslverify' => false ) );
  105. break;
  106. default:
  107. Keyring::error( __( 'Unsupported method specified for request_token.', 'keyring' ) );
  108. exit;
  109. }
  110. Keyring_Util::debug( 'OAuth1 Response' );
  111. Keyring_Util::debug( $res );
  112. if ( 200 == wp_remote_retrieve_response_code( $res ) ) {
  113. // Get the values returned from the remote service
  114. $token = wp_remote_retrieve_body( $res );
  115. parse_str( trim( $token ), $token );
  116. Keyring_Util::debug( 'OAuth1 Token Response' );
  117. Keyring_Util::debug( $token );
  118. $meta = array(
  119. '_classname' => get_called_class(), // Must include this for re-hydration, since we're using manual update()
  120. 'user_id' => get_current_user_id(),
  121. 'blog_id' => get_current_blog_id(),
  122. );
  123. // Use the ?for param to mark a connection as being for a specific plugin/feature
  124. if ( isset( $_REQUEST['for'] ) ) {
  125. $meta['for'] = (string) esc_attr( $_REQUEST['for'] );
  126. }
  127. $request_token = new Keyring_Request_Token(
  128. $this->get_name(),
  129. $token,
  130. apply_filters(
  131. 'keyring_request_token_meta',
  132. $meta,
  133. $this->get_name(),
  134. $token,
  135. $this
  136. ),
  137. $request_token_id // Overwrite the previous one
  138. );
  139. $request_token = apply_filters( 'keyring_request_token', $request_token, $this );
  140. $this->store->update( $request_token );
  141. } else {
  142. Keyring::error(
  143. sprintf( __( 'There was a problem connecting to %s to create an authorized connection. Please try again in a moment.', 'keyring' ), $this->get_label() )
  144. );
  145. return false;
  146. }
  147. // Redirect user to authorize access
  148. $authorize = add_query_arg( 'oauth_token', urlencode( $token['oauth_token'] ), $this->authorize_url ) ;
  149. if ( $this->callback_url ) {
  150. // Add reference to our request token to the callback. Use "state" a la OAuth2 for consistency
  151. $authorize = add_query_arg(
  152. 'oauth_callback',
  153. urlencode(
  154. add_query_arg(
  155. 'state',
  156. $request_token_id,
  157. $this->callback_url
  158. )
  159. ),
  160. $authorize
  161. );
  162. }
  163. Keyring_Util::debug( "OAuth Authorize Redirect: $authorize", KEYRING__DEBUG_NOTICE );
  164. wp_redirect( $authorize );
  165. exit;
  166. }
  167. function verify_token() {
  168. Keyring_Util::debug( 'Keyring_Service_OAuth1::verify_token()' );
  169. if ( !isset( $_REQUEST['nonce'] ) || !wp_verify_nonce( $_REQUEST['nonce'], 'keyring-verify-' . $this->get_name() ) ) {
  170. Keyring::error( __( 'Invalid/missing verification nonce.', 'keyring' ) );
  171. exit;
  172. }
  173. // Load up the request token that got us here and globalize it
  174. if ( isset( $_GET['state'] ) ) {
  175. global $keyring_request_token;
  176. $state = preg_replace( '/[^\x20-\x7E]/', '', $_GET['state'] );
  177. $keyring_request_token = $this->store->get_token( array( 'id' => $state, 'type' => 'request' ) );
  178. Keyring_Util::debug( 'OAuth1 Loaded Request Token ' . $state );
  179. Keyring_Util::debug( $keyring_request_token );
  180. $secret = $keyring_request_token->token['oauth_token_secret'];
  181. // Remove request token, don't need it any more.
  182. $this->store->delete( array( 'id' => $state, 'type' => 'request' ) );
  183. }
  184. // Get an access token, using the temporary token passed back
  185. $token = isset( $_GET['oauth_token'] ) ? $_GET['oauth_token'] : false;
  186. $access_token_url = $this->access_token_url;
  187. if ( !empty( $_GET['oauth_verifier'] ) )
  188. $access_token_url = add_query_arg( array( 'oauth_verifier' => urlencode( $_GET['oauth_verifier'] ) ), $access_token_url );
  189. // Set up a consumer token and make the request for an access_token
  190. $token = new OAuthConsumer( $token, $secret );
  191. $this->set_token( new Keyring_Access_Token( $this->get_name(), $token, array() ) );
  192. $res = $this->request( $access_token_url, array( 'method' => $this->access_token_method, 'raw_response' => true ) );
  193. Keyring_Util::debug( 'OAuth1 Access Token Response' );
  194. Keyring_Util::debug( $res );
  195. if ( !Keyring_Util::is_error( $res ) ) {
  196. $token = $this->parse_access_token( $res );
  197. $access_token = new Keyring_Access_Token(
  198. $this->get_name(),
  199. new OAuthToken(
  200. $token['oauth_token'],
  201. $token['oauth_token_secret']
  202. ),
  203. $this->build_token_meta( $token )
  204. );
  205. $access_token = apply_filters( 'keyring_access_token', $access_token, $token );
  206. Keyring_Util::debug( 'OAuth1 Access Token for storage' );
  207. Keyring_Util::debug( $access_token );
  208. $id = $this->store_token( $access_token );
  209. $this->verified( $id, $keyring_request_token );
  210. exit;
  211. } else {
  212. Keyring::error(
  213. sprintf( __( 'There was a problem connecting to %s to create an authorized connection. Please try again in a moment.', 'keyring' ), $this->get_label() )
  214. );
  215. return false;
  216. }
  217. }
  218. function request( $url, array $params = array() ) {
  219. if ( $this->requires_token() && empty( $this->token ) )
  220. return new Keyring_Error( 'keyring-request-error', __( 'No token', 'keyring' ) );
  221. $raw_response = false;
  222. if ( isset( $params['raw_response'] ) ) {
  223. $raw_response = (bool) $params['raw_response'];
  224. unset( $params['raw_response'] );
  225. }
  226. $method = 'GET';
  227. if ( isset( $params['method'] ) ) {
  228. $method = strtoupper( $params['method'] );
  229. unset( $params['method'] );
  230. }
  231. $sign_parameters = true;
  232. if ( isset( $params['sign_parameters'] ) ) {
  233. $sign_parameters = (bool) $params['sign_parameters'];
  234. unset( $params['sign_parameters'] );
  235. }
  236. // Should be an OAuthToken object
  237. $token = $this->token->token ? $this->token->token : null;
  238. Keyring_Util::debug( $token );
  239. $sign_vars = false;
  240. if ( isset( $params['body'] ) && $sign_parameters ) {
  241. if ( is_string( $params['body'] ) ) {
  242. wp_parse_str( $params['body'], $sign_vars );
  243. } else if ( is_array( $params['body'] ) ) {
  244. $sign_vars = $params['body'];
  245. }
  246. }
  247. $req = $this->prepare_request( $token, $method, $url, $sign_vars );
  248. $request_url = (string) $req;
  249. if ( $this->token && $this->authorization_header ) {
  250. $header = $req->to_header( $this->authorization_realm ); // Gives a complete header string, not just the second half
  251. $bits = explode( ': ', $header, 2 );
  252. $params['headers']['Authorization'] = $bits[1];
  253. // This hack was introduced for Instapaper (http://stackoverflow.com/a/9645033/1507683), which is overly strict on
  254. // header formatting, but it doesn't seem to cause problems anywhere else.
  255. $params['headers']['Authorization'] = str_replace( '",', '", ', $params['headers']['Authorization'] );
  256. Keyring_Util::debug( 'OAuth1 Authorization Header' );
  257. Keyring_Util::debug( $params['headers']['Authorization'] );
  258. // oauth_verifier was probably added directly to the URL, need to manually remove it
  259. $request_url = remove_query_arg( 'oauth_verifier', $url );
  260. }
  261. $query = '';
  262. $parsed = parse_url( $request_url );
  263. if ( !empty( $parsed['query'] ) && 'POST' == $method ) {
  264. $request_url = str_replace( '?' . $parsed['query'], '', $request_url );
  265. $query = $parsed['query'];
  266. }
  267. Keyring_Util::debug( "OAuth1 Request URL: $request_url" );
  268. switch ( $method ) {
  269. case 'GET':
  270. Keyring_Util::debug( 'OAuth1 GET ' . $request_url );
  271. $res = wp_remote_get( $request_url, $params );
  272. break;
  273. case 'POST':
  274. $params = array_merge( array( 'body' => $query, 'sslverify' => false ), $params );
  275. Keyring_Util::debug( 'OAuth1 POST ' . $request_url );
  276. Keyring_Util::debug( $params );
  277. $res = wp_remote_post( $request_url, $params );
  278. break;
  279. case 'PUT':
  280. $params = array_merge( array( 'method' => 'PUT' ), $params );
  281. $res = wp_remote_request( $request_url, $params );
  282. break;
  283. default:
  284. Keyring::error( __( 'Unsupported method specified.', 'keyring' ) );
  285. exit;
  286. }
  287. Keyring_Util::debug( $res );
  288. $this->set_request_response_code( wp_remote_retrieve_response_code( $res ) );
  289. if ( 200 == wp_remote_retrieve_response_code( $res ) || 201 == wp_remote_retrieve_response_code( $res ) ) {
  290. if ( $raw_response )
  291. return wp_remote_retrieve_body( $res );
  292. else
  293. return $this->parse_response( wp_remote_retrieve_body( $res ) );
  294. } else {
  295. return new Keyring_Error( 'keyring-request-error', $res );
  296. }
  297. }
  298. function prepare_request( $token, $method, $url, $sign_vars = false ) {
  299. $req = OAuthRequest::from_consumer_and_token(
  300. $this->consumer,
  301. $token,
  302. $method,
  303. $url,
  304. $sign_vars
  305. );
  306. $req->sign_request(
  307. $this->signature_method,
  308. $this->consumer,
  309. $token
  310. );
  311. return $req;
  312. }
  313. function get_display( Keyring_Access_Token $token ) {
  314. return (string) $token->token->key;
  315. }
  316. /**
  317. * OAuth1 always returns access tokens in querystring format,
  318. * but we provide an extendable method here just in case, and to
  319. * remain consistent with OAuth2.
  320. */
  321. function parse_access_token( $token ) {
  322. parse_str( $token, $token );
  323. return $token;
  324. }
  325. /**
  326. * This method is provided as a base point for parsing/decoding response
  327. * values provided by ->request(). Different services encode their responses
  328. * differently, but this provides a standardized place to handle that. You
  329. * may use JSON, XML, parse_str or some other, completely unique method here
  330. * to provide more workable data structures based on the responses from a
  331. * Service's API. The default just returns the string.
  332. *
  333. * @param string $response
  334. * @return Mixed data that is easier to work with, based on each Service
  335. */
  336. function parse_response( $response ) {
  337. return $response;
  338. }
  339. }