Skip to content

Commit 7eadaa8

Browse files
committed
add functions to handle more of the indieauth client flow
1 parent 4747d9e commit 7eadaa8

File tree

2 files changed

+263
-5
lines changed

2 files changed

+263
-5
lines changed

README.md

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,110 @@ This is a simple library to help with IndieAuth. There are two ways you may want
55

66
[![Build Status](https://travis-ci.org/indieweb/indieauth-client-php.png?branch=master)](http://travis-ci.org/indieweb/indieauth-client-php)
77

8-
Usage for Clients
9-
-----------------
8+
9+
Quick Start
10+
-----------
11+
12+
If you want to get started quickly, and if you're okay with letting the library store things in the PHP session itself, then you can follow the examples below. If you need more control or want to step into the details of the IndieAuth flow, see the [Detailed Usage for Clients](#detailed) below.
13+
14+
### Create a Login Form
15+
16+
You'll first need to create a login form to prompt the user to enter their website address. This might look something like the HTML below.
17+
18+
```html
19+
<form action="/login.php" method="post">
20+
<input type="url" name="url">
21+
<input type="submit" value="Log In">
22+
</form>
23+
```
24+
25+
### Begin the Login Flow
26+
27+
In the `login.php` file, you'll need to initialize the session, and tell this library to discover the user's endpoints. If everything succeeds, the library will return a URL that you can use to redirect the user to begin the flow.
28+
29+
The example below will have some really basic error handling, which you'll probably want to replace with something nicer looking.
30+
31+
Example `login.php` file:
32+
33+
```php
34+
<?php
35+
36+
if(!isset($_POST['url'])) {
37+
die('Missing URL');
38+
}
39+
40+
// Start a session for the library to be able to save state between requests.
41+
session_start();
42+
43+
// You'll need to set up two pieces of information before you can use the client,
44+
// the client ID and and the redirect URL.
45+
46+
// The client ID should be the home page of your app.
47+
IndieAuth\Client::$clientID = 'https://example.com/';
48+
49+
// The redirect URL is where the user will be returned to after they approve the request.
50+
IndieAuth\Client::$redirectURL = 'https://example.com/redirect.php';
51+
52+
// Pass the user's URL and your requested scope to the client.
53+
// If you are writing a Micropub client, you should include at least the "create" scope.
54+
// If you are just trying to log the user in, you can omit the second parameter.
55+
56+
list($authorizationURL, $error) = IndieAuth\Client::begin($_POST['url'], 'create');
57+
// or list($authorizationURL, $error) = IndieAuth\Client::begin($_POST['url']);
58+
59+
// Check whether the library was able to discover the necessary endpoints
60+
if($error) {
61+
echo "<p>Error: ".$error['error']."</p>";
62+
echo "<p>".$error['error_description']."</p>";
63+
} else {
64+
// Redirect the user to their authorization endpoint
65+
header('Location: '.$authorizationURL);
66+
}
67+
68+
```
69+
70+
### Handling the Callback
71+
72+
In your callback file, you just need to pass all the query string parameters to the library and it will take care of things! It will use the authorization or token endpoint it found in the initial step, and will check the authorization code or exchange it for an access token as appropriate.
73+
74+
The result will be the response from the authorization endpoint, which will contain the user's final `me` URL as well as the access token if you requested one or more scopes.
75+
76+
If there were any problems, the error information will be returned to you as well.
77+
78+
The library takes care of canonicalizing the user's URL, as well as checking that the final URL is on the same domain as the entered URL.
79+
80+
Example `redirect.php` file:
81+
82+
```php
83+
<?php
84+
session_start();
85+
IndieAuth\Client::$clientID = 'https://example.com/';
86+
IndieAuth\Client::$redirectURL = 'https://example.com/redirect.php';
87+
88+
list($user, $error) = IndieAuth\Client::complete($_GET);
89+
90+
if($error) {
91+
echo "<p>Error: ".$error['error']."</p>";
92+
echo "<p>".$error['error_description']."</p>";
93+
} else {
94+
// Login succeeded!
95+
// If you requested a scope, then there will be an access token in the response.
96+
// Otherwise there will just be the user's URL.
97+
echo "URL: ".$user['me']."<br>";
98+
if(isset($user['access_token'])) {
99+
echo "Access Token: ".$user['access_token']."<br>";
100+
echo "Scope: ".$user['scope']."<br>";
101+
}
102+
103+
// You'll probably want to save the user's URL in the session
104+
$_SESSION['user'] = $user['me'];
105+
}
106+
107+
```
108+
109+
110+
Detailed Usage for Clients {#detailed}
111+
--------------------------
10112

11113
The first thing an IndieAuth client needs to do is to prompt the user to enter their web address. This is the basis of IndieAuth, requiring each person to have their own website. A typical IndieAuth sign-in form may look something like the following.
12114

src/IndieAuth/Client.php

Lines changed: 159 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,127 @@ class Client {
1212

1313
public static $http;
1414

15+
public static $clientID;
16+
public static $redirectURL;
17+
18+
// Handles everything you need to start the authorization process.
19+
// Discovers the user's auth endpoints, generates and stores a state in the session.
20+
// Returns an authorization URL or an error array.
21+
public static function begin($url, $scope=false) {
22+
if(!isset(self::$clientID) || !isset(self::$redirectURL)) {
23+
return [false, [
24+
'error' => 'not_configured',
25+
'error_description' => 'Before you can begin, you need to configure the clientID and redirectURL of the IndieAuth client'
26+
]];
27+
}
28+
29+
$url = self::normalizeMeURL($url);
30+
31+
$authorizationEndpoint = self::discoverAuthorizationEndpoint($url);
32+
33+
if(!$authorizationEndpoint) {
34+
return [false, [
35+
'error' => 'missing_authorization_endpoint',
36+
'error_description' => 'Could not find your authorization endpoint'
37+
]];
38+
}
39+
40+
if($scope) {
41+
$tokenEndpoint = self::discoverTokenEndpoint($url);
42+
43+
if(!$tokenEndpoint) {
44+
return [false, [
45+
'error' => 'missing_token_endpoint',
46+
'error_description' => 'Could not find your token endpoint'
47+
]];
48+
}
49+
}
50+
51+
$state = self::generateStateParameter();
52+
53+
$_SESSION['indieauth_url'] = $url;
54+
$_SESSION['indieauth_state'] = $state;
55+
$_SESSION['indieauth_authorization_endpoint'] = $authorizationEndpoint;
56+
if($scope)
57+
$_SESSION['indieauth_token_endpoint'] = $tokenEndpoint;
58+
59+
$authorizationURL = self::buildAuthorizationURL($authorizationEndpoint, $url, self::$redirectURL, self::$clientID, $state, $scope);
60+
61+
return [$authorizationURL, false];
62+
}
63+
64+
public static function complete($params) {
65+
$requiredSessionKeys = ['indieauth_url', 'indieauth_state', 'indieauth_authorization_endpoint'];
66+
foreach($requiredSessionKeys as $key) {
67+
if(!isset($_SESSION[$key])) {
68+
return [false, [
69+
'error' => 'invalid_session',
70+
'error_description' => 'The session was missing data. Ensure that you are initializing the session before using this library'
71+
]];
72+
}
73+
}
74+
75+
if(isset($params['error'])) {
76+
return [false, [
77+
'error' => $params['error'],
78+
'error_description' => (isset($params['error_description']) ? $params['error_description'] : '')
79+
]];
80+
}
81+
82+
if(!isset($params['code'])) {
83+
return [false, [
84+
'error' => 'invalid_response',
85+
'error_description' => 'The response from the authorization server did not return an authorization code or error information'
86+
]];
87+
}
88+
89+
if(!isset($params['state'])) {
90+
return [false, [
91+
'error' => 'missing_state',
92+
'error_description' => 'The authorization server did not return the state parameter'
93+
]];
94+
}
95+
96+
if($params['state'] != $_SESSION['indieauth_state']) {
97+
return [false, [
98+
'error' => 'invalid_state',
99+
'error_description' => 'The authorization server returned an invalid state parameter'
100+
]];
101+
}
102+
103+
if(isset($_SESSION['indieauth_token_endpoint'])) {
104+
$verify = self::getAccessToken($_SESSION['indieauth_token_endpoint'], $params['code'], $_SESSION['indieauth_url'], self::$redirectURL, self::$clientID);
105+
} else {
106+
$verify = self::verifyIndieAuthCode($_SESSION['indieauth_authorization_endpoint'], $params['code'], null, self::$redirectURL, self::$clientID);
107+
}
108+
109+
$expectedURL = $_SESSION['indieauth_url'];
110+
unset($_SESSION['indieauth_url']);
111+
unset($_SESSION['indieauth_state']);
112+
unset($_SESSION['indieauth_authorization_endpoint']);
113+
unset($_SESSION['indieauth_token_endpoint']);
114+
115+
if(!isset($verify['me'])) {
116+
return [false, [
117+
'error' => 'indieauth_error',
118+
'error_description' => 'The authorization code was not able to be verified'
119+
]];
120+
}
121+
122+
// Check that the returned URL is on the same domain as the original URL
123+
if(parse_url($verify['me'], PHP_URL_HOST) != parse_url($expectedURL, PHP_URL_HOST)) {
124+
return [false, [
125+
'error' => 'invalid user',
126+
'error_description' => 'The domain for the user returned did not match the domain of the user initially signing in'
127+
]];
128+
}
129+
130+
$verify['me'] = self::normalizeMeURL($verify['me']);
131+
132+
return [$verify, false];
133+
}
134+
135+
15136
public static function setUpHTTP() {
16137
// Unfortunately I've seen a bunch of websites return different content when the user agent is set to something like curl or other server-side libraries, so we have to pretend to be a browser to successfully get the real HTML
17138
if(!isset(self::$http)) {
@@ -102,6 +223,41 @@ private static function _extractEndpointFromHTML($html, $url, $name) {
102223
return false;
103224
}
104225

226+
public static function resolveMeURL($url, $max=4) {
227+
// Follow redirects and return the identity URL at the end of the chain.
228+
// Permanent redirects affect the identity URL, temporary redirects do not.
229+
// A maximum of N redirects will be followed.
230+
self::setUpHTTP();
231+
232+
$oldmax = self::$http->_max_redirects;
233+
self::$http->_max_redirects = 0;
234+
235+
$i = 0;
236+
while($i < $max) {
237+
$result = self::$http->head($url);
238+
if($result['code'] == 200) {
239+
break;
240+
} elseif($result['code'] == 301) {
241+
// Follow the permanent redirect
242+
if(isset($result['headers']['Location']) && is_string($result['headers']['Location'])) {
243+
$url = $result['headers']['Location'];
244+
} else {
245+
$url = false; // something wrong with the Location header
246+
}
247+
} elseif($result['code'] == 302) {
248+
// Temporary redirect, so abort with the current URL
249+
break;
250+
} else {
251+
$url = false;
252+
break;
253+
}
254+
$i++;
255+
}
256+
257+
self::$http->_max_redirects = $oldmax;
258+
return $url;
259+
}
260+
105261
public static function discoverAuthorizationEndpoint($url) {
106262
return self::_discoverEndpoint($url, 'authorization_endpoint');
107263
}
@@ -209,7 +365,7 @@ public static function build_url($parsed_url) {
209365
public static function getAccessToken($tokenEndpoint, $code, $me, $redirectURI, $clientID, $debug=false) {
210366
self::setUpHTTP();
211367

212-
$response = self::$http->post($url, http_build_query(array(
368+
$response = self::$http->post($tokenEndpoint, http_build_query(array(
213369
'grant_type' => 'authorization_code',
214370
'me' => $me,
215371
'code' => $code,
@@ -237,11 +393,11 @@ public static function getAccessToken($tokenEndpoint, $code, $me, $redirectURI,
237393
}
238394
}
239395

240-
// Used by a token endpoint to verify the auth code
396+
// Note: the $me parameter is deprecated and you can just pass null instead
241397
public static function verifyIndieAuthCode($authorizationEndpoint, $code, $me, $redirectURI, $clientID, $debug=false) {
242398
self::setUpHTTP();
243399

244-
$response = self::$http->post($url, http_build_query(array(
400+
$response = self::$http->post($authorizationEndpoint, http_build_query(array(
245401
'code' => $code,
246402
'redirect_uri' => $redirectURI,
247403
'client_id' => $clientID

0 commit comments

Comments
 (0)