oauth2.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. /**
  3. * Spec OAuth2 implementation for services using OAuth for authentication.
  4. * You will want to define an authorize and access_token endpoint. 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_OAuth2 extends Keyring_Service_OAuth1 {
  13. /**
  14. * Tokens should be passed in the authorization header if the service supports it
  15. * and only fallback to the query string if neccessary. Set to false to use ?oauth_token=
  16. */
  17. var $authorization_header = 'OAuth';
  18. /**
  19. * If you're not sending the authorization in the header, some services will accept
  20. * it as a querystring parameter. The spec says to send it as oauth_token, but some services
  21. * want it called something else... like 'access_token'
  22. * @var string
  23. */
  24. var $authorization_parameter = 'oauth_token';
  25. function request_token() {
  26. Keyring_Util::debug( 'Keyring_Service_OAuth2::request_token()' );
  27. if ( !isset( $_REQUEST['nonce'] ) || !wp_verify_nonce( $_REQUEST['nonce'], 'keyring-request-' . $this->get_name() ) ) {
  28. Keyring::error( __( 'Invalid/missing request nonce.', 'keyring' ) );
  29. exit;
  30. }
  31. // Need to create a request token now, so that we have a state to pass
  32. $request_token = new Keyring_Request_Token(
  33. $this->get_name(),
  34. array(),
  35. apply_filters(
  36. 'keyring_request_token_meta',
  37. array(
  38. 'for' => isset( $_REQUEST['for'] ) ? (string) $_REQUEST['for'] : false
  39. ),
  40. $this->get_name(),
  41. array(), // no token
  42. $this
  43. )
  44. );
  45. $request_token = apply_filters( 'keyring_request_token', $request_token, $this );
  46. $request_token_id = $this->store_token( $request_token );
  47. $url = $this->authorize_url;
  48. if ( !stristr( $url, '?' ) )
  49. $url .= '?';
  50. $params = array(
  51. 'response_type' => 'code',
  52. 'client_id' => $this->key,
  53. 'redirect_uri' => $this->callback_url,
  54. 'state' => $request_token_id,
  55. );
  56. $params = apply_filters( 'keyring_' . $this->get_name() . '_request_token_params', $params );
  57. Keyring_Util::debug( 'OAuth2 Redirect URL: ' . $url . http_build_query( $params ) );
  58. wp_redirect( $url . http_build_query( $params ) );
  59. exit;
  60. }
  61. function verify_token() {
  62. Keyring_Util::debug( 'Keyring_Service_OAuth2::verify_token()' );
  63. if ( !isset( $_REQUEST['nonce'] ) || !wp_verify_nonce( $_REQUEST['nonce'], 'keyring-verify-' . $this->get_name() ) ) {
  64. Keyring::error( __( 'Invalid/missing verification nonce.', 'keyring' ) );
  65. exit;
  66. }
  67. if ( !isset( $_GET['code'] ) || !isset( $_GET['state']) ) {
  68. Keyring::error(
  69. sprintf( __( 'There was a problem authorizing with %s. Please try again in a moment.', 'keyring' ), $this->get_label() )
  70. );
  71. return false;
  72. }
  73. // Load up the request token that got us here and globalize it
  74. global $keyring_request_token;
  75. $state = preg_replace( '/[^\x20-\x7E]/', '', $_GET['state'] );
  76. $keyring_request_token = $this->store->get_token( array( 'id' => $state, 'type' => 'request' ) );
  77. Keyring_Util::debug( 'OAuth2 Loaded Request Token ' . $state );
  78. Keyring_Util::debug( $keyring_request_token );
  79. if ( !$keyring_request_token ) {
  80. Keyring::error(
  81. sprintf( __( 'Failed to load your request token while connecting to %s. Please try again in a moment.', 'keyring' ), $this->get_label() )
  82. );
  83. return false;
  84. }
  85. $error_debug_info = array();
  86. if ( !empty( $keyring_request_token->meta['blog_id'] ) && !empty( $keyring_request_token->meta['user_id'] ) ) {
  87. $error_debug_info = array(
  88. 'blog_id' => $keyring_request_token->meta['blog_id'],
  89. 'user_id' => $keyring_request_token->meta['user_id']
  90. );
  91. }
  92. // Remove request token, don't need it any more.
  93. $this->store->delete( array( 'id' => $state, 'type' => 'request' ) );
  94. $url = $this->access_token_url;
  95. if ( !stristr( $url, '?' ) )
  96. $url .= '?';
  97. $params = array(
  98. 'client_id' => $this->key,
  99. 'client_secret' => $this->secret,
  100. 'grant_type' => 'authorization_code',
  101. 'redirect_uri' => $this->callback_url,
  102. 'code' => $_GET['code'],
  103. );
  104. $params = apply_filters( 'keyring_' . $this->get_name() . '_verify_token_params', $params );
  105. Keyring_Util::debug( 'OAuth2 Access Token URL: ' . $url . http_build_query( $params ) );
  106. switch ( strtoupper( $this->access_token_method ) ) {
  107. case 'GET':
  108. $res = wp_remote_get( $url . http_build_query( $params ) );
  109. break;
  110. case 'POST':
  111. $res = wp_remote_post( $url, array( 'body' => $params ) );
  112. break;
  113. }
  114. Keyring_Util::debug( 'OAuth2 Response' );
  115. Keyring_Util::debug( $res );
  116. if ( 200 == wp_remote_retrieve_response_code( $res ) ) {
  117. $token = wp_remote_retrieve_body( $res );
  118. Keyring_Util::debug( $token );
  119. $token = $this->parse_access_token( $token );
  120. $access_token = new Keyring_Access_Token(
  121. $this->get_name(),
  122. $token['access_token'],
  123. $this->build_token_meta( $token )
  124. );
  125. $access_token = apply_filters( 'keyring_access_token', $access_token, $token );
  126. Keyring_Util::debug( 'OAuth2 Access Token for storage' );
  127. Keyring_Util::debug( $access_token );
  128. $id = $this->store_token( $access_token );
  129. $this->verified( $id, $keyring_request_token );
  130. exit;
  131. }
  132. Keyring::error(
  133. sprintf( __( 'There was a problem authorizing with %s. Please try again in a moment.', 'keyring' ), $this->get_label() ),
  134. $error_debug_info
  135. );
  136. return false;
  137. }
  138. /**
  139. * The OAuth2 spec indicates that responses should be in JSON, but separating
  140. * this allows different services to potentially use querystring-encoded
  141. * responses or something else, and just define this method within themselves
  142. * to handle decoding the access_token response.
  143. *
  144. * @param string $token The response from the access_token request
  145. * @return Array containing key/value pairs from the token response
  146. */
  147. function parse_access_token( $token ) {
  148. return (array) json_decode( $token );
  149. }
  150. function request( $url, array $params = array() ) {
  151. Keyring_Util::debug( $url );
  152. if ( $this->requires_token() && empty( $this->token ) )
  153. return new Keyring_Error( 'keyring-request-error', __( 'No token' ) );
  154. $token = $this->token ? $this->token : null;
  155. if ( !is_null( $token ) ) {
  156. if ( $this->authorization_header ) {
  157. // type can be OAuth, Bearer, ...
  158. $params['headers']['Authorization'] = $this->authorization_header . ' ' . (string) $token;
  159. } else {
  160. $url = add_query_arg( array( $this->authorization_parameter => urlencode( (string) $token ) ), $url );
  161. }
  162. }
  163. $raw_response = false;
  164. if ( isset( $params['raw_response'] ) ) {
  165. $raw_response = (bool) $params['raw_response'];
  166. unset( $params['raw_response'] );
  167. }
  168. $method = 'GET';
  169. if ( isset( $params['method'] ) ) {
  170. $method = strtoupper( $params['method'] );
  171. unset( $params['method'] );
  172. }
  173. Keyring_Util::debug( 'OAuth2 Params' );
  174. Keyring_Util::debug( $params );
  175. switch ( strtoupper( $method ) ) {
  176. case 'GET':
  177. $res = wp_remote_get( $url, $params );
  178. break;
  179. case 'POST':
  180. $params = array_merge( array( 'sslverify' => false ), $params );
  181. $res = wp_remote_post( $url, $params );
  182. break;
  183. default:
  184. $params = array_merge( array( 'method' => $method, 'sslverify' => false ), $params );
  185. $res = wp_remote_request( $url, $params );
  186. break;
  187. }
  188. Keyring_Util::debug( 'OAuth2 Response' );
  189. Keyring_Util::debug( $res );
  190. $this->set_request_response_code( wp_remote_retrieve_response_code( $res ) );
  191. if ( 200 == wp_remote_retrieve_response_code( $res ) || 201 == wp_remote_retrieve_response_code( $res ) )
  192. if ( $raw_response )
  193. return wp_remote_retrieve_body( $res );
  194. else
  195. return $this->parse_response( wp_remote_retrieve_body( $res ) );
  196. else
  197. return new Keyring_Error( 'keyring-request-error', $res );
  198. }
  199. /**
  200. * OAuth2 implementations generally use JSON. You can still override this
  201. * per service if you like, but by default we'll assume JSON.
  202. */
  203. function parse_response( $response ) {
  204. return json_decode( $response );
  205. }
  206. }