@@ -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 (
0 commit comments