Skip to content

Commit fb6fc1e

Browse files
committed
Begin work on OAuth 2
1 parent 30e4339 commit fb6fc1e

14 files changed

+735
-236
lines changed

inc/authentication/namespace.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace WP\OAuth2\Authentication;
4+
5+
use WP_Http;
6+
use WP\OAuth2\Tokens;
7+
8+
/**
9+
* Get the authorization header
10+
*
11+
* On certain systems and configurations, the Authorization header will be
12+
* stripped out by the server or PHP. Typically this is then used to
13+
* generate `PHP_AUTH_USER`/`PHP_AUTH_PASS` but not passed on. We use
14+
* `getallheaders` here to try and grab it out instead.
15+
*
16+
* @return string|null Authorization header if set, null otherwise
17+
*/
18+
function get_authorization_header() {
19+
if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
20+
return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] );
21+
}
22+
23+
if ( function_exists( 'getallheaders' ) ) {
24+
$headers = getallheaders();
25+
26+
// Check for the authoization header case-insensitively
27+
foreach ( $headers as $key => $value ) {
28+
if ( strtolower( $key ) === 'authorization' ) {
29+
return $value;
30+
}
31+
}
32+
}
33+
34+
return null;
35+
}
36+
37+
function get_provided_token() {
38+
$header = get_authorization_header();
39+
if ( empty( $header ) || ! is_string( $header ) ) {
40+
return null;
41+
}
42+
43+
// Attempt to parse as a Bearer header.
44+
$is_valid = preg_match( '/Bearer ([a-zA-Z0-9=.~\-\+\/]+)/', trim( $header ), $matches );
45+
if ( ! $is_valid ) {
46+
return null;
47+
}
48+
49+
return $matches[1];
50+
}
51+
52+
/**
53+
* Try to authenticate if possible.
54+
*
55+
* @param WP_User|null $user Existing authenticated user.
56+
*/
57+
function attempt_authentication( $user = null ) {
58+
if ( ! empty( $user ) ) {
59+
return $user;
60+
}
61+
62+
// Were we given an token?
63+
$token_value = get_provided_token();
64+
if ( empty( $token_value ) ) {
65+
// No data provided, pass.
66+
return $user;
67+
}
68+
69+
// Attempt to find the token.
70+
$token = Tokens\get_by_id( $token_value );
71+
if ( empty( $token ) ) {
72+
return new WP_Error(
73+
'oauth2.authentication.attempt_authentication.invalid_token',
74+
__( 'Supplied token is invalid.', 'oauth2' ),
75+
array(
76+
'status' => WP_Http::FORBIDDEN,
77+
'token' => $token_value,
78+
),
79+
);
80+
}
81+
82+
// Token found, authenticate as the user.
83+
return $token->get_user_id();
84+
}

