Skip to content

Commit 16351d3

Browse files
authored
Merge pull request #186 from dshanske/newspec
Tries to Adopt some of the New IndieAuth Changes
2 parents 1d6e3e8 + d04401a commit 16351d3

17 files changed

+393
-341
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"installer-name": "indieauth"
1818
},
1919
"require": {
20-
"php": ">=5.4.0",
20+
"php": ">=5.6.0",
2121
"composer/installers": "~1.0"
2222
},
2323
"require-dev": {

includes/class-indieauth-admin.php

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ public function __construct() {
1111
add_action( 'init', array( $this, 'settings' ) );
1212
add_action( 'login_form_authdiag', array( $this, 'login_form_authdiag' ) );
1313
add_action( 'admin_menu', array( $this, 'admin_menu' ) );
14-
add_filter( 'wp_pre_insert_user_data', array( $this, 'unique_user_url' ), 10, 3 );
15-
add_filter( 'manage_users_columns', array( $this, 'add_user_url_column' ) );
16-
add_filter( 'manage_users_custom_column', array( $this, 'user_url_column' ), 10, 3 );
1714
add_filter( 'site_status_tests', array( $this, 'add_indieauth_tests' ) );
1815
}
1916

@@ -26,10 +23,6 @@ public function add_indieauth_tests( $tests ) {
2623
'label' => __( 'SSL Test', 'indieauth' ),
2724
'test' => array( $this, 'site_health_https_test' ),
2825
);
29-
$tests['direct']['indieauth_users'] = array(
30-
'label' => __( 'Unique User URL Test', 'indieauth' ),
31-
'test' => array( $this, 'site_health_users_test' ),
32-
);
3326
return $tests;
3427
}
3528

@@ -63,34 +56,6 @@ public function site_health_https_test() {
6356
return $result;
6457
}
6558

