cb698454c0e88bdbfe7932bc705e9ccb013b95ed
[indieauth-openid.git] / www / index.php
1 <?php
2 /**
3  * IndieAuth to OpenID proxy.
4  * Proxies IndieAuth authorization requests to one's OpenID server
5  *
6  * PHP version 5
7  *
8  * @package indieauth-openid
9  * @author  Christian Weiske <cweiske@cweiske.de>
10  * @license http://www.gnu.org/licenses/agpl.html GNU AGPL v3
11  * @link    http://indiewebcamp.com/login-brainstorming
12  * @link    http://indiewebcamp.com/authorization-endpoint
13  * @link    http://indiewebcamp.com/auth-brainstorming
14  * @link    https://indieauth.com/developers
15  */
16 header('IndieAuth: authorization_endpoint');
17 if (($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
18     && count($_GET) == 0
19 ) {
20     include 'about.php';
21     exit();
22 }
23
24 require_once 'Net/URL2.php';
25 require_once 'OpenID.php';
26 require_once 'OpenID/RelyingParty.php';
27 require_once 'OpenID/Message.php';
28 require_once 'OpenID/Exception.php';
29
30 function loadDb()
31 {
32     $pharFile = \Phar::running();
33     if ($pharFile == '') {
34         $dsn = 'sqlite:' . __DIR__ . '/../data/tokens.sq3';
35         $cfgFilePath = __DIR__ . '/config.php';
36     } else {
37         //remove phar:// from the path
38         $dir = dirname(substr($pharFile, 7)) . '/';
39         $dsn = 'sqlite:' . $dir . '/tokens.sq3';
40         $cfgFilePath = substr($pharFile, 7) . '.config.php';
41     }
42     //allow overriding DSN
43     if (file_exists($cfgFilePath)) {
44         include $cfgFilePath;
45     }
46
47     $db = new PDO($dsn);
48     $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
49     $db->exec("CREATE TABLE IF NOT EXISTS authtokens(
50 code TEXT,
51 me TEXT,
52 redirect_uri TEXT,
53 client_id TEXT,
54 state TEXT,
55 created DATE
56 )");
57     //clean old tokens
58     $stmt = $db->prepare('DELETE FROM authtokens WHERE created < :created');
59     $stmt->execute(array(':created' => date('c', time() - 60)));
60
61     return $db;
62 }
63
64 function create_token($me, $redirect_uri, $client_id, $state)
65 {
66     $code = base64_encode(openssl_random_pseudo_bytes(32));
67     $db = loadDb();
68     $db->prepare(
69         'INSERT INTO authtokens (code, me, redirect_uri, client_id, state, created)'
70         . ' VALUES(:code, :me, :redirect_uri, :client_id, :state, :created)'
71     )->execute(
72         array(
73             ':code' => $code,
74             ':me' => $me,
75             ':redirect_uri' => $redirect_uri,
76             ':client_id' => $client_id,
77             ':state' => (string) $state,
78             ':created' => date('c')
79         )
80     );
81     return $code;
82 }
83
84 function validate_token($code, $redirect_uri, $client_id, $state)
85 {
86     $db = loadDb();
87     $stmt = $db->prepare(
88         'SELECT me FROM authtokens WHERE'
89         . ' code = :code'
90         . ' AND redirect_uri = :redirect_uri'
91         . ' AND client_id = :client_id'
92         . ' AND state = :state'
93         . ' AND created >= :created'
94     );
95     $stmt->execute(
96         array(
97             ':code'         => $code,
98             ':redirect_uri' => $redirect_uri,
99             ':client_id'    => $client_id,
100             ':state'        => (string) $state,
101             ':created'      => date('c', time() - 60)
102         )
103     );
104     $row = $stmt->fetch(PDO::FETCH_ASSOC);
105
106     $stmt = $db->prepare('DELETE FROM authtokens WHERE code = :code');
107     $stmt->execute(array(':code' => $code));
108
109     if ($row === false) {
110         return false;
111     }
112     return $row['me'];
113 }
114
115 function error($msg)
116 {
117     header('HTTP/1.0 400 Bad Request');
118     header('Content-type: text/plain; charset=utf-8');
119     echo $msg . "\n";
120     exit(1);
121 }
122
123 function verifyUrlParameter($givenParams, $paramName)
124 {
125     if (!isset($givenParams[$paramName])) {
126         error('"' . $paramName . '" parameter missing');
127     }
128     $url = parse_url($givenParams[$paramName]);
129     if (!isset($url['scheme'])) {
130         error('Invalid URL in "' . $paramName . '" parameter: scheme missing');
131     }
132     if (!isset($url['host'])) {
133         error('Invalid URL in "' . $paramName . '" parameter: host missing');
134     }
135
136     return $givenParams[$paramName];
137 }
138
139 function getBaseUrl()
140 {
141     if (!isset($_SERVER['REQUEST_SCHEME'])) {
142         $_SERVER['REQUEST_SCHEME'] = 'http';
143     }
144     $file = preg_replace('/[?#].*$/', '', $_SERVER['REQUEST_URI']);
145     return $_SERVER['REQUEST_SCHEME'] . '://'
146         . $_SERVER['HTTP_HOST']
147         . $file;
148 }
149
150 session_start();
151 $returnTo = getBaseUrl();
152 $realm    = getBaseUrl();
153
154 if (isset($_GET['openid_mode']) && $_GET['openid_mode'] != '') {
155     //verify openid response
156     if (!count($_POST)) {
157         list(, $queryString) = explode('?', $_SERVER['REQUEST_URI']);
158     } else {
159         $queryString = file_get_contents('php://input');
160     }
161
162     $message = new \OpenID_Message($queryString, \OpenID_Message::FORMAT_HTTP);
163     $id      = $message->get('openid.claimed_id');
164     if (OpenID::normalizeIdentifier($id) != OpenID::normalizeIdentifier($_SESSION['me'])) {
165         error(
166             sprintf(
167                 'Given identity URL "%s" and claimed OpenID "%s" do not match',
168                 $_SESSION['me'], $id
169             )
170         );
171     }
172     try {
173         $o = new \OpenID_RelyingParty($returnTo, $realm, $_SESSION['me']);
174         $result = $o->verify(new \Net_URL2($returnTo . '?' . $queryString), $message);
175
176         if ($result->success()) {
177             $token = create_token(
178                 $_SESSION['me'], $_SESSION['redirect_uri'],
179                 $_SESSION['client_id'], $_SESSION['state']
180             );
181             //redirect to indieauth
182             $url = new Net_URL2($_SESSION['redirect_uri']);
183             $url->setQueryVariable('code', $token);
184             $url->setQueryVariable('me', $_SESSION['me']);
185             $url->setQueryVariable('state', $_SESSION['state']);
186             header('Location: ' . $url->getURL());
187             exit();
188         } else {
189             error('Error verifying OpenID login: ' . $result->getAssertionMethod());
190         }
191     } catch (OpenID_Exception $e) {
192         error('Error verifying OpenID login: ' . $e->getMessage());
193     } catch (Exception $e) {
194         error(get_class($e) . ': ' . $e->getMessage());
195     }
196 }
197
198 if ($_SERVER['REQUEST_METHOD'] == 'GET') {
199     $me           = verifyUrlParameter($_GET, 'me');
200     $redirect_uri = verifyUrlParameter($_GET, 'redirect_uri');
201     $client_id    = verifyUrlParameter($_GET, 'client_id');
202     $state        = null;
203     if (isset($_GET['state'])) {
204         $state = $_GET['state'];
205     }
206     $response_type = 'id';
207     if (isset($_GET['response_type'])) {
208         $response_type = $_GET['response_type'];
209     }
210     if ($response_type != 'id') {
211         error('unsupported response_type: ' . $response_type);
212     }
213
214     $_SESSION['me']           = $me;
215     $_SESSION['redirect_uri'] = $redirect_uri;
216     $_SESSION['client_id']    = $client_id;
217     $_SESSION['state']        = $state;
218
219     try {
220         $o = new \OpenID_RelyingParty($returnTo, $realm, $me);
221         //if you get timeouts (errors like
222         // OpenID error: Request timed out after 3 second(s)
223         //) then uncomment the following line which disables
224         // all timeouts:
225         //$o->setRequestOptions(array('follow_redirects' => true));
226         $authRequest = $o->prepare();
227         $url = $authRequest->getAuthorizeURL();
228         header("Location: $url");
229         exit(0);
230     } catch (OpenID_Exception $e) {
231         error('OpenID error: ' . $e->getMessage());
232     } catch (Exception $e) {
233         error(get_class($e) . ': ' . $e->getMessage());
234     }
235 } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
236     $redirect_uri = verifyUrlParameter($_POST, 'redirect_uri');
237     $client_id    = verifyUrlParameter($_POST, 'client_id');
238     $state        = null;
239     if (isset($_POST['state'])) {
240         $state = $_POST['state'];
241     }
242     if (!isset($_POST['code'])) {
243         error('"code" parameter missing');
244     }
245     $token = $_POST['code'];
246
247     $me = validate_token($token, $redirect_uri, $client_id, $state);
248     if ($me === false) {
249         header('HTTP/1.0 400 Bad Request');
250         echo "Validating token failed\n";
251         exit(1);
252     }
253     header('Content-type: application/x-www-form-urlencoded');
254     echo 'me=' . urlencode($me);
255 }
256 ?>