inc/class-client.php

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<?php
2+
3+
namespace WP\OAuth2;
4+
5+
use WP_Error;
6+
use WP_Post;
7+
8+
class Client {
9+
const POST_TYPE = 'oauth2_client';
10+
const CLIENT_ID_KEY = '_oauth2_client_id';
11+
const CLIENT_SECRET_KEY = '_oauth2_client_secret';
12+
const TYPE_KEY = '_oauth2_client_type';
13+
const REDIRECT_URI_KEY = '_oauth2_redirect_uri';
14+
const AUTH_CODE_KEY_PREFIX = '_oauth2_authcode_';
15+
const AUTH_CODE_LENGTH = 12;
16+
const CLIENT_ID_LENGTH = 12;
17+
const CLIENT_SECRET_LENGTH = 48;
18+
const AUTH_CODE_AGE = 600; // 10 * MINUTE_IN_SECONDS
19+
20+
protected $post;
21+
22+
/**
23+
* Constructor.
24+
*/
25+
protected function __construct( WP_Post $post ) {
26+
$this->post = $post;
27+
}
28+
29+
/**
30+
* Get the client's ID.
31+
*
32+
* @return string Client ID.
33+
*/
34+
public function get_id() {
35+
$result = get_post_meta( $this->get_post_id(), static::CLIENT_ID_KEY, false );
36+
if ( empty( $result ) ) {
37+
return null;
38+
}
39+
40+
return $result[0];
41+
}
42+
43+
/**
44+
* Get the client's post ID.
45+
*
46+
* For internal (WordPress) use only. For external use, use get_key()
47+
*
48+
* @return int Client ID.
49+
*/
50+
public function get_post_id() {
51+
return $this->post->ID;
52+
}
53+
54+
/**
55+
* Get the client's name.
56+
*
57+
* @return string HTML string.
58+
*/
59+
public function get_name() {
60+
return get_the_title( $this->get_post_id() );
61+
}
62+
63+
/**
64+
* Get the client's type.
65+
*
66+
* @return string|null Type ID if available, null otherwise.
67+
*/
68+
public function get_type() {
69+
$result = get_post_meta( $this->get_post_id(), static::TYPE_KEY, false );
70+
if ( empty( $result ) ) {
71+
return null;
72+
}
73+
74+
return $result[0];
75+
}
76+
77+
/**
78+
* Get registered URIs for the client.
79+
*
80+
* @return string[] List of valid redirect URIs.
81+
*/
82+
public function get_redirect_uris() {
83+
return get_post_meta( $this->get_post_id(), static::REDIRECT_URI_KEY, false );
84+
}
85+
86+
/**
87+
* Check if a redirect URI is valid for the client.
88+
*
89+
* @param string $url Supplied redirect URI to check.
90+
* @return boolean True if the URI is valid, false otherwise.
91+
*/
92+
public function check_redirect_uri( $uri ) {
93+
return false;
94+
}
95+
96+
public function generate_authorization_code( WP_User $user ) {
97+
$code = wp_generate_password( static::AUTH_CODE_LENGTH, false );
98+
$meta_key = static::AUTH_CODE_KEY_PREFIX . $code;
99+
$data = array(
100+
'user' => $user->ID,
101+
'expiration' => static::AUTH_CODE_AGE,
102+
);
103+
$result = add_post_meta( $this->get_post_id(), wp_slash( $meta_key ), wp_slash( $data ), true );
104+
if ( ! $result ) {
105+
return new WP_Error();
106+
}
107+
108+
return $code;
109+
}
110+
111+
/**
112+
* Issue token for a user.
113+
*
114+
* @param WP_User $user
115+
*/
116+
public function issue_token( WP_User $user ) {
117+
return Tokens\Access_Token::create( $this, $user );
118+
}
119+
120+
/**
121+
* Get a client by ID.
122+
*
123+
* @param int $id Client/post ID.
124+
* @return static|null Client instance on success, null if invalid/not found.
125+
*/
126+
public static function get_by_id( $id ) {
127+
$post = get_post( $id );
128+
if ( ! $post ) {
129+
return null;
130+
}
131+
132+
return new static( $post );
133+
}
134+
135+
/**
136+
* Create a new client.
137+
*
138+
* @param array $data {
139+
* }
140+
* @return static|WP_Error Client instance on success, error otherwise.
141+
*/
142+
public static function create( $data ) {
143+
$post_data = array(
144+
'post_type' => static::POST_TYPE,
145+
'post_title' => $data['name'],
146+
'post_content' => $data['description'],
147+
'post_author' => $data['author'],
148+
'post_status' => 'draft',
149+
);
150+
151+
$post_id = wp_insert_post( wp_slash( $post_data ), true );
152+
if ( is_wp_error( $post_id ) ) {
153+
return $post_id;
154+
}
155+
156+
// Generate ID and secret.
157+
$meta = array(
158+
static::CLIENT_ID_KEY => wp_generate_password( static::CLIENT_ID_LENGTH, false ),
159+
static::CLIENT_SECRET_KEY => wp_generate_password( static::CLIENT_SECRET_LENGTH, false ),
160+
);
161+
162+
foreach ( $meta as $key => $value ) {
163+
$result = update_post_meta( $post_id, wp_slash( $key ), wp_slash( $value ) );
164+
if ( ! $result ) {
165+
// Failed, rollback.
166+
return new WP_Error( 'oauth2.client.create.failed_meta', __( 'Could not save meta value.', 'oauth2' ) );
167+
}
168+
}
169+
170+
return new static( $post );
171+
}
172+
173+
/**
174+
* Register the underlying post type.
175+
*/
176+
public static function register_type() {
177+
register_post_type( static::POST_TYPE, array(
178+
'public' => false,
179+
'hierarchical' => true,
180+
'capability_type' => array(
181+
'client',
182+
'clients',
183+
),
184+
'supports' => array(
185+
'title',
186+
'editor',
187+
'revisions',
188+
'author',
189+
'thumbnail',
190+
),
191+
));
192+
}
193+
}

inc/class-scopes.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace WP\OAuth2;
4+
5+
class Scopes {
6+
protected $capabilities;
7+
8+
public function __construct() {
9+
$this->capabilities = array();
10+
}
11+
12+
public function register( $id, $capabilities ) {
13+
$this->scopes[ $id ] = $capabilities;
14+
}
15+
}

inc/endpoints/class-authorization.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace WP\OAuth2\Endpoints;
4+
5+
use WP\OAuth2\Client;
6+
use WP\OAuth2\Types;
7+
8+
class Authorization {
9+
const LOGIN_ACTION = 'oauth2_authorize';
10+
11+
/**
12+
* Register required actions and filters
13+
*/
14+
public function register_hooks() {
15+
add_action( 'login_form_' . static::LOGIN_ACTION, array( $this, 'handle_request' ) );
16+
add_action( 'oauth2_authorize_form', array( $this, 'render_page_fields' ) );
17+
}
18+
19+
public function handle_request() {
20+
// If the form hasn't been submitted, show it.
21+
$type = wp_unslash( $_GET['response_type'] );
22+
23+
switch ( $type ) {
24+
case 'code':
25+
$handler = new Types\Authorization_Code();
26+
break;
27+
28+
case 'token':
29+
$handler = new Types\Implicit();
30+
break;
31+
32+
default:
33+
return new WP_Error(
34+
'oauth2.endpoints.authorization.handle_request.invalid_type',
35+
__( 'Invalid response type specified.', 'oauth2' )
36+
);
37+
}
38+
39+
$result = $handler->handle_authorisation();
40+
if ( is_wp_error( $result ) ) {
41+
// TODO: Handle it.
42+
wp_die( $result->get_error_message() );
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)