oauth1.php 13 KB

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