66-
public function site_health_users_test() {
67-
$result = array(
68-
'label' => __( 'Unique User URLs Check Passed', 'indieauth' ),
69-
'status' => 'good',
70-
'badge' => array(
71-
'label' => __( 'IndieAuth', 'indieauth' ),
72-
'color' => 'green',
73-
),
74-
'description' => sprintf(
75-
'<p>%s</p>',
76-
__( 'You are using HTTPS and IndieAuth will be secure', 'indieauth' )
77-
),
78-
'actions' => '',
79-
'test' => 'indieauth_headers',
80-
);
81-
82-
if ( $this->check_dupe_user_urls() ) {
83-
$result['status'] = 'critical';
84-
$result['label'] = __( 'Unique User URLs Test Failed', 'indieauth' );
85-
$result['description'] = sprintf(
86-
'<p>%s</p>',
87-
__( 'Multiple user accounts have the same URL set. This is not permitted as this value is used by IndieAuth for login. Please resolve', 'indieauth' )
88-
);
89-
$result['actions'] = __( 'Under IndieAuth, your URL is your identity. Two accounts cannot have the same website URL in their user profile as this might allow one user to gain the credentials of another', 'indieauth' );
90-
}
91-
return $result;
92-
}
93-
9459
public function site_health_header_test() {
9560
$result = array(
9661
'label' => __( 'Authorization Header Passed', 'indieauth' ),
@@ -120,63 +85,6 @@ public function site_health_header_test() {
12085
return $result;
12186
}
12287

123-
public function user_url_column( $val, $column_id, $user_id ) {
124-
if ( 'user_url' === $column_id ) {
125-
$user = get_user_by( 'id', $user_id );
126-
if ( ! wp_http_validate_url( $user->user_url ) ) {
127-
return __( 'None', 'indieauth' );
128-
}
129-
$url = esc_url( $user->user_url );
130-
return sprintf( '<a href="%1s">%1$s</a>', $url );
131-
}
132-
return $val;
133-
}
134-
135-
public function add_user_url_column( $column ) {
136-
$column['user_url'] = __( 'Website', 'indieauth' );
137-
return $column;
138-
}
139-
140-
/**
141-
* Ensure all user URL fields are unique
142-
*
143-
*/
144-
public function unique_user_url( $data, $update, $id ) {
145-
if ( empty( $data['user_url'] ) ) {
146-
return $data;
147-
}
148-
$data['user_url'] = normalize_url( $data['user_url'] );
149-
$users = get_users(
150-
array(
151-
'search' => '*' . wp_parse_url( $data['user_url'], PHP_URL_HOST ) . '*',
152-
'search_columns' => array( 'user_url' ),
153-
'fields' => array( 'ID', 'user_url' ),
154-
)
155-
);
156-
157-
$url = normalize_url( $data['user_url'], true );
158-
foreach ( $users as $user ) {
159-
if ( ( normalize_url( $user->user_url, true ) === $url ) && $user->ID !== $id ) {
160-
$data['user_url'] = '';
161-
}
162-
}
163-
return $data;
164-
}
165-
166-
public function check_dupe_user_urls() {
167-
$urls = get_users(
168-
array(
169-
'fields' => array( 'user_url' ),
170-
)
171-
);
172-
$urls = array_filter( wp_list_pluck( $urls, 'user_url' ) );
173-
$urls = array_map( 'normalize_url', $urls );
174-
if ( count( array_unique( $urls ) ) === count( $urls ) ) {
175-
return false;
176-
}
177-
return true;
178-
}
179-
18088
public function login_form_authdiag() {
18189
$return = '';
18290
if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) {

includes/class-indieauth-authorization-endpoint.php

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,36 +26,81 @@ public function register_routes() {
2626
'methods' => WP_REST_Server::READABLE,
2727
'callback' => array( $this, 'request' ),
2828
'args' => array(
29-
'response_type' => array(),
30-
'client_id' => array(
29+
/* Code is currently the only type as of IndieAuth 1.1 and a response_type is now required, but not requiring it here yet.
30+
* Indicates to the authorization server that an authorization code should be returned as the response.
31+
*/
32+
'response_type' => array(
33+
'default' => 'code',
34+
),
35+
// The Client URL.
36+
'client_id' => array(
3137
'validate_callback' => 'rest_is_valid_url',
3238
'sanitize_callback' => 'esc_url_raw',
39+
'required' => true,
3340
),
34-
'redirect_uri' => array(
41+
// The redirect URL indicating where the user should be redirected to after approving the request.
42+
'redirect_uri' => array(
3543
'validate_callback' => 'rest_is_valid_url',
3644
'sanitize_callback' => 'esc_url_raw',
45+
'required' => true,
3746
),
38-
'me' => array(
47+
/* A parameter set by the client which will be included when the user is redirected back to the client.
48+
* This is used to prevent CSRF attacks. The authorization server MUST return the unmodified state value back to the client.
49+
*/
50+
'state' => array(
51+
'required' => true,
52+
),
53+
/* Code Challenge.
54+
* IndieAuth 1.1 requires PKCE, but for now these parameters will remain optional to give time for other implementers.
55+
*/
56+
'code_challenge' => array(),
57+
/* The hashing method used to calculate the code challenge, e.g. "S256"
58+
*/
59+
'code_challenge_method' => array(),
60+
61+
/* A space-separated list of scopes the client is requesting, e.g. "profile", or "profile create".
62+
* If the client omits this value, the authorization server MUST NOT issue an access token for this authorization code.
63+
* Only the user's profile URL may be returned without any scope requested. See Profile Information for details about
64+
* which scopes to request to return user profile information. Optional.
65+
*/
66+
'scope' => array(),
67+
/* The Profile URL the user entered. Optional.
68+
*/
69+
'me' => array(
3970
'validate_callback' => 'rest_is_valid_url',
4071
'sanitize_callback' => 'esc_url_raw',
4172
),
42-
'state' => array(),
4373
),
4474
'permission_callback' => '__return_true',
4575
),
4676
array(
4777
'methods' => WP_REST_Server::CREATABLE,
4878
'callback' => array( $this, 'verify' ),
4979
'args' => array(
50-
'code' => array(),
51-
'client_id' => array(
80+
/* grant_type=authorization_code is the only one supported right now. This remains optional as not required in
81+
* the original IndieAuth spec, but will eventually be mandatory.
82+
*/
83+
'grant_type' => array(
84+
'default' => 'authorization_code',
85+
),
86+
/* The authorization code received from the authorization endpoint in the redirect.
87+
*/
88+
'code' => array(),
89+
/* The client's URL, which MUST match the client_id used in the authentication request.
90+
*/
91+
'client_id' => array(
5292
'validate_callback' => 'rest_is_valid_url',
5393
'sanitize_callback' => 'esc_url_raw',
5494
),
55-
'redirect_uri' => array(
95+
/* The client's redirect URL, which MUST match the initial authentication request.
96+
*/
97+
'redirect_uri' => array(
5698
'validate_callback' => 'rest_is_valid_url',
5799
'sanitize_callback' => 'esc_url_raw',
58100
),
101+
/* The original plaintext random string generated before starting the authorization request.
102+
*/
103+
'code_verifier' => array(),
59104
),
60105
'permission_callback' => '__return_true',
61106
),
@@ -82,7 +127,8 @@ public static function scopes( $scope = 'all' ) {
82127
'channels' => __( 'Allows the application to manage channels', 'indieauth' ),
83128
'save' => __( 'Allows the application to save content for later retrieval', 'indieauth' ),
84129
// Profile
85-
'profile' => __( 'Returns a complete profile to the application. Without this only a display name, avatar, and url will be returned', 'indieauth' ),
130+
'profile' => __( 'Allows access to the users default profile information which includes name, photo, and url', 'indieauth' ),
131+
'email' => __( 'Allows access to the users email address', 'indieauth' ),
86132
);
87133
if ( 'all' === $scope ) {
88134
return $scopes;
@@ -91,15 +137,28 @@ public static function scopes( $scope = 'all' ) {
91137
return apply_filters( 'indieauth_scope_description', $description, $scope );
92138
}
93139

140+
/*
141+
* Output a list of checkboxes to select scopes.
142+
*
143+
* @param array $scopes Scopes to Output.
144+
*/
145+
public static function scope_list( $scopes ) {
146+
if ( ! empty( $scopes ) ) {
147+
foreach ( $scopes as $s ) {
148+
printf( '<li><input type="checkbox" name="scope[]" value="%1$s" %2$s /><strong>%1$s</strong> - %3$s</li>', $s, checked( true, true, false ), self::scopes( $s ) );
149+
}
150+
}
151+
}
152+
94153
public function request( $request ) {
95154
$params = $request->get_params();
96-
if ( ! isset( $params['response_type'] ) ) {
97-
$params['response_type'] = 'id';
155+
if ( ! isset( $params['response_type'] ) || 'id' === $params['response_type'] ) {
156+
$params['response_type'] = 'code';
98157
}
99-
if ( 'code' !== $params['response_type'] && 'id' !== $params['response_type'] ) {
158+
if ( 'code' !== $params['response_type'] ) {
100159
return new WP_OAuth_Response( 'unsupported_response_type', __( 'Unsupported Response Type', 'indieauth' ), 400 );
101160
}
102-
$required = array( 'redirect_uri', 'client_id', 'state', 'me' );
161+
$required = array( 'redirect_uri', 'client_id', 'state' );
103162
foreach ( $required as $require ) {
104163
if ( ! isset( $params[ $require ] ) ) {
105164
// translators: Name of missing parameter
@@ -113,18 +172,22 @@ public function request( $request ) {
113172
'_wpnonce' => wp_create_nonce( 'wp_rest' ),
114173
'response_type' => $params['response_type'],
115174
'client_id' => $params['client_id'],
116-
'me' => $params['me'],
175+
'me' => isset( $params['me'] ) ? $params['me'] : null,
117176
'state' => $params['state'],
118177
'code_challenge' => isset( $params['code_challenge'] ) ? $params['code_challenge'] : null,
119178
'code_challenge_method' => isset( $params['code_challenge_method'] ) ? $params['code_challenge_method'] : null,
120179
)
121180
);
122181

123182
if ( 'code' === $params['response_type'] ) {
124-
$args['scope'] = isset( $params['scope'] ) ? $params['scope'] : 'create update';
183+
$args['scope'] = isset( $params['scope'] ) ? $params['scope'] : '';
125184
if ( ! preg_match( '@^([\x21\x23-\x5B\x5D-\x7E]+( [\x21\x23-\x5B\x5D-\x7E]+)*)?$@', $args['scope'] ) ) {
126185
return new WP_OAuth_Response( 'invalid_grant', __( 'Invalid scope request', 'indieauth' ), 400 );
127186
}
187+
$scopes = explode( ' ', $args['scope'] );
188+
if ( in_array( 'email', $scopes, true ) && ! in_array( 'profile', $scopes, true ) ) {
189+
return new WP_OAuth_Response( 'invalid_grant', __( 'Cannot request email scope without profile scope', 'indieauth' ), 400 );
190+
}
128191
}
129192
$url = add_query_params_to_url( $args, $url );
130193

@@ -147,14 +210,18 @@ public function delete_code( $code, $user_id = null ) {
147210
}
148211

149212
public function verify( $request ) {
150-
$params = $request->get_params();
151-
$required = array( 'redirect_uri', 'client_id', 'code' );
213+
$params = $request->get_params();
214+
215+
$required = array( 'redirect_uri', 'client_id', 'code', 'grant_type' );
152216
foreach ( $required as $require ) {
153217
if ( ! isset( $params[ $require ] ) ) {
154218
// translators: Name of missing parameter
155219
return new WP_OAuth_Response( 'parameter_absent', sprintf( __( 'Missing Parameter: %1$s', 'indieauth' ), $require ), 400 );
156220
}
157221
}
222+
if ( 'authorization_code' !== $params['grant_type'] ) {
223+
return new WP_OAuth_Response( 'invalid_grant', __( 'Endpoint only accepts authorization_code grant_type', 'indieauth' ), 400 );
224+
}
158225
$params = wp_array_slice_assoc( $params, array( 'client_id', 'redirect_uri' ) );
159226
$code = $request->get_param( 'code' );
160227
$token = $this->get_code( $code );
@@ -185,7 +252,7 @@ public function verify( $request ) {
185252
if ( array() === array_diff_assoc( $params, $token ) ) {
186253
$this->delete_code( $code, $token['user'] );
187254

188-
$return = array( 'me' => get_url_from_user( $user->ID ) );
255+
$return = array( 'me' => $token['me'] );
189256

190257
if ( isset( $token['scope'] ) ) {
191258
$return['scope'] = $token['scope'];
@@ -218,7 +285,7 @@ public function authorize() {
218285
$client_icon = $info->get_icon();
219286
$redirect_uri = isset( $_GET['redirect_to'] ) ? wp_unslash( $_GET['redirect_to'] ) : null;
220287
$scope = isset( $_GET['scope'] ) ? wp_unslash( $_GET['scope'] ) : null;
221-
$scopes = explode( ' ', $scope );
288+
$scopes = array_filter( explode( ' ', $scope ) );
222289
$state = isset( $_GET['state'] ) ? $_GET['state'] : null;
223290
$me = isset( $_GET['me'] ) ? wp_unslash( $_GET['me'] ) : null;
224291
$response_type = isset( $_GET['response_type'] ) ? wp_unslash( $_GET['response_type'] ) : null;
@@ -238,34 +305,45 @@ public function authorize() {
238305
)
239306
);
240307
$url = add_query_params_to_url( $args, wp_login_url() );
241-
if ( 'code' === $_GET['response_type'] ) {
242-
include plugin_dir_path( __DIR__ ) . 'templates/indieauth-authorize-form.php';
243-
} elseif ( 'id' === $_GET['response_type'] ) {
308+
if ( empty( $scopes ) || empty( array_diff( $scopes, array( 'profile', 'email' ) ) ) ) {
244309
include plugin_dir_path( __DIR__ ) . 'templates/indieauth-authenticate-form.php';
310+
} else {
311+
include plugin_dir_path( __DIR__ ) . 'templates/indieauth-authorize-form.php';
245312
}
313+
246314
include plugin_dir_path( __DIR__ ) . 'templates/indieauth-auth-footer.php';
247315
}
248316

249317
public function confirmed() {
250318
$current_user = wp_get_current_user();
319+
$user = $current_user->ID;
251320
// phpcs:disable
252321
$client_id = wp_unslash( $_POST['client_id'] ); // WPCS: CSRF OK
253322
$redirect_uri = isset( $_POST['redirect_uri'] ) ? wp_unslash( $_POST['redirect_uri'] ) : null;
254323
$scope = isset( $_POST['scope'] ) ? $_POST['scope'] : array();
255324
$code_challenge = isset( $_POST['code_challenge'] ) ? wp_unslash( $_POST['code_challenge'] ) : null;
256325
$code_challenge_method = isset( $_POST['code_challenge_method'] ) ? wp_unslash( $_POST['code_challenge_method'] ) : null;
326+
327+
// Do not allow the post scope as deprecated. For compatibility, instead update the offering to the more limited but functionally identical create/update.
257328
$search = array_search( 'post', $scope, true );
258329
if ( is_numeric( $search ) ) {
259330
unset( $scope[ $search ] );
260331
$scope = array_unique( array_merge( $scope, array( 'create', 'update' ) ) );
261332
}
333+
262334
$scope = implode( ' ', $scope );
263335

264336
$state = isset( $_POST['state'] ) ? $_POST['state'] : null;
265-
$me = isset( $_POST['me'] ) ? wp_unslash( $_POST['me'] ) : null;
337+
338+
// In IndieAuth 1.1, me parameter is optional. Me should actually be derived only from the logged in user not from this parameter.
339+
// In other implementations, there may be multiple identities permitted for a single user, but this is not currently practical on a
340+
// WordPress site, so we will just ignore the optional me parameter and always return our own.
341+
$me = get_url_from_user( $user );
342+
343+
266344
$response_type = isset( $_POST['response_type'] ) ? wp_unslash( $_POST['response_type'] ) : null;
267345
/// phpcs:enable
268-
$token = compact( 'response_type', 'client_id', 'redirect_uri', 'scope', 'me', 'code_challenge', 'code_challenge_method' );
346+
$token = compact( 'response_type', 'client_id', 'redirect_uri', 'scope', 'me', 'code_challenge', 'code_challenge_method', 'user' );
269347
$token = array_filter( $token );
270348
$code = self::set_code( $current_user->ID, $token );
271349
$url = add_query_params_to_url(

includes/class-indieauth-authorize.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,10 @@ public function get_token_from_bearer_header( $header ) {
273273
* @return string|null Token on success, null on failure.
274274
*/
275275
public function get_token_from_request() {
276-
if ( empty( $_POST['access_token'] ) ) {
276+
if ( empty( $_POST['access_token'] ) ) { // phpcs:ignore
277277
return null;
278278
}
279-
$token = $_POST['access_token'];
279+
$token = $_POST['access_token']; // phpcs:ignore
280280

281281
if ( is_string( $token ) ) {
282282
return $token;

includes/class-indieauth-local-authorize.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public function verify_access_token( $token ) {
3535
return $return;
3636
}
3737
$return['last_accessed'] = time();
38+
$return['last_ip'] = $_SERVER['REMOTE_ADDR'];
3839
$tokens->update( $token, $return );
3940
return $return;
4041
}

0 commit comments

Comments
 (